purescript icon indicating copy to clipboard operation
purescript copied to clipboard

Use Math.imul(a, b) instead of a * b | 0

Open rightfold opened this issue 7 years ago • 58 comments

Math.imul correctly handles arithmetic overflow, whereas | 0 just discards the most significant bits. This affects purescript-prelude as well as the inliner. Using Math.imul would also make Semiring Int lawful.

rightfold avatar Jul 09 '17 17:07 rightfold

It'd be better if we can auto-polyfill this or something, since imul does not have solid browser support. It's not even ES5, nevermind the currently-supported ES3. 😕

garyb avatar Jul 09 '17 17:07 garyb

There's already an issue for this, isn't there?

hdgarrood avatar Jul 09 '17 18:07 hdgarrood

Ok, there's https://github.com/purescript/purescript/issues/2921, but it was closed, so maybe let's just use this one.

MDN's Math.imul page suggests the following polyfill:

Math.imul = Math.imul || function(a, b) {
  var ah = (a >>> 16) & 0xffff;
  var al = a & 0xffff;
  var bh = (b >>> 16) & 0xffff;
  var bl = b & 0xffff;
  // the shift by 0 fixes the sign on the high part
  // the final |0 converts the unsigned value into a signed value
  return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)|0);
};

hdgarrood avatar Jul 09 '17 18:07 hdgarrood

Come to think of it, we shouldn't mutate Math, and we can't exactly inline all that every time someone uses Int's *. Without a stdlib it's hard to see how to achieve this without either giving up inlining or dropping support for platforms which don't provide Math.imul.

hdgarrood avatar Jul 15 '17 14:07 hdgarrood

If we had a general inliner i.e. one that didn't only use a set of special cases built in to the compiler, this would probably be doable, though.

hdgarrood avatar Jul 15 '17 14:07 hdgarrood

@hdgarrood Do we have an official stance on JS engine support? It'd be nice to make a requirement that can shift over time like "browsers with >0.1% market share (by page loads) globally" or "browsers with >0.5% market share (by installations) in every individual market" or "browsers released in the last 5 years". A target of "just ES3" is problematic both because it doesn't actually represent meaningful browser support, and it doesn't allow us to improve/adapt as time passes and things get better.

michaelficarra avatar Jul 15 '17 14:07 michaelficarra

I think it's reasonable to say that PureScript does not require a runtime, but relies on some set of easily polyfilled features, such as Math.imul or Object.keys.

natefaubion avatar Jul 15 '17 15:07 natefaubion

I don't think we really have an official stance. The compiler generates ES3, most of the Node libraries assume node >= 4, and I guess from @natefaubion's comment that maps assumes Object.keys, so there's a mix. I think what you say makes sense though. I guess we haven't really needed to develop a clear stance as I don't think there are that many situations where we can't reasonably accomplish what we want using just ES3.

Would we be able to justify using Math.imul with any of those stances you gave, incidentally?

hdgarrood avatar Jul 15 '17 15:07 hdgarrood

@hdgarrood I personally don't see a problem with mutating Math

The polyfill will only be added if Math.imul doesn't already exist, and the polyfill should have the correct behavior (it's just slower than native support), so I don't think the polyfill will cause any problems.

@michaelficarra Math.imul is supported in every browser except IE11.

This is because Math.imul is super easy for browsers to support (it's just standard C multiplication), and it was necessary for asm.js support, so all the browsers added it in really fast.

IE11 has ~4% market share, so I don't think it's viable to drop support for it right now.

Pauan avatar Jul 15 '17 18:07 Pauan

Is there anything wrong with providing this as a library function in math for now?

paf31 avatar Jul 15 '17 21:07 paf31

Only that it doesn't fix (*) for integers.

garyb avatar Jul 15 '17 21:07 garyb

If we mutated Math I reckon that in 99% of projects it would work fine, but I'm certain there will be people who have some weird setup that makes strange decisions based on the value of this property and would send them (and possibly also us) on a debugging goose chase for hours. It's maybe justifiable to mutate globals if you're writing a framework but I think it's poor etiquette to do so in a library. I just remember too many instances of this kind of code breaking things in totally unpredictable and unexpected ways back when I was a Rubyist.

If we were going to provide a temporary opt-in solution I'd rather do it in the form of a newtype over Int than a function in math, as I think that would be more useful. But what would we be waiting for if we did that? For IE's market share to die off sufficiently so that we could assume the presence of Math.imul?

hdgarrood avatar Jul 15 '17 22:07 hdgarrood

It wouldn't fix (*) but I think that might be okay. We could just document that Int works most of the time but that there are catches, and that you can choose to use imul via a newtype if you want the fix at the cost of compatibility.

paf31 avatar Jul 16 '17 00:07 paf31

That sounds like a good thing to do for now — the Semiring documentation is already sufficient in that sense I think. I am still quite keen to get the default Semiring Int instance fixed, though.

hdgarrood avatar Jul 16 '17 00:07 hdgarrood

@hdgarrood If some code breaks because Math.imul exists, then it's already broken on all modern browsers. The only possible way it could break is if it relied upon the string representation of the Math.imul function (which is a horrible thing to do in any case).

Yes global mutation has been abused in the past in JavaScript, and of course I recommend against it, but in this specific case, it should be fine.

Having said that, I'm okay with putting it in math and using a newtype

Pauan avatar Jul 16 '17 07:07 Pauan

I'm not convinced, personally - I've been bitten too many times by conflating "this is safe" with "I cannot think of a way this could be unsafe". And it's not just about that property existing - it seems a reasonable assumption to make that the number of properties of Math isn't going to change over the course of running the code on a webpage, for instance, but this would no longer be true. It probably only is horrible code which could be affected but the quality of any other code on a page containing some PureScript code isn't any of our business, imo.

hdgarrood avatar Jul 16 '17 08:07 hdgarrood

If we document that overflow on multiplication gives an unspecified result then switching to Math.imul later won't be a breaking change.

rightfold avatar Aug 13 '17 09:08 rightfold

Ok, so to recap: since no version of IE supports Math.imul, it will probably be a quite long time before we can just start using it without having to worry about compatibility.

We could say that PureScript depends on some easily-polyfillable features, and just let the user polyfill themselves if they want IE support, but for something as basic as integer multiplication I think this would be a poor UX for devs not familiar with PureScript and/or this specific issue.

We could polyfill it ourselves by getting the compiler to insert some code which mutates window.Math to add the imul property if there are any uses of multiplication with the Int type, but I'd rather not do this because I think it is poor etiquette. It also sounds like it could be quite fiddly, and I think it goes against the "no stdlib" principle.

One alternative option that has just occurred to me is that we change the definition of intMul so that it uses Math.imul via the FFI if it is there, or otherwise it uses a polyfill, but whichever path is taken, it should be done without mutating window.Math. See this PR for how we might achieve that (thanks @matthewleon :smile:). Then, we'd need to improve the inliner so that this could be inlined properly (i.e. by generating a require statement if necessary). I think this would be my preferred option, although I'm not sure how difficult modifying the inliner would be.

hdgarrood avatar Nov 20 '17 17:11 hdgarrood

Another option is to make it explicit that the user needs to polyfill things, either by moving it into its own module (Math.NonPortable or something), or by using a type class à la Partial, or both.

paf31 avatar Nov 20 '17 17:11 paf31

Anything bad about changing the definition of intMul https://github.com/purescript/purescript-prelude/blob/v4.1.0/src/Data/Semiring.js#L10 to be the defaulted polyfill that @hdgarrood posted?

exports.imul = Math.imul || function(a, b) {
  var ah = (a >>> 16) & 0xffff;
  var al = a & 0xffff;
  var bh = (b >>> 16) & 0xffff;
  var bl = b & 0xffff;
  // the shift by 0 fixes the sign on the high part
  // the final |0 converts the unsigned value into a signed value
  return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)|0);
};
foreign import imul :: Fn2 Int Int Int
instance semiringInt :: Semiring Int where
  add = intAdd
  zero = 0
  mul = runFn2 imul
  one = 1

LiamGoodacre avatar Sep 25 '18 20:09 LiamGoodacre

Ah sorry @hdgarrood, I didn't read your latest comment properly, you already suggested that ^

LiamGoodacre avatar Sep 25 '18 20:09 LiamGoodacre

Inlining?

paf31 avatar Sep 25 '18 21:09 paf31

I guess we would inline from x * y to runFn2 imul x y? and then delete it when we get better elimination of dictionaries

LiamGoodacre avatar Sep 26 '18 12:09 LiamGoodacre

That sounds like quite the deoptimization for quite a lot of simple numeric code.

paf31 avatar Sep 26 '18 15:09 paf31

I've set up a jsperf (https://jsperf.com/imul-or-mul). Unless it's a bad test, I get unexpectedly comparable performance between the following on Chrome:

(x * y) | 0
((x | 0) * (y | 0)) | 0
Math.imul(x | 0, y | 0) | 0
imulPolyfill(x | 0, y | 0) | 0

And on Firefox, all of the cases have comparable performance to the best of Chrome.

LiamGoodacre avatar Sep 26 '18 18:09 LiamGoodacre

This is on Linux Mint 18.3, not Ubuntu. firefox62-linuxmint18-3

JordanMartinez avatar Sep 26 '18 19:09 JordanMartinez

Perhaps a slightly better structure of test? https://jsperf.com/mul-imul-poly-2

But yeah, under the assumption that any of these tests are useful, it seems to me that imulPolyfill(x | 0, y | 0) | 0 would be an acceptable choice of implementation.

LiamGoodacre avatar Oct 02 '18 09:10 LiamGoodacre

There's an alternative implementation from MDN (and of course we are not mutating Math; this is just verbatim):

if (!Math.imul) Math.imul = function(opA, opB) {
  opB |= 0; // ensure that opB is an integer. opA will automatically be coerced.
  // floating points give us 53 bits of precision to work with plus 1 sign bit
  // automatically handled for our convienence:
  // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001
  //    0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
  var result = (opA & 0x003fffff) * opB;
  // 2. We can remove an integer coersion from the statement above because:
  //    0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001
  //    0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
  if (opA & 0xffc00000 /*!== 0*/) result += (opA & 0xffc00000) * opB |0;
  return result |0;
};

Those polyfills would basically only target IE, and its JScript engine wouldn't perform any better than this on the more widely known one unless the engine has any optimizations for integer arithmetic, which seems unlikely.

@LiamGoodacre I think the Math.imul option needs to be reevaluated with a correction to fetch the imul property and store it in a closure beforehand, pulling the property get overhead out of the loop. And it's what we're going to do anyway, since that way the mul function in the Semiring Int instance would preserve its referential transparency even if Math.imul is later set to any other value. That is, assuming the property is kept its initial value upon PureScript's runtime initialization.

iamahuman avatar May 24 '19 13:05 iamahuman

Is there anything to resolve here? I think we should make a decision one way or the other.

natefaubion avatar Feb 06 '20 18:02 natefaubion

I think we should definitely fix Int multiplication one way or another. My preference is this:

One alternative option that has just occurred to me is that we change the definition of intMul so that it uses Math.imul via the FFI if it is there, or otherwise it uses a polyfill, but whichever path is taken, it should be done without mutating window.Math. See this PR for how we might achieve that (thanks matthewleon). Then, we'd need to improve the inliner so that this could be inlined properly (i.e. by generating a require statement if necessary). I think this would be my preferred option, although I'm not sure how difficult modifying the inliner would be.

hdgarrood avatar Feb 08 '20 12:02 hdgarrood