译文:Improving DataView performance in V8
原文链接:https://v8.dev/blog/dataview
Improving DataView performance in V8
DataViews是在JavaScript中进行底层内存访问的两种可能方法之一,另一种是TypedArray。到目前为止,与V8中的TypedArray相比,DataView的优化程度要少得多,导致在图形密集型工作负载或解码/编码二进制数据等任务上的性能降低。造成这种情况的主要原因是历史上的选择,比如asm.js选择TypedArray而不是DataViews,因此引擎被激励关注TypedArray的性能。
由于性能损失,JavaScript开发人员(如GoogleMaps团队)决定避免使用DataViews,转而使用TypedArray,而代价是增加了代码复杂性。本文解释了我们如何在V8 v6.9中优化DataView性能来匹配(甚至超过)等价的TypedArray代码,从而有效地使DataView可用于性能关键的现实世界应用程序。
Background
自ES 2015引入以来,JavaScript一直支持在名为ArrayBuffers的原始二进制缓冲区中读取和写入数据。但不能直接访问ArrayBuffers,相反,程序必须使用所谓的array buffer view对象,该对象可以是DataView,也可以是TypedArray。
TypedArray允许程序以统一类型值(如Int16Array或Float32Array)的数组形式访问缓冲区。
1 | const buffer = new ArrayBuffer(32); |
另一方面,DataViews允许更细粒度的数据访问。它们允许程序员通过为每个Number类型提供专门的getter和setter来选择从缓冲区读取和写入的值的类型,这使得它们在序列化数据结构时非常有用。
1 | const buffer = new ArrayBuffer(32); |
此外,DataViews还允许选择数据存储的功能,这在从外部源(如网络、文件或GPU)接收数据时非常有用。
1 | const buffer = new ArrayBuffer(32); |
一个高效的DataView实现已经是很长时间以来的一个特性请求(请参阅5年前的这个bug报告),我们很高兴地宣布DataView的性能是相当好的!
Legacy runtime implementation
直到最近,DataView方法以前在V8中被实现为内置的C+运行时函数。这非常昂贵,因为每次调用都需要从JavaScript到C++(和Back)的转换。
为了研究此实现所产生的实际性能成本,我们建立了一个性能基准,将原生DataView getter实现与模拟DataView行为的JavaScript包装进行比较。此包装器使用Uint8Array从基础缓冲区逐字节读取数据,然后从这些字节计算返回值。例如,下面是读取小Endian 32位无符号整数值的函数。
1 | function LittleEndian(buffer) { // Simulate little-endian DataView reads. |
TypedArray已经在V8中得到了很大的优化,所以它们代表了我们想要达到的性能目标。
我们的基准测试显示,本机DataView getter性能比基于Uint8Array的包装器慢4倍,无论是大端读取还是小端点读取。
Improving baseline performance
我们改进DataView对象性能的第一步是将实现从C++运行时移到CodeStubAssembler(也称为CSA)。CSA是一种可移植的汇编语言,它允许我们直接用TurboFan的机器级中间表示(IR)编写代码,并使用它来实现V8的JavaScript标准库的优化部分。在CSA中重写代码完全绕过了对C+的调用,并通过利用TurboFan的后端生成高效的机器代码。
然而,手工编写CSA代码很麻烦。CSA中的控制流与程序集中的控制流非常相似,使用显式标签和Gotos,这使得代码很难一眼就读懂。
为了使开发人员更容易对V8中的优化JavaScript标准库做出贡献,并提高可读性和可维护性,我们开始设计一种名为V8 TORQUE的新语言,该语言可以编译成CSA。扭矩的目标是抽象出低层次的细节,使csa代码更难编写和维护,同时保留相同的性能配置文件。
重写DataView代码是一个很好的机会,可以开始为新代码使用TORQUE,并有助于为TORQUE开发人员提供大量关于该语言的反馈。这就是DataView的getUint 32()方法的样子,用TORQUE编写:
1 | macro LoadDataViewUint32(buffer: JSArrayBuffer, offset: intptr, |
将DataView方法移动到TORQUE已经显示性能有3×改进,但还没有完全匹配基于Uint8Array的包装器性能。
Optimizing for TurboFan
当JavaScript代码变热时,我们使用TurboFan优化编译器对其进行编译,以便生成比解释字节码运行效率更高的优化机器代码。
TurboFan的工作方式是将传入的JavaScript代码转换为内部图形表示(更准确地说,是一个“节点之海”)。它从与JavaScript操作和语义相匹配的高级节点开始,并逐步将它们细化为低级和低级节点,直到最终生成机器代码。
特别是,函数调用(例如调用DataView方法之一)在内部表示为JSCall节点,该节点最终归结为生成的机器代码中的实际函数调用。
然而,TurboFan允许我们检查jscall节点是否实际上是对已知函数的调用。例如,内置函数之一,并在IR中内联此节点。这意味着复杂的JSCall在编译时被表示函数的子图所取代。这允许TurboFan在以后的过程中优化函数的内部,作为更广泛的上下文的一部分,而不是它自己,最重要的是摆脱昂贵的函数调用。
实现TurboFan内联最终使我们能够与我们的Uint8Array包装器的性能相匹配,甚至超过它的性能,其速度是以前C+实现速度的8倍。
Further TurboFan optimizations
看看TurboFan在内联DataView方法之后生成的机器代码,仍然有一些改进的余地。这些方法的第一个实现非常接近于标准,并在规范指示时抛出错误(例如,当试图读取或写入基础ArrayBuffer的边界时)。
然而,我们用TurboFan编写的代码意味着要尽可能快地对常见的热情况进行优化-它不需要支持所有可能的边缘情况。通过删除对这些错误的所有复杂处理,并在需要抛出时将优化降回基线扭矩实现,我们能够将生成代码的大小减少35%左右,从而产生相当明显的加速,以及相当简单的涡扇代码。
跟进这一想法,尽可能专门在TurboFan,我们也取消了对指数或偏移太大(SMI范围之外)的TurboFan优化代码的支持。这使我们能够避免处理浮点数64算法,这是不适合32位值的偏移量所需的,并且避免在堆中存储大整数。
与最初的TurboFan实现相比,这是DataView基准分数的两倍多。DataViews现在的速度是Uint8Array包装器的3倍,大约是原始DataView实现的16倍!
Impact
我们已经在我们自己的基准之上,在一些实际的例子上评估了新的实现对性能的影响。
在从JavaScript解码以二进制格式编码的数据时,通常使用DataViews。例如二进制格式FBX,一种用于交换3D动画的格式。我们已经测试了流行的Three.js JavaScript 3D库的FBX加载程序,并测量了它的执行时间减少了10%(约80 ms)。
我们比较了DataViews与TypedArray的总体性能。我们发现,我们的新DataView实现在访问本机endianness(Intel处理器上的小Endian)中对齐的数据时,提供了与TypedArray几乎相同的性能,弥补了很大的性能差距,使DataViews成为V8中的一个实际选择。
我们希望您现在能够在有意义的地方开始使用DataViews,而不是依赖TypedArray shims。请将您使用DataView的问题反馈给我们!