资讯专栏INFORMATION COLUMN

V8 使用“常量折叠”优化技巧,导致幂(**)运算有时候不等于 Math.pow()

jerryloveemily / 2353人阅读

摘要:究其原因,引擎使用了常量折叠。常量折叠是一种编译器的编译优化技术。也就是说是在编译时进行计算常量折叠,而总是在运行时进行计算。当我们使用变量进行幂运算时例此时不存在常量折叠,因此的值在运行时进行计算,会被编译成调用。

在如今的主流 Web 编程语言中,如 PHP 或 Python 等,都包含幂运算符(一般来说符号是 ^ 或者 **)。而最新的 ES7 中也增加了对幂运算的支持,使用符号 **,最新的 Chrome 已经提供了对幂运算的支持。

但是在 javascript 中,** 运算有时候并不等于 Math.pow(a,b),在最新的 Chrome 55 中:

Math.pow(99,99) 的结果是 3.697296376497263e+197,

但是 99**99 的结果是 3.697296376497268e+197。

两者并不相等

3.697296376497263e+197
3.697296376497268e+197

而且 Math.pow(99,99) - 99**99 的结果也不是 0 而是 -5.311379928167671e+182。

因此我们猜测,** 操作符只是幂运算的另一个实现。但是当我们写一个函数时,幂运算又表现出诡异的特性:

function diff(x) {
  return Math.pow(x,x) - x**x;
}

调用 diff(99) 返回 0。WTF?两者又相等了!

猜猜下面代码输出什么?

var x = 99;
x**x - 99**99;

这段代码的运行结果是 -5.311379928167671e+182。

这简直是薛定谔的幂

究其原因,V8 引擎使用了常量折叠(const folding)。常量折叠是一种编译器的编译优化技术。

考虑如下代码:

for (let i = 0; i < 100*100*100; i++){
  // 循环体
}

该循环的条件 i<100*100*100 是一个表达式(expression),如果放到判断时再求值那么 100*100*100 的计算将会进行 1000000 次。如果编译器在语法分析阶段进行常量合并,该循环将会变为这样:

for (let i = 0; i < 1000000; i++){
  // 循环体
}

而上文中提到的 99**99 的计算也使用到了常量折叠。也就是说 99**99 是在编译时进行计算(常量折叠),而 Math.pow 总是在运行时进行计算。当我们使用变量进行幂运算时(例 a**b)此时不存在常量折叠,因此 a ** b 的值在运行时进行计算,** 会被编译成 Math.pow 调用。

在源码 src/parsing/parser.cc 文件中,编译时计算代码:

case Token::EXP: {
double value = Pow(x_val, y_val);
int int_value = static_cast(value);
*x = factory()->NewNumberLiteral(
    int_value == value && value != -0.0 ? int_value : value, pos,
    has_dot);
return true;

可以看到使用了 Pow 函数计算了幂运算的求值结果。Pow 是一个 inline 的函数,内部做了一些常规优化,对不能优化的情况则使用了 std::pow(x, y) 来计算最终结果。

而 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);
}

可见两者使用了不同的算法。但是当不做常量折叠的时候,** 则转换成了 Math.pow 函数调用:

Expression* Parser::RewriteExponentiation(
    Expression* left, 
    Expression* right,
    int pos) {
  ZoneList* args = new (zone()) ZoneList(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

因此第一个结果更接近准确的值。

上周(2017年1月16日)这个怪异的行为已经作为一个 bug 提交给了 V8 项目,bug 编号 #5848。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81363.html

相关文章

  • V8引擎深入研究目录贴

    摘要:对于每个前端程序员来讲都有一个终极理想,那就是搞懂引擎是如何工作的。性能经过了两次飞跃第次飞跃是年发布,第次则是年的。从去年底开始连载源码分析,记录一下自己学习源码的点点滴滴。月星期六晚点和大家一起聊聊引擎前端程序员应该懂点知识讲堂。 对于每个前端程序员来讲都有一个终极理想,那就是搞懂 javascript 引擎是如何工作的。 从我的网络 ID(justjavac)可以看出来,当我开始...

    blastz 评论0 收藏0
  • ES6 知识整理一(es6快速入门)

    摘要:函数调用会在内存形成一个调用记录,又称调用帧,保存调用位置和内部变量等信息。等到运行结束,将结果返回到,的调用帧才会消失。方法用于将一组值,转换为数组。,和遍历数组方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的方法类似。 ES6 简介 ES6, 全称 ECMAScript 6.0 ,是 JavaScript 的下一个版本标准,2015.06 发版。 let 和 const...

    elva 评论0 收藏0
  • 深入理解ES6之《ES7》

    摘要:指数运算符是可以进行求幂运算的求幂运算符是两个星号求幂运算符的优先级高于其它二元运算符,但是一元运算符的优先级高于求幂运算符求幂运算符左侧的一元运算符只能是或者是语法错误可以这样包裹也可以这样包裹在中可通过方法来检查给定字符串中是 指数运算符 Math.pow是可以进行求幂运算的求幂运算符是两个星号 let result = 5 ** 2 console.log(result) //2...

    lx1036 评论0 收藏0
  • 深入理解ES6之《ES7》

    摘要:指数运算符是可以进行求幂运算的求幂运算符是两个星号求幂运算符的优先级高于其它二元运算符,但是一元运算符的优先级高于求幂运算符求幂运算符左侧的一元运算符只能是或者是语法错误可以这样包裹也可以这样包裹在中可通过方法来检查给定字符串中是 指数运算符 Math.pow是可以进行求幂运算的求幂运算符是两个星号 let result = 5 ** 2 console.log(result) //2...

    xeblog 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<