Jop*_*ppy 12 javascript jit v8 spidermonkey
在用 Javascript 编写对大型数字数组进行操作的性能敏感代码时(想想线性代数包,对整数或浮点数进行操作),人们总是希望 JIT 尽可能多地提供帮助。大致意思是:
vec.add(x, y)既x和y被包装SMI阵列,或两者打包双阵列。当一个人偏离这些情况时,就会发生突然而剧烈的性能下降。这可能由于各种无害的原因而发生:
myArray.map(x => -x). 这实际上是“最好的”坏情况,因为压缩的 Double 数组仍然非常快。nullor的函数上undefined。这种糟糕的情况很容易避免。vec.add()通过传入太多类型的东西并将其变成超态。如果您想进行“通用编程”,vec.add()则可能会发生这种情况,在您不注意类型的情况下(因此它会看到很多类型进入)以及在您想要获得最大性能的情况下使用(例如,它应该只接收盒装双打)。我的问题更像是一个软问题,关于如何根据上述考虑编写高性能 Javascript 代码,同时仍然保持代码的美观和可读性。一些具体的子问题,以便您知道我想要什么样的答案:
vec.add()调用站点之类的东西内联?A高速使用线性代数包,然后我导入一个B依赖于的包A,但B使用其他类型调用它并对其进行反优化,突然(没有更改我的代码)我的代码运行速度变慢。jmr*_*mrk 14
V8 开发人员在这里。考虑到对这个问题的兴趣,并且缺乏其他答案,我可以试一试;恐怕这不会是您希望的答案。
是否有一套关于如何在封装 SMI 阵列的世界中进行编程的指南(例如)?
简短的回答:就在这里:const guidelines = ["keep your integers small enough"]。
更长的答案:由于各种原因,很难给出一套全面的指导方针。总的来说,我们的观点是 JavaScript 开发人员应该编写对他们和他们的用例有意义的代码,而 JavaScript 引擎开发人员应该弄清楚如何在他们的引擎上快速运行这些代码。另一方面,这种理想显然存在一些局限性,因为无论引擎实现选择和优化工作如何,某些编码模式的性能成本始终高于其他编码模式。
当我们谈论性能建议时,我们会尽量牢记这一点,并仔细估计哪些建议很可能在许多引擎和多年中保持有效,并且还合理地惯用/非侵入性。
回到手头的例子:在内部使用 Smis 应该是用户代码不需要知道的实现细节。它会使某些情况更有效,而在其他情况下不应该受到伤害。并非所有引擎都使用 Smis(例如,AFAIK Firefox/Spidermonkey 历史上没有;我听说在某些情况下,他们现在确实使用 Smis;但我不知道任何细节,也无法与任何权威人士交谈事)。在 V8 中,Smis 的大小是一个内部细节,实际上随着时间和版本的变化而变化。在曾经是大多数用例的 32 位平台上,Smis 一直是 31 位有符号整数;在 64 位平台上,它们曾经是 32 位有符号整数,最近这似乎是最常见的情况,直到在 Chrome 80 中我们推出了“指针压缩” 对于 64 位架构,这需要将 Smi 大小降低到 32 位平台已知的 31 位。如果您碰巧基于 Smis 通常是 32 位的假设来实现,您会遇到不幸的情况,例如这个。
值得庆幸的是,正如您所指出的,双数组仍然非常快。对于数字密集型代码,假设/定位双数组可能是有意义的。鉴于双精度在 JavaScript 中的流行,可以合理地假设所有引擎都对双精度和双精度数组提供良好的支持。
是否可以在 Javascript 中进行通用的高性能编程而不使用宏系统之类的东西将 vec.add() 之类的东西内联到调用站点中?
“通用”通常与“高性能”不一致。这与 JavaScript 或特定引擎实现无关。
“通用”代码意味着必须在运行时做出决定。每次执行函数时,都必须运行代码以确定“是x整数吗?如果是,则取那个代码路径。是x字符串?然后跳过这里。它是一个对象吗?它有.valueOf吗?不是吗?然后也许.toString()?也许在它的原型链上?调用它,然后从结果开始重新开始”。“高性能”优化代码基本上建立在放弃所有这些动态检查的想法之上;只有当引擎/编译器有某种方法可以提前推断类型时才有可能:如果它可以证明(或以足够高的概率假设)它x总是一个整数,那么它只需要为这种情况生成代码(如果涉及未经证实的假设,则由类型检查保护)。
内联与这一切是正交的。“通用”函数仍然可以内联。在某些情况下,编译器可能能够将类型信息传播到内联函数中以减少那里的多态性。
(作为比较:C++ 作为一种静态编译语言,具有解决相关问题的模板。简而言之,它们让程序员明确指示编译器创建函数(或整个类)的专门副本,并在给定类型上进行参数化。这是一个对于某些情况,这是一个不错的解决方案,但并非没有其自身的一系列缺点,例如编译时间长和二进制文件大。当然,JavaScript 没有模板之类的东西。您可以使用它eval来构建一个有点相似的系统,但随后您会遇到类似的缺点:您必须在运行时执行与 C++ 编译器等效的工作,并且您必须担心生成的大量代码。)
鉴于诸如超态调用站点和去优化之类的事情,人们如何将高性能代码模块化为库?例如,如果我愉快地高速使用线性代数包 A,然后我导入一个依赖于 A 的包 B,但 B 使用其他类型调用它并对其进行反优化,突然(没有我的代码更改)我的代码运行速度变慢.
是的,这是 JavaScript 的一个普遍问题。V8 过去常常Array.sort在 JavaScript 内部实现某些内置函数(比如),这个问题(我们称之为“类型反馈污染”)是我们完全放弃该技术的主要原因之一。
也就是说,对于数字代码,没有那么多类型(只有 Smis 和 doubles),并且正如您所指出的那样,它们在实践中应该具有相似的性能,因此虽然类型反馈污染确实是一个理论上的问题,并且在某些情况下可以有重大影响,也很可能在线性代数场景中您不会看到可测量的差异。
此外,在引擎内部,除了“一种类型 == 快”和“多种类型 == 慢”之外,还有更多的情况。如果给定的操作同时看到 Smis 和 doubles,那完全没问题。从两种数组加载元素也很好。我们使用术语“megamorphic”来表示负载已经看到如此多不同类型的情况,以至于它放弃单独跟踪它们,而是使用更通用的机制,可以更好地扩展到大量类型——包含此类负载的函数可以仍然得到优化。“去优化”是一种非常具体的行为,必须丢弃函数的优化代码,因为看到了以前没有见过的新类型,因此优化代码无法处理。但即使这样也很好:只需返回未优化的代码以收集更多类型反馈,并稍后再次优化。如果这种情况发生几次,那就没什么好担心的;只有在病态严重的情况下才会成为问题。
所以所有的总结是:别担心。只写合理的代码,让引擎来处理。我所说的“合理”是指:对您的用例有意义的东西,可读、可维护、使用高效算法、不包含诸如读取超出数组长度之类的错误。理想情况下,这就是全部内容,您无需执行任何其他操作。如果做某事让你感觉更好,和/或如果你真的在观察性能问题,我可以提供两个想法:
使用 TypeScript会有所帮助。大警告:TypeScript 的类型旨在提高开发人员的生产力,而不是执行性能(事实证明,这两种观点对类型系统的要求非常不同)。也就是说,存在一些重叠:例如,如果您始终将事物注释为number,那么如果您不小心放入null一个应该只包含/操作数字的数组或函数,TS 编译器会警告您。当然,仍然需要纪律:一个number_func(random_object as number)逃生舱门可以默默地破坏一切,因为类型注释的正确性并没有在任何地方强制执行。
使用 TypedArrays 也有帮助。与常规 JavaScript 数组相比,它们每个数组的开销(内存消耗和分配速度)稍多一些(因此,如果您需要许多小数组,那么常规数组可能更高效),而且灵活性较差,因为它们无法增长或在分配后收缩,但它们确实提供了所有元素都只有一种类型的保证。
是否有任何易于使用的测量工具来检查 Javascript 引擎在内部使用类型做什么?
不,这是故意的。如上所述,我们不希望您根据 V8 今天可以特别优化的任何模式专门定制您的代码,我们也不相信您真的想这样做。这组事情可以在任何一个方向上改变:如果有你喜欢使用的模式,我们可能会在未来的版本中针对它进行优化(我们之前曾考虑过将未装箱的 32 位整数存储为数组元素的想法.. . 但工作还没有开始,所以没有承诺);有时,如果我们过去曾经优化过某个模式,如果它妨碍了其他更重要/有影响力的优化,我们可能会决定放弃它。此外,众所周知,内联启发式之类的事情很难做到正确,因此,在正确的时间做出正确的内联决策是一个正在进行的研究领域,并对引擎/编译器的行为做出相应的改变;这使得这是另一种情况,对每个人来说都是不幸的(你和我们)如果你花了很多时间调整你的代码,直到一些当前的浏览器版本做了你认为(或知道?)最好的内联决定,但半年后回来才意识到当时的浏览器改变了他们的启发式。
当然,您可以始终从整体上衡量应用程序的性能——这才是最重要的,而不是引擎内部做出的具体选择。当心微基准测试,因为它们具有误导性:如果您只提取两行代码并对其进行基准测试,那么场景可能会大不相同(例如,不同类型的反馈),引擎将做出非常不同的决定。