1

我一直在修补 Javascript 国际象棋引擎。是的,是的,我知道(轻笑),这不是那种事情的最佳平台。这有点像一个宠物项目,我很享受学术练习,并且对接近编译语言速度的挑战很感兴趣。Javascript 中还有其他一些古怪的挑战,比如缺少 64 位整数,这使得它不适合国际象棋,但也很有趣。

不久前,我意识到小心构造、函数参数等非常重要。在国际象棋编程中一切都很重要,但在 Chrome 中通过 Javascript 使用 JIT 编译器(V8 Turbofan)时似乎很重要。

通过一些痕迹,我看到了一些急切的 DEOPT,我无法弄清楚如何避免。

DEOPT 急切,地图错误

跟踪引用的代码:

if (validMoves.length) { ...do some stuff... }

跟踪直接指向 IF 条件的 validMoves.length 参数。validMoves 只是一个空数组 [] 或移动对象数组 [{Move},{Move},...]

空数组 [] 会启动 DEOPT 吗?

顺便说一句,我有很多懒惰和软 DEOPT,但如果我理解正确的话,这些并不是那么重要,只是 V8 在最终优化它之前如何围绕我的代码包装它的一部分;在 --trace-opt 中,具有软、惰性 DEOPT 的功能似乎最终会被 Turbofan 优化,并且从长远来看可能不会对性能造成太大影响。(就此而言,急切的 DEOPT 功能似乎最终也得到了重新优化。)这是一个正确的评估吗?

最后,我有时发现通过将显示 DEOPT 的函数分解为多个较小的函数调用,我获得了显着的性能提升。据此,我推断更大更复杂的功能难以优化,通过分解它们,较小的分隔功能正在被优化,从而为我提供了收益。这听起来合理吗?

4

1 回答 1

1

缺少 64 位整数

好吧,现在有 BigInts :-) (但在大多数引擎/情况下,它们还不适合高性能操作。)

空数组 [] 会启动 DEOPT 吗?

一般没有。然而,数组有不同的内部表示,所以这可能是也可能不是那里发生的事情。

【懒惰、软弱、急切……】这是正确的评价吗?

一般是的。通常您不必担心 deopts,特别是对于早期经历一些 deopts 的长期运行程序。对于所有--trace-deopt报告的形容词都是如此——这些都只是内部细节。(“eager”和“lazy”是彼此直接对立的,只是表明必须取消优化的函数的激活是否是栈顶的。“soft”是deopt的一个特殊原因,即缺乏类型反馈,并且 V8 选择去优化而不是生成“优化”代码,尽管缺乏类型反馈,这根本不会被优化。)

作为一名 JavaScript 开发人员,在极少数情况下,您可能想要关心 deopts。一个示例是,当您遇到相同的 deopt 一遍又一遍地发生的情况时。当它发生时,这是 V8 中的一个错误。这些“deopt 循环”很少见,但偶尔会发生。如果您发现了这种情况,请提交带有重现说明的错误。

另一种情况是每个 CPU 周期都很重要,尤其是在启动期间/在短时间运行的应用程序中,并且一些代价高昂的功能由于可能可以避免的原因而被取消优化。不过,这似乎不是你的情况。

[分解功能...] 这听起来合理吗?

分解功能可能是有益的,是的;特别是如果您开始使用的功能很大。一般来说,各种规模的功能都会得到优化;显然更大的功能需要更长的时间来优化。这是一个棘手的领域,没有简单的答案;如果函数太小,那么这对性能也没有帮助。V8 将执行一些内联​​,但决策基于自然并不总是完美的启发式方法。以我的经验,手动拆分函数尤其可以为长时间运行的循环(您将循环放入自己的函数中)带来回报。

编辑:根据要求详细说明最后一点,这里有一个例子:而不是

function big() {
  for (...) {
    // long-running loop
  }
  /* lots more stuff... */
}

您将其拆分为:

function loop() {
  for (...) {
    // same loop as before
  }
}
function outer() {
  loop();
  /* same other stuff as before */
}

对于短循环,这是完全没有必要的,但如果在循环中花费大量时间并且函数的整体大小很大,那么这种拆分允许优化以更细粒度的块和更少(“软”)进行选择。

并且要非常清楚:我只建议您在遇到特定问题时这样做(例如:--trace-opt告诉您最大的功能已优化两次或更多次,每次需要一秒钟)。请不要离开阅读此答案,以为“每个人都应该始终拆分他们的功能”,这根本不是我要说的。在大型函数的极端情况下,拆分它们可能是有益的。

于 2019-12-25T15:39:38.907 回答