【问题标题】:Researching Javascript DEOPT reasons in Chrome 79 for a pet project为宠物项目研究 Chrome 79 中的 Javascript DEOPT 原因
【发布时间】:2019-12-21 06:03:02
【问题描述】:

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

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

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

DEOPT 急切,地图错误

trace 引用的代码:

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

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

空数组 [] 会启动 DEOPT 吗?

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

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

【问题讨论】:

    标签: javascript google-chrome v8 deoptimization


    【解决方案1】:

    缺少 64 位整数

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

    空数组 [] 会启动 DEOPT 吗?

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

    [懒惰、柔软、急切...]这是正确的评价吗?

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

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

    另一种情况是每个 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 */
    }
    

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

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

    【讨论】:

    • 感谢您对我冗长的多部分问题的全面答复。你能详细说明一下吗? >> 根据我的经验,手动拆分函数尤其可以为长时间运行的循环带来回报(您可以将循环放入自己的函数中)。
    • 顺便说一句,这是我运行大约 68 秒的 perft 例程的跟踪日志。大约 10 秒后,日志平静下来并停止 DEOPTing 并显示出任何活动。 s000.tinyupload.com/index.php?file_id=35726284828535596476
    • 添加了一个例子来说明。 -- 记录的行为听起来符合预期:一段时间后,引擎已看到所有代码,并且所有热门代码都已优化。
    • 谢谢你;我通过实验在某种程度上发生了这种情况,但我主要是像这样分裂(雾化):function big() { for (...) { innards() } /* lots more stuff... */ } function innards() { ... } - 所以我会尝试你推荐的方式(即在子函数中包含'for'),看看是否我发现任何性能差异。我可能只有一两个构成“大”功能的案例。我预计如果有任何差异,它将是在加载后不久,在事情真正开始之前的早期储蓄。
    • @AMoore:请不要添加与此问题/答案无关的 cmets。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-14
    • 2017-03-20
    • 1970-01-01
    • 1970-01-01
    • 2012-03-28
    • 2021-10-09
    • 2014-04-27
    相关资源
    最近更新 更多