Is bit shifting in JavaScript fast?
As a programmer, we know that bitwise operators are fast. And to optimize our code, we usually replace the regular operators like multiply, divide to bit shifting.
On currently available processors, a bit-wise shift instruction is faster than a multiply instruction and can be used to multiply (shift left) and divide (shift right) by powers of two. - Wikipedia
For example, we can use left shift instead of multiply:
$ (4 \times 2) = (4 \ll 1) = 8 $
Or we can use right shift for division:
$ (16 \div 4) = (16 \gg 2) = 4 $
In fact, some modern compilers automatically convert multiplication to left shift, for example, let's take a look at this simple C program:
shift_test.c
int main() {
int a = 10;
return a * 2;
}
If you compile it with Apple's LLVM compiler (on macOS, gcc
is just a symlink to LLVM clang
):
gcc -S shift_test.c -o shift.s
The assembler code should look like:
//allocate a variable, bind it with 10
movl $10, -8(%rbp)
movl -8(%rbp), %eax
// shift 1 bit left (a << 1)
shll $1, %eax
// return the value
popq %rbp
retq
So, how about JavaScript? Let's do some benchmark.
We will benchmark this two cases, and you can run it yourself here on JSPerf:
Case 1: Using multiply operator
let a = 10;
a * 2;
Case 2: Using left shift
let a = 10;
a << 1;
And here's the result on some popular browsers:
Browser | a * 2 | a << 1 |
---|---|---|
Firefox 57.0.0 | 1,728,015,792 ops/sec (faster) | 1,707,778,280 ops/sec |
Chrome 61.0.3163 | 633,856,007 ops/sec (faster) | 621,598,716 ops/sec |
Safari 11.0.0 | 1,691,557,398 ops/sec (faster) | 1,644,711,522 ops/sec |
Surprisingly, all of the browsers give us the same result, left shift is slower than multiply. Why?
The reason is hiding behind the implementation of these operators. Let's take a look at the ECMAScript specs to find the answer.
So this is the algorithm behind left shift operator (LEFT << RIGHT):
- Let lref be the result of evaluating LEFT.
- Let lval be GetValue(lref).
- ReturnIfAbrupt(lval).
- Let rref be the result of evaluating RIGHT.
- Let rval be GetValue(rref).
- ReturnIfAbrupt(rval).
- Let lnum be
ToInt32(lval)
. - ReturnIfAbrupt(lnum).
- Let rnum be
ToUint32(rval)
. - ReturnIfAbrupt(rnum).
- Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute
rnum & 0x1F
. - Return the result of left shifting lnum by shiftCount bits. The result is a signed 32-bit integer.
And the algorithm behind multiply operator (LEFT * RIGHT):
- Let left be the result of evaluating LEFT.
- Let leftValue be GetValue(left).
- ReturnIfAbrupt(leftValue).
- Let right be the result of evaluating RIGHT.
- Let rightValue be GetValue(right).
- Let lnum be
ToNumber(leftValue)
. - ReturnIfAbrupt(lnum).
- Let rnum be
ToNumber(rightValue)
. - ReturnIfAbrupt(rnum).
- Return the result of applying the operator (*, /, or %) to lnum and rnum (in this case is multiply).
That's a lot of things happen in just an operation! And let's look closer at the two function calls ToInt32()
, ToUint32()
call of left shift, compared to the two ToNumber()
calls of multiply.
The algorithm behind ToInt32() function:
- Let number be
ToNumber(argument)
. - ReturnIfAbrupt(number).
- If number is NaN, +0, −0, +∞, or −∞, return +0.
- Let int be the mathematical value that is the same sign as number and whose magnitude is
floor(abs(number))
. - Let int32bit be int modulo $2^{32}$.
- If int32bit ≥ $2^{31}$, return int32bit − $2^{32}$, otherwise return int32bit.
And the algorithm behind ToUint32():
- Let number be
ToNumber(argument)
. - ReturnIfAbrupt(number).
- If number is NaN, +0, −0, +∞, or −∞, return +0.
- Let int be the mathematical value that is the same sign as number and whose magnitude is
floor(abs(number))
. - Let int32bit be int modulo $2^{32}$.
- Return
int32bit
.
These implementations even make the call to ToNumber()
, on the other hand, this is the implementation of ToNumber()
if the argument is a number:
- Return argument (no conversion).
Nothing happens, just return the value directly. So, by analyzing the algorithms, we can definitely say that ToNumber()
is way faster than ToInt32()
and ToUint32()
. And so on, we can say the multiply algorithm is faster than left shift algorithm.
It's very interesting that it turned out bitwise shifting is not as fast as we think because of the underlying implementation of JavaScript.
Thank you so much for reading this. I hope you found this post helpful. Any feedback would be greatly appreciated!