0

Why the methods from Math is faster than the bitwise operators in JavaScript?

For example:

  1. Math.floor vs | 0 (http://jsperf.com/floor-vs-bitwise22)
  2. Math.max vs a ^ ((a ^ b) & -(a < b)) (http://jsperf.com/max-vs-bitwise-22)
  3. Math.min vs b ^ ((a ^ b) & -(a < b)) (http://jsperf.com/min-vs-bitwise-22)

I want to know at implementation level, because executing something at bit level I thought I would jump all the translations and calls that a computer is gonna do to make what I want, at bit level, which is less CPU operations.

What's happening?

7
  • Can you show how you're calculating the timings? Commented May 13, 2015 at 12:54
  • 3
    That depends on the browser engine... Commented May 13, 2015 at 12:55
  • 1
    2 and 3 are obviously slower because you're performing much more in JavaScript, the question becomes "Why is Math.floor faster than the | operator?", and comes down to both browser engine and maybe floor saves a little time by not needing to do a int32 conversion Commented May 13, 2015 at 13:08
  • 1
    Too many operations. Check this out: jsperf.com/max-vs-bitwise-22/2 Commented May 13, 2015 at 13:09
  • 1
    A simple implementation of Math.max (or Math.min) uses just one comparison to get at the answer. Your code uses 5 calculations. Commented May 13, 2015 at 13:09

2 Answers 2

2

… I thought I would jump all the translations and calls that a computer is gonna do …

Modern optimizing JavaScript engines can perform surprising optimizations. In this case, I'd guess that the Math calls simply get inlined.

If you really want to know what's going on, and not just guess, then you'll have to look at the end product of what your optimizer generates. See e.g. How can I see the machine code generated by v8? for details on that.

Sign up to request clarification or add additional context in comments.

Comments

0

JS numbers are floating-point, except when the JIT optimizer can use an integer type inside a loop or function. As such, instructions like x86 minsd and maxsd can do the job in one single-uop instruction with good throughput and fairly good latency (https://uops.info/). (More efficiently than for integers in general-purpose registers, ironically, although cmp / cmov is fairly efficient there, too and still only 2 uops instead of 1.)

A 2's complement bithack hides the real logic from the JIT optimizer so you're unlikely to have it turn that mess back into x86 cmp / cmov or AArch64 cmp / csel or whatever. (Or in a simple test where the same input is always the max, branchy code-gen can look good, when it would mispredict in real cases. Of course with constant inputs you'd expect things to optimize away after constant-propagation.)


IDK why floor would be faster than | 0. Check the asm to make sure neither optimized away. |0 should be able to use x86 cvttsd2si (convert (with Truncation) scalar double to signed/scalar integer.

You'd expect a JIT to use SSE4.1 roundsd for floor on new-enough x86 CPUs, which is less efficient (2 uops on mainstream Intel CPUs.) Even if the result is used with integer stuff after floor, rounding toward -Infinity is different from rounding toward 0, so it can't just use cvttsd2si. If AVX-512 was available, that could allow a rounding-mode override for vcvtsd2si (use the current rounding mode, or an AVX-512 override) but I doubt that was happening in 2015. Changing the current FP rounding mode for a loop would be possible, but unlikely.

Both are of course no-ops if the value is already an integer.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.