【问题标题】:Why do neither V8 nor spidermonkey seem to unroll static loops?为什么 V8 和 spidermonkey 似乎都没有展开静态循环?
【发布时间】:2021-05-10 21:28:43
【问题描述】:

做个小检查,看起来既不是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.`
);

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


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

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

普通版和展开版都做,效果一样。

【问题讨论】:

  • 呃,你真的希望展开 100000000 次重复的循环吗?没有理智的编译器会生成那么多代码。
  • 否,但在某些情况下(例如上面显示的微基准测试),正常的“展开”,例如10 个循环步骤组合在一起,是有益的(请注意,正如公认的答案所提到的,折叠在给出的示例中起作用,但话又说回来,它也是 chrome 上速度差异的 50 倍)。展开不必是全部或全部,在其他语言中也不是这样。你只是想尽可能地避免循环的条件(即使它主要被分支预测器捕获,如果循环体做的很少,它仍然是一个问题)。

标签: javascript v8 spidermonkey loop-unrolling


【解决方案1】:

(这里是 V8 开发人员。)

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

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

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

不管是不是“标准for-loop”;理论上任何循环都可以展开。碰巧的情况是,除了微基准之外,循环展开往往没有什么不同:只是进行另一次迭代并没有那么多开销,所以如果循环体的工作量超过counter++,则不会有很多从避免每次迭代开销中获得。而且,首先,每次迭代的开销不是你的测试测量的:重复的增量都被折叠了,所以你在这里真正比较的是 counter += 1 的 100M 迭代与 10M 的迭代counter += 10.

所以这是许多误导性微基准测试试图诱使我们得出错误结论的例子之一;-)

【讨论】:

  • 非常翔实的答案,谢谢。我对此很好奇,了解幕后发生的事情很有趣。
【解决方案2】:

我建议您阅读this answer,因为它解释得很清楚。简而言之,展开并不意味着代码会运行得更快。

例如,如果不是简单的counter++,而是有一个函数调用(取自链接的答案):

function one() {
  // something complex running here, in this case a very long comment:
  // bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla
  
  return 1;
}

for (let i = 0; i < 1000; ++i) {
  counter += one();
}

如果函数很短,on-stack replacement 以及内联函数将使展开代码更快,然而如果函数很长,则循环实际上更快(所有示例再次,取从我链接的答案中)。

  counter += one();
  counter += one();
  ...

现在,从我在大学时使用汇编语言(已经由处理器优化)创建一个简单的编译器,一直到 C/C++(它本身已经可以产生令人难以置信的效率)的个人旅程ASM 代码),然后逐步升级到更高级别的语言,例如 PHP 和 Javascript:

我的看法是,负责优化的人必须做很多启发式的工作,并且很可能会对能够产生真实结果的真实代码感兴趣。

现在,我无法确定在 for 循环中进行算术运算是否比调用函数更常见,但我的直觉告诉我,具有简单算术运算的 for 循环不太可能在浏览器如今已成为巨大的生态系统。再说一次,这是一个很好的学习和深入研究的练习。

【讨论】:

  • 谢谢,但有一些问题:首先,我无法再复制阻止函数内联的 cmets。可能发生了一些变化(答案是三年前)。其次,我因此无法检查展开的版本是否真的不如循环,如果函数内联都没有发生。最后,我可以看到,有一个“停止展开的明智点”。如果上述循环完全展开,它将有 1 亿行,这会使解析器在查看它时大吃一惊。不过,这并不是一个合理的方法。
  • 我可以确认 V8 已经不再使用源大小(包括 cmets)作为其内联启发式的一部分。但一般观点仍然存在:内联和展开都可以帮助或损害性能,这真的取决于。这就是为什么很难让引擎的决策(即启发式)正确。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-14
  • 1970-01-01
相关资源
最近更新 更多