在如今的主流 Web 編程語言中,如 PHP 或 Python 等究抓,都包含冪運算符(一般來說符號是 ^ 或者 **
)猾担。而最新的 ES7 中也增加了對冪運算的支持,使用符號 **
刺下,最新的 Chrome 已經(jīng)提供了對冪運算的支持垒探。
但是在 javascript 中,**
運算有時候并不等于 Math.pow(a,b)
怠李,在最新的 Chrome 55 中:
Math.pow(99,99)
的結(jié)果是 3.697296376497263e+197,
但是 99**99
的結(jié)果是 3.697296376497268e+197蛤克。
兩者并不相等
3.697296376497263e+197
3.697296376497268e+197
而且 Math.pow(99,99) - 99**99
的結(jié)果也不是 0 而是 -5.311379928167671e+182捺癞。
因此我們猜測,**
操作符只是冪運算的另一個實現(xiàn)构挤。但是當(dāng)我們寫一個函數(shù)時髓介,冪運算又表現(xiàn)出詭異的特性:
function diff(x) {
return Math.pow(x,x) - x**x;
}
調(diào)用 diff(99)
返回 0。WTF筋现?兩者又相等了唐础!
猜猜下面代碼輸出什么?
var x = 99;
x**x - 99**99;
這段代碼的運行結(jié)果是 -5.311379928167671e+182矾飞。
這簡直是薛定諤的冪一膨。
究其原因,V8 引擎使用了常量折疊(const folding)洒沦。常量折疊是一種編譯器的編譯優(yōu)化技術(shù)豹绪。
考慮如下代碼:
for (let i = 0; i < 100*100*100; i++){
// 循環(huán)體
}
該循環(huán)的條件 i<100*100*100
是一個表達(dá)式(expression),如果放到判斷時再求值那么 100*100*100
的計算將會進(jìn)行 1000000 次申眼。如果編譯器在語法分析階段進(jìn)行常量合并瞒津,該循環(huán)將會變?yōu)檫@樣:
for (let i = 0; i < 1000000; i++){
// 循環(huán)體
}
而上文中提到的 99**99
的計算也使用到了常量折疊。也就是說 99**99
是在編譯時進(jìn)行計算(常量折疊)括尸,而 Math.pow
總是在運行時進(jìn)行計算巷蚪。當(dāng)我們使用變量進(jìn)行冪運算時(例 a**b
)此時不存在常量折疊,因此 a ** b
的值在運行時進(jìn)行計算濒翻,** 會被編譯成 Math.pow
調(diào)用屁柏。
在源碼 src/parsing/parser.cc 文件中啦膜,編譯時計算代碼:
case Token::EXP: {
double value = Pow(x_val, y_val);
int int_value = static_cast<int>(value);
*x = factory()->NewNumberLiteral(
int_value == value && value != -0.0 ? int_value : value, pos,
has_dot);
return true;
可以看到使用了 Pow 函數(shù)計算了冪運算的求值結(jié)果。Pow 是一個 inline 的函數(shù)前联,內(nèi)部做了一些常規(guī)優(yōu)化功戚,對不能優(yōu)化的情況則使用了 std::pow(x, y)
來計算最終結(jié)果。
而 Math.pow
的算法為:
// ES6 section 20.2.2.26 Math.pow ( x, y )
TF_BUILTIN(MathPow, CodeStubAssembler) {
Node* x = Parameter(1);
Node* y = Parameter(2);
Node* context = Parameter(5);
Node* x_value = TruncateTaggedToFloat64(context, x);
Node* y_value = TruncateTaggedToFloat64(context, y);
Node* value = Float64Pow(x_value, y_value);
Node* result = ChangeFloat64ToTagged(value);
Return(result);
}
可見兩者使用了不同的算法似嗤。但是當(dāng)不做常量折疊的時候啸臀,**
則轉(zhuǎn)換成了 Math.pow
函數(shù)調(diào)用:
Expression* Parser::RewriteExponentiation(
Expression* left,
Expression* right,
int pos) {
ZoneList<Expression*>* args = new (zone()) ZoneList<Expression*>(2, zone());
args->Add(left, zone());
args->Add(right, zone());
return factory()->NewCallRuntime(Context::MATH_POW_INDEX, args, pos);
}
于是就造成了 **
有時不等于 Math.pow
的怪異問題。再看看如下代碼:
console.log(99**99);
a = 99, b = 99;
console.log(a**b);
console.log(Math.pow(99, 99));
分別輸出:
3.697296376497268e+197
3.697296376497263e+197
3.697296376497263e+197
其實
9999=369729637649726772657187905628805440595668764281741102430259972423552570455277523421410650010128232727940978889548326540119429996769494359451621570193644014418071060667659301384999779999159200499899
因此第一個結(jié)果更接近準(zhǔn)確的值烁落。
上周(2017年1月16日)這個怪異的行為已經(jīng)作為一個 bug 提交給了 V8 項目乘粒,bug 編號 #5848。