在V8中使用数组(性能问题)

ytt*_*ium 25 javascript arrays firefox google-chrome v8

我尝试了下一个代码(它在Google Chrome和nodejs中显示了类似的结果):

var t = new Array(200000); console.time('wtf'); for (var i = 0; i < 200000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 27839.499ms
undefined
Run Code Online (Sandbox Code Playgroud)

我也跑了下一个测试:

var t = []; console.time('wtf'); for (var i = 0; i < 400000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 449.948ms
undefined
var t = []; console.time('wtf'); for (var i = 0; i < 400000; ++i) {t.push(undefined);} console.timeEnd('wtf');
wtf: 406.710ms
undefined
Run Code Online (Sandbox Code Playgroud)

但在Firefox中,第一个变体看起来都很好:

>>> var t = new Array(200000); console.time('wtf'); ...{t.push(Math.random());} console.timeEnd('wtf');
wtf: 602ms
Run Code Online (Sandbox Code Playgroud)

V8会发生什么?

UPD *神奇地降低了性能*

var t = new Array(99999); console.time('wtf'); for (var i = 0; i < 200000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 220.936ms
undefined
var t = new Array(100000); t[99999] = 1; console.time('wtf'); for (var i = 0; i < 200000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 1731.641ms
undefined
var t = new Array(100001); console.time('wtf'); for (var i = 0; i < 200000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 1703.336ms
undefined
var t = new Array(180000); console.time('wtf'); for (var i = 0; i < 200000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 1725.107ms
undefined
var t = new Array(181000); console.time('wtf'); for (var i = 0; i < 200000; ++i) {t.push(Math.random());} console.timeEnd('wtf');
wtf: 27587.669ms
undefined
Run Code Online (Sandbox Code Playgroud)

Esa*_*ija 57

如果您预先分配,请不要使用,.push因为您将创建由哈希表支持的稀疏数组.您可以预先分配最多99999个元素的稀疏数组,这些元素将由C数组支持,之后它就是一个哈希表.

使用第二个数组,您将从0开始以连续的方式添加元素,因此它将由真正的C数组支持,而不是哈希表.

粗略地说:

如果你的数组索引很好地从0到长度-1,没有空洞,那么它可以用快速C数组表示.如果你的数组中有漏洞,那么它将由一个慢得多的哈希表表示.例外情况是,如果您预先分配大小<100000的数组,那么您可以在数组中有漏洞并仍然得到C数组的支持:

var a = new Array(N); 

//If N < 100000, this will not make the array a hashtable:
a[50000] = "sparse";

var b = [] //Or new Array(N), with N >= 100000
//B will be backed by hash table
b[50000] = "Sparse";
//b.push("Sparse"), roughly same as above if you used new Array with N > 0
Run Code Online (Sandbox Code Playgroud)

  • @Esailija其实你所写的并不完全正确.如果元素处于快速模式,则内置的ArrayPush会尝试将元素保持在快速模式.请参阅https://code.google.com/p/v8/source/browse/trunk/src/builtins.cc#547中的逻辑 (5认同)
  • @yttrium由于从字典模式返回快速模式的启发式工作方式,差异发生了.如果您的数组处于字典模式,那么每当它需要增长时,V8会检查它是否足够密集以及它是否可以通过使用连续(类C)数组而不是字典来赢得空间.以180000为起点,启发式攻击速度很快,启发式命中次数为181000次.因此差异.启发式在这里:https://code.google.com/p/v8/source/browse/trunk/src/objects.cc?r = 14954#12483 (3认同)

Dom*_*omi 7

看似无限的数组 [2020]

在现代 V8 中,数组现在可以具有任意大小。您可以以任何您喜欢的方式使用[]new Array(len),甚至可以随机访问。

在当前的 Chrome(我猜是任何 V8 环境)中,数组的长度最多可达 2^32-1。

在此输入图像描述

在此输入图像描述

但是,有一些注意事项:

字典模式仍然适用

正如jmrk在评论中提到的,数组并不是神奇的存在。相反,较小的数组(达到某个阈值,显然现在达到几百万个元素)根本不稀疏,而只是看起来稀疏。因此,它们将耗尽其所有元素的实际内存。一旦达到阈值,数组就会退回到字典模式

它们现在更容易使用,但它们内部的工作原理仍然与以前相同。

您需要初始化一个空数组

一方面,for循环按预期工作,但是,Array内置的高阶函数(例如mapfilterfindsome会忽略未分配的元素。他们fill首先需要(或其他一些填充方法):

const a = new Array(10);
const b = new Array(10).fill(0);

a.forEach(x => console.log(x)); // does nothing
b.forEach(x => console.log(x)); // works as intended
Run Code Online (Sandbox Code Playgroud)

数组构造函数代码函数SetLengthWouldNormalize常量来看,在采用字典模式之前kMaxFastArrayLength它现在可以支持几乎任意数量(目前上限为 3200 万)的元素

但请注意,随着 V8 优化变得越来越复杂,现在还有更多的考虑因素。2017 年的这篇官方博客文章解释了数组可以区分 21 种不同类型的数组(或者更确切地说,数组元素类型),引用一下:

“每个都有自己的一组可能的优化”

如果“稀疏数组有效,那就这样吧!” 对你来说还不够好,我推荐以下内容:

原帖

如果您在 Chrome 或 Node 中(或者更一般地说,在 V8 中)预先分配带有 >100000元素的数组,它们会回退到字典模式,从而使速度变得非常慢。

感谢该线程中的一些评论,我能够追踪到object.h 的 kInitialMaxFastElementArray

然后,我使用该信息在 v8 存储库中提交了一个问题,该问题现在开始获得一些关注,但仍需要一段时间。我引用:

我希望我们最终能够完成这项工作。但这可能还有一段路要走。