编写高性能的Javascript代码而无需优化


10

当使用对大型数值数组(例如线性代数程序包,对整数或浮点数进行操作)的Javascript编写对性能敏感的代码时,总是希望JIT尽可能提供帮助。大概意味着:

  1. 我们始终希望数组是压缩的SMI(小整数)或压缩的Double,这取决于我们是在进行整数还是浮点计算。
  2. 我们始终希望将相同类型的事物传递给函数,以使它们不会被标记为“大形”或未优化。例如,我们始终希望同时调用vec.add(x, y)这两个xy打包SMI数组,或者同时打包两个Double数组。
  3. 我们希望尽可能内联函数。

当一个人偏离这些情况时,会突然而突然地降低性能。出于各种无害的原因,可能会发生这种情况:

  1. 您可以通过看似无害的操作将压缩的SMI数组转换为压缩的Double数组,例如的等效项myArray.map(x => -x)。实际上,这是“最佳”的坏情况,因为压缩Double数组仍然非常快。
  2. 您可以将打包的数组变成通用的盒装数组,例如通过将数组映射到(意外地)返回null或的函数上undefined。这种情况很容易避免。
  3. 您可能会取消对整个函数的优化,例如vec.add()通过传递太多类型的事物并将其变为大形。如果您想进行“泛型编程”,vec.add()则可能会发生这种情况,这种情况在您对类型不十分小心的情况下使用(这样会看到很多类型),并且在想要最大程度地提高性能的情况下也可以使用(例如,它只能收到盒装双打)。

我的问题更多是一个软问题,关于如何根据上述考虑编写高性能的Javascript代码,同时又保持代码的美观和可读性。一些特定的子问题,以便您了解我的目标是:

  • 在打包SMI阵列的世界中,例如,在编程方面有一些准则吗?
  • 是否可以使用Javascript进行通用的高性能编程,而无需使用诸如宏系统之类的工具将其内联vec.add()到callsite中?
  • 鉴于宏调用站点和优化问题,如何将高性能代码模块化到库中?例如,如果我愉快地A高速使用Linear Algebra程序包,然后导入B依赖的程序包A,但B使用其他类型调用该程序包,然后对其进行优化,那么突然(无需更改代码)我的代码运行速度就会变慢。
  • 是否有任何易于使用的良好测量工具来检查Javascript引擎在内部对类型进行的操作?

1
这是一个非常有趣的话题,并且写得很好,表明您已经正确完成了自己的研究工作。但是,我担心这个问题对于SO格式而言过于宽泛,而且不可避免地会吸引比事实更多的意见。代码优化是一件非常复杂的事情,并且引擎的两个版本可能行为不同。我认为有时会有一名负责V8 JIT的人员徘徊,因此也许他们可以为自己的引擎提供正确的答案,但是即使对于他们来说,我也认为单个Q / A涉及的范围太广。
海道岛

“我的问题更像是一个软问题,关于如何编写高性能的Javascript代码...”另外,请注意,javascript提供了后台进程(网络工作者)的产生,并且还有一些库可用于GPU(tensorflow.js和gpu.js)的提供方式不仅仅依靠编译来提高基于javascript的应用程序的计算吞吐量...
Jon Trent

@JonTrent实际上,我在帖子中撒了点谎,我不太关心经典的线性代数应用,但是更关心整数的计算机代数。这意味着许多现有的数字包会立即被排除在外,因为(例如)在对矩阵进行行缩减时,它们可能会被2除,这在我正在使用的世界中是“不允许的”,因为(1/2)不是整数。我已经考虑过网络工作者(尤其是对于一些我想取消的长时间运行的计算),但是我要解决的问题是降低等待时间以充分响应交互。
Joppy

对于JavaScript中的整数算术,您可能正在看asm.js样式的代码,大致是“ |0在每个操作后面都加了”。它不是很漂亮,但是您可以在没有适当整数的语言中做到最好。您也可以使用BigInts,但到目前为止,它们在任何通用引擎中的运行速度都不是很快(主要是由于缺乏需求)。
jmrk

Answers:


8

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中使用double的情况很普遍,可以合理地假设所有引擎都对double和double数组有良好的支持。

是否可以使用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和两倍,那完全没问题。从两种数组中加载元素也是可以的。当负载看到了太多不同的类型以至于不再单独跟踪它们时,我们使用术语“巨形”来代替,而是使用一种更通用的机制来更好地缩放到大量类型,包含此类负载的函数可以仍然得到优化。“非优化”是一种非常具体的行为,必须丢弃某个函数的优化代码,因为看到的是以前从未见过的新类型,因此优化代码无法处理。但这还好:只需返回未优化的代码以收集更多的类型反馈,然后稍后再进行优化。如果这种情况发生了两次,则无需担心。它仅在病态严重的情况下才成为问题。

因此,所有内容的摘要是:不必担心。只需编写合理的代码,让引擎处理它即可。所谓“合理”,是指:对您的用例有意义的,可读的,可维护的,使用高效的算法,不包含诸如读取数组长度之外的错误。理想情况下,仅此而已,您无需执行其他任何操作。如果您觉得做某件事更好,和/或您实际上在观察性能问题,我可以提供两个想法:

使用TypeScript 可以提供帮助。大提示:TypeScript的类型针对开发人员的生产力,而不是执行性能(事实证明,这两种观点与类型系统的要求非常不同)。也就是说,存在一些重叠:例如,如果您始终将注释为number,则TS编译器会在您不小心将null本应仅包含/操作于数字的数组或函数中时警告您。当然,仍然需要纪律:单个number_func(random_object as number)逃生舱口可以无声地破坏所有内容,因为类型注释的正确性没有在任何地方强制执行。

使用TypedArrays也可以提供帮助。与常规的JavaScript数组相比,它们每个数组的开销(内存消耗和分配速度)要多一些(因此,如果您需要许多小数组,那么常规数组可能会更有效率),并且灵活性较差,因为它们无法增长或分配后缩小,但是它们确实提供了所有元素完全具有一种类型的保证。

是否有任何易于使用的良好测量工具来检查Javascript引擎在内部对类型进行的操作?

不,这是故意的。如上所述,我们不希望您专门针对V8今天可以特别优化的任何模式量身定制代码,我们也不相信您也确实希望这样做。这组事情可以朝任一方向改变:如果有一种您想使用的模式,我们可能会在将来的版本中对此进行优化(我们以前曾想过将未装箱的32位整数存储为数组元素的想法。 ,但有关工作尚未开始,因此没有承诺);有时,如果过去有一种我们用来进行优化的模式,那么如果它妨碍了其他更重要/更具影响力的优化,我们可能会决定放弃它。另外,众所周知,内联启发法很难解决,因此,在正确的时间做出正确的内联决策是一个正在进行的研究领域,并且是对引擎/编译器行为的相应更改;这又使每个人都不幸(您我们),如果您花费大量时间来调整代码,直到某些当前浏览器版本做出了您认为(或知道?)的最佳内联决策,那么半年后才回来,以了解当时的浏览器改变了他们的试探法。

当然,您始终可以衡量整个应用程序的性能-这才是最重要的,而不是内部引擎专门做出的选择。当心微基准测试,因为它们会误导您:如果您仅提取两行代码并对它们进行基准测试,那么情况很可能会完全不同(例如,不同的类型反馈),从而引擎将做出非常不同的决策。


2
感谢这个优秀的答案,它证明的事情是如何工作的多了我的怀疑,他们正在重要的是如何打算工作。顺便说一句,是否有关于您提到的“类型反馈”问题的博客文章等Array.sort()?我很想阅读更多有关它的信息。
Joppy

我认为我们没有针对特定方面写博客。从本质上讲,这就是您自己在问题中描述的内容:用JavaScript实现内建函数时,它们就像是一个库,在某种意义上说,如果不同的代码段使用不同的类型调用它们,那么性能可能会受到影响-有时会有所下降,有时更多。这不是该技术的唯一问题,甚至可以说不是最大问题。我主要只是想说我对一般问题很熟悉。
jmrk
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.