为什么在 V8 中 i ** 2 比 (i + 1) ** 2 慢

Nic*_*ick 1 javascript performance v8

考虑以下片段和运行结果:

片段 1:

let final_result, final_result2;
let start = new Date();
for(let i = 0; i < 100000000; i++) {
    final_result = Math.pow(i + 1, 2);
}
let end = new Date();
console.log(end - start); // Output 1

let start2 = new Date();
for(let i = 0; i < 100000000; i++) {
    final_result2 = (i + 1) ** 2;
}
let end2 = new Date();
console.log(end2 - start2); // Output 2
Run Code Online (Sandbox Code Playgroud)

片段 2:

let final_result, final_result2;
let start = new Date();
for(let i = 0; i < 100000000; i++) {
    final_result = Math.pow(i, 2);
}
let end = new Date();
console.log(end - start); // Output 1

let start2 = new Date();
for(let i = 0; i < 100000000; i++) {
    final_result2 = i ** 2;
}
let end2 = new Date();
console.log(end2 - start2); // Output 2
Run Code Online (Sandbox Code Playgroud)

片段 3:

let final_result, final_result2;

function t1(){
    for(let i = 0; i < 100000000; i++) {
        final_result = Math.pow(i, 2);
    }
}

function t2(){
    for(let i = 0; i < 100000000; i++) {
        final_result2 = i ** 2;
    }
}

let start = new Date();
t1();
let end = new Date();
console.log(end - start); // Output 1

let start2 = new Date();
t2();
let end2 = new Date();
console.log(end2 - start2); // Output 2
Run Code Online (Sandbox Code Playgroud)

结果:

输出 火狐 88(毫秒) 边缘 90(毫秒)
片段 1 - 输出 1 63 467
片段 1 - 输出 2 63 487
片段 2 - 输出 1 63 468
片段 2 - 输出 2 63 1180
片段 3 - 输出 1 64 480
片段 3 - 输出 2 64 1200

这些结果得到了持续超过无数次的试验和添加的数量并不会影响性能,即其它类似操作((i * 1) ** 2(i + i) ** 2,等)都产生了加速超过仅仅使用i ** 2。同时Math.pow它的速度是一致的。

使用 V8 浏览器(Edge 和 Chrome 都具有相似的结果)时,重复计算如何(i + n) ** 2i ** 2后者计算得更少,同时 Firefox 的运行时在 2 个片段之间是一致的。

jmr*_*mrk 6

(i + n) ** 2 的重复计算如何比 i ** 2 的计算速度更快?

那是因为这个微基准测试不是在测量求幂时间。谨防误导性微基准测试!

相反,它测量的是:

  • HeapNumber 分配和相关操作(写屏障,垃圾收集),
  • 在较慢的情况下,函数调用开销(与内联相反)和 JS 规范规定的一些检查(没有得到优化)。

Spidermonkey 和 V8 之间的基本架构差异之一是,前者使用“NaN 装箱”,而后者使用“指针标记”。两者各有利弊;在这种特殊情况下,结果是 V8 需要为您写入的每个结果分配一个新的“HeapNumber”final_result,而 Firefox 只能在那里写入原始 IEEE 双。(这几乎是指针标记方法的最坏情况比较。)这​​解释了两个引擎之间的速度差异。这很容易通过修改测试来验证,以便将结果存储到一个数组(即let final_result = [];final_result[0] = ...)中——在这种情况下,V8 的“数组元素类型”跟踪开始并且它也存储原始双精度数。

使用**而不是较慢的情况Math.pow似乎是 V8 中未开发的优化潜力。来源中有一个关键评论

// We currently don't optimize exponentiation based on feedback.
Run Code Online (Sandbox Code Playgroud)

引入此评论的提交提供了更多背景信息:**操作符曾经是“语法糖”Math.pow,而 V8 实际上是通过将其“脱糖”给后者来实现的;但是随着 BigInt 支持的引入,它不得不停止这样做。像往常一样,第一个实现旨在正确性而不是最大性能,并且这个第一个实现今天仍在使用(这可能意味着这个细节在现实世界的代码中并不是特别重要......否则之前有人会抱怨) . 这意味着 V8 的优化编译器目前缺乏内联求幂所需的类型反馈;相反,它发出对“内置”的调用,它必须为它想要返回的结果分配一个 HeapNumber。但是,作为一个相当聪明的优化编译器,它可以从其他地方传播类型信息;这就是为什么添加一些其他操作(例如(i+1) ** 2(i+0) ** 2) 在这种情况下具有有益的影响。

总结:不要被微基准测试所迷惑。从微基准测试中得出有用的结论确实需要检查引擎在引擎盖下的工作;否则很有可能你没有测量你认为你在测量的东西。此外,这很好地说明了微基准测试的另一个问题:可能在您的真实代码中,周围环境有所不同(例如,您可能正在存储到数组中,或者您可能正在执行生成类型反馈的其他操作,等),因此微基准测试的结果可能甚至不适用。