JavaScript 数组性能在 13k-16k 元素时下降

Tei*_*iem 17 javascript arrays v8

我正在对数组的创建和编辑性能进行一些性能测试,并注意到大约有 13k-16k 元素的数组周围有一些奇怪的特征。

下图显示了创建数组并从中读取每个元素所需的时间(在本例中是对数组中的数字求和)。capacitypush与数组的创建方式相关:

  • 容量const arr = new Array(length)然后arr[i] = data
  • const arr = [];然后arr.push(data)

根据长度创建数组的时间 基于长度的数组操作时间

正如您所看到的,在这两种情况下,创建数组并读取数组时,与元素少 1k 时每个元素的性能相比,性能降低了约 2-3 倍。
当使用push方法创建数组时,与事先使用正确的容量创建数组相比,这种跳转发生得更早一些。我认为发生这种情况是因为,当推送到已经达到最大容量的阵列时,添加的额外容量超过了实际需要的额外容量(以避免很快再次添加新容量),并且达到了较慢性能路径的阈值早些时候。

如果您想查看代码或自己测试:github

我的问题是,为什么性能会下降到 13k-16k 左右?

对我来说,在 v8 中,从大约 13k-16k 元素开始,较大的数组会受到不同的处理,以提高它们的性能,但截止点(至少在我的代码中)有点太早,因此在优化带来之前性能下降任何好处。

您可以看到性能改进在大约 500 个元素后下降,并在下降后再次上升。

遗憾的是我找不到任何相关信息。

另外,如果您碰巧知道为什么在容量创建和推送求和结束时会出现这些峰值,请随时告诉我:)

编辑:

正如 @ggorlen 所建议的,我在另一台机器上运行相同的测试,以排除缓存作为所见行为的原因(使用不同的、较弱的 CPU 和较少的 RAM)。结果看起来非常相似。

在不同机器上根据长度创建数组的时间 在不同机器上基于长度的数组操作时间

编辑:

我使用该--allow-natives-syntax标志运行节点来调试日志创建的数组%DebugPrint(array);,希望看到不同数组长度之间的差异,但除了长度和内存地址之外,它们看起来都相同。这是一个例子:

// For array created with capacity
DebugPrint: 000002CB8E1ACE19: [JSArray]
 - map: 0x035206283321 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x036b86245b19 <JSArray[0]>
 - elements: 0x02cb8e1ace39 <FixedArray[1]> [HOLEY_SMI_ELEMENTS]
 - length: 1
 - properties: 0x0114c5d01309 <FixedArray[0]>
 - All own properties (excluding elements): {
    00000114C5D04D41: [String] in ReadOnlySpace: #length: 0x03f907ac1189 <AccessorInfo> (const accessor descriptor), location: descriptor
 }

0000035206283321: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: HOLEY_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x035206283369 <Map(PACKED_SMI_ELEMENTS)>
 - prototype_validity cell: 0x03f907ac15e9 <Cell value= 1>
 - instance descriptors #1: 0x009994a6aa31 <DescriptorArray[1]>
 - transitions #1: 0x009994a6a9d1 <TransitionArray[4]>Transition array #1:
     0x0114c5d05949 <Symbol: (elements_transition_symbol)>: (transition to PACKED_DOUBLE_ELEMENTS) -> 0x0352062832d9 <Map(PACKED_DOUBLE_ELEMENTS)>

 - prototype: 0x036b86245b19 <JSArray[0]>
 - constructor: 0x031474c124e9 <JSFunction Array (sfi = 000003CECD93C3A9)>
 - dependent code: 0x0114c5d01239 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0


// For array created with push
DebugPrint: 000003B09882CE19: [JSArray]
 - map: 0x02ff94f83369 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x0329b3805b19 <JSArray[0]>
 - elements: 0x03b09882ce39 <FixedArray[17]> [PACKED_SMI_ELEMENTS]
 - length: 1
 - properties: 0x03167aa81309 <FixedArray[0]>
 - All own properties (excluding elements): {
    000003167AA84D41: [String] in ReadOnlySpace: #length: 0x02094f941189 <AccessorInfo> (const accessor descriptor), location: descriptor
 }

000002FF94F83369: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x03167aa81599 <undefined>
 - prototype_validity cell: 0x02094f9415e9 <Cell value= 1>
 - instance descriptors #1: 0x00d25122aa31 <DescriptorArray[1]>
 - transitions #1: 0x00d25122aa01 <TransitionArray[4]>Transition array #1:
     0x03167aa85949 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x02ff94f83321 <Map(HOLEY_SMI_ELEMENTS)>

 - prototype: 0x0329b3805b19 <JSArray[0]>
 - constructor: 0x009ff8a524e9 <JSFunction Array (sfi = 0000025A84ABC3A9)>
 - dependent code: 0x03167aa81239 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
Run Code Online (Sandbox Code Playgroud)

编辑

当数组大小从 13_994 变为 13_995 时,求和的性能会下降:

在此输入图像描述

jmr*_*mrk 31

(V8 开发人员在此。)

TL;DR:这里没有什么可看的,只是产生令人困惑的工件的微基准。


这里有两个独立的效果:

  1. 在 16384 个元素处发生的情况是,后备存储分配在“大对象空间”中,这是针对大对象优化的堆的特殊区域。在启用了指针压缩的 Chrome 中,发生这种情况的元素数量是关闭指针压缩的 Node 中的两倍。其结果是分配本身不再能够直接在优化代码中作为内联指令序列发生;相反,它是对 C++ 函数的调用;除了有一些调用开销之外,它也是一个更通用的实现,因此速度有点慢(可能有一些优化潜力,不确定)。因此,这并不是一项太早开始的优化;而是一项优化。这只是巨大物体所付出的代价。而且它只会在(微小的微基准测试?)分配许多大型数组然后不对它们做太多事情的情况下显着出现。

  2. 在 13995 个元素处发生的情况是,对于特定函数sum,优化编译器会启动该函数的“OSR”(堆栈上替换),即该函数在运行时被优化代码替换。无论什么时候发生,这都是一笔一次性成本,而且很快就会收回成本。因此,将其视为特定时间的特定点击是典型的微基准测试工件,与现实世界的使用无关。(例如,如果您在同一进程中多次运行测试,您将不会再看到 13995 处的步骤。如果您使用多个大小运行它,那么很可能不需要 OSR(因为该函数可以下次调用时切换到优化代码)并且这种一次性成本根本不会发生。)