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

做一个小检查,看起来V8和蜘蛛猴都没有展开循环,即使很明显,它们有多长(字面上是条件,在本地声明):

数据-lang="js"数据-隐藏="假"数据-控制台="真"数据-巴贝尔="假">
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.`
);

这有什么原因吗?它们执行的优化要复杂得多。标准的for循环在Java脚本中是不常见的吗?它不值得吗?


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

数据-lang="js"数据-隐藏="真"数据-控制台="假"数据-巴贝尔="假">
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);

使用普通版本和展开版本时,效果相同。


解决方案

(此处为V8开发人员)

TL;DR:因为对于现实世界的代码来说,这几乎不值得。

循环展开与其他增加代码大小(如内联)的优化一样,是一把双刃剑。是的,它是有帮助的;尤其是对于像这里张贴的这样的小玩具例子来说,它经常是有帮助的。但它也会损害性能,最明显的原因是它增加了编译器必须完成的工作量(因此增加了完成该工作所需的时间),但也会产生次要影响,如代码越大,从CPU缓存工作中获益越少。

V8的优化编译器实际上喜欢展开循环的第一次迭代。此外,碰巧的是,我们目前有一个正在进行的项目来展开更多的循环;目前的状态是它有时有用,有时有害,所以我们仍然在微调启发式,以确定它何时应该起作用,什么时候不应该起作用。这一困难还表明,对于现实世界的Java脚本,好处通常很小。

不管它是不是标准的for-loop";;理论上任何循环都可以展开。碰巧的情况是,除了微基准测试之外,循环展开往往不会有什么不同:仅仅进行另一次迭代不会产生太多开销,所以如果循环体做的比counter++更多,那么避免每次迭代的开销不会有太大好处。简而言之,每个迭代的开销不是您的测试测量的:重复的增量全部折叠,所以您在这里真正比较的是counter += 1的100M次迭代和counter += 10的10M次迭代。

这是许多误导性微基准的例子之一,这些例子试图欺骗我们得出错误的结论;-)

相关文章