为什么 V8 和 Spidermonkey 似乎都没有展开静态循环?

Doo*_*fus 5 javascript v8 spidermonkey loop-unrolling

做一个小检查,它看起来既不是 V8 也不是 Spidermonkey 展开循环,即使它是完全明显的,它们是多长时间(字面作为条件,在本地声明):

const f = () => {
  let counter = 0;
  for (let i = 0; i < 100_000_000; i++) {
    counter++;
  }
  return counter;
};

const g = () => {
  let counter = 0;
  for (let i = 0; i < 10_000_000; i += 10) {
    counter++;
    counter++;
    counter++;
    counter++;
    counter++;
    counter++;
    counter++;
    counter++;
    counter++;
    counter++;
  }
  return counter;
}

let start = performance.now();
f();
let mid = performance.now();
g();
let end = performance.now();

console.log(
  `f took ${(mid - start).toFixed(2)}ms, g took ${(end - mid).toFixed(2)}ms, ` +
  `g was ${((mid - start)/(end - mid)).toFixed(2)} times faster.`
);
Run Code Online (Sandbox Code Playgroud)

这有什么原因吗?它们执行相当复杂的优化。标准for循环在 javascript 中就那么不常见,不值得吗?


编辑:就像注释一样:有人可能会争辩说,也许优化被延迟了。情况似乎并非如此,尽管我不是这里的专家。我使用node --allow-natives-syntax --trace-deopt,手动执行优化,并观察到没有发生反优化(折叠片段,实际上无法在浏览器中运行):

const { performance } = require('perf_hooks');

const f = () => {
  let counter = 0;
  for (let i = 0; i < 100_000_000; i++) {
    counter++;
  }
  return counter;
};
// collect metadata and optimize
f(); f();
%OptimizeFunctionOnNextCall(f);
f();

const start = performance.now();
f();
console.log(performance.now() - start);
Run Code Online (Sandbox Code Playgroud)

用普通版和展开版完成,效果相同。

jmr*_*mrk 6

(此处为 V8 开发人员。)

TL;DR:因为对于现实世界的代码来说,它很少值得。

循环展开与其他增加代码大小的优化(例如内联)一样,是一把双刃剑。是的,它可以提供帮助;特别是它通常有助于像这里张贴的那样的小玩具示例。但它也可能损害性能,最明显的是因为它增加了编译器必须做的工作量(因此增加了完成这项工作所需的时间),而且还通过次要影响,例如较大的代码从 CPU 的缓存工作中受益较少.

V8 的优化编译器实际上确实喜欢展开循环的第一次迭代。此外,碰巧的是,我们目前有一个正在进行的项目来展开更多的循环;目前的状态是它有时有帮助,有时会受到伤害,因此我们仍在微调启发式方法,以了解它何时应该启动,何时不应该启动。这个困难也表明,对于现实世界的 JavaScript,好处通常很小。

它是否是“标准for循环”并不重要;理论上任何循环都可以展开。碰巧的是,除了微基准测试之外,循环展开往往几乎没有什么区别:仅仅进行另一次迭代并没有那么多开销,因此如果循环体执行的次数超过counter++,则不会有太多收获避免每次迭代的开销。而且,fwiw,这个每次迭代的开销不是你的测试所测量的:重复的增量都被折叠了,所以你在这里真正比较的是 100M 迭代和counter += 110M 迭代counter += 10

因此,这是试图欺骗我们得出错误结论的误导性微基准测试的众多示例之一;-)