警惕位移運算<<的坑

zy445566發表於2018-10-10

在一次程式碼優化的過程中把

// a,b都為正整數且大於0
while (a>=b) {
    a-=b;
}
複製程式碼

優化為

// a,b都為正整數且大於0
while (a>=b) {
    let tmpB = b;
    while (a>=tmpB) {
        let tmpShiftB = tmpB<<1;
        if (a>=tmpShiftB) {
            tmpB = tmpShiftB;
        } else {
            a-=tmpB;
        }
    }
}
複製程式碼

本意是如果a很大,b很小的情況,也能把快速進行減法運算。
但後來發現a一旦很大,就會死迴圈,這個還是概率出現。
後來debug發現到一定時候tmpShiftB會變成0,導致死迴圈。
當時就想肯定是位移符的坑,後來發現有下面問題

tmpShiftB到達2147483648後左移一位變0
複製程式碼

我一看樂了,這不是2的-32次方嘛,肯定是JS當有符號的int32型算了,然後溢位了,八成是谷歌引擎的的BUG!
但想想別高興的太早,看看標準怎麼說,畢竟制定標準的人也賊坑。讓我們看看標準怎麼寫的。

ecma-262的第9版左位移說明

12.9.3The Left Shift Operator ( << )
NOTE
Performs a bitwise left shift operation on the left operand by the amount specified by the right operand.

12.9.3.1Runtime Semantics: Evaluation
ShiftExpression:ShiftExpression<<AdditiveExpression
Let lref be the result of evaluating ShiftExpression.
Let lval be ? GetValue(lref).
Let rref be the result of evaluating AdditiveExpression.
Let rval be ? GetValue(rref).
Let lnum be ? ToInt32(lval).
Let rnum be ? ToUint32(rval).
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.
複製程式碼

翻譯下來大概意思是:

左表示式<<右表示式

左表示式結果轉成Int32(有符號的int的32位型別),結果取名lnum

右表示式結果轉成Uint32(無符號的int的32未型別),同時進行& 0x1F運算(即保留2進位制的後5位,再白話一點就是保留32以內的位的數值,但和%32又有些不同),結果取名shiftCount

最後再把lnum左位移shiftCount位,並把這個結果再轉換成有符號的int的32位型別

複製程式碼

一看下來壞了,果然是標準坑爹,且不說左表示式結果轉成了有符號的int的32位型別,位移後的結果也給轉成了有符號的int的32位型別。果然是標準坑爹。

結論

看來以後使用左位移符都要小心了,只適用於int32的範圍(-2^32~2^32),要是有可能超過,看來是斷斷不能用了。看來JS的世界精確整數也不一定就是(-2^53~2^53)範圍了。

相關文章