对象/数组在JavaScript中的性能是什么? (专门针对Google V8)

与JavaScript中的数组和对象(尤其是Google V8)相关的性能将会非常有趣。 我在互联网上的任何地方找不到关于这个主题的综合性文章

我知道有些对象使用类作为其基础数据结构。 如果有很多属性,它有时被视为一个哈希表?

我也明白,数组有时被视为C ++数组(即快速随机索引,缓慢删除和resize)。 而且,其他时候,它们更像对象(快速索引,快速插入/删除,更多内存)。 而且,也许有时它们被存储为链接列表(即,慢速随机索引,在开始/结束时快速移除/插入)

Array / Object的检索和操作在JavaScript中的精确性能是什么? (专门针对Google V8)

更具体地说,它对性能的影响如下:

  • 将属性添加到对象
  • 从对象中删除一个属性
  • 索引对象中的属性
  • 将项目添加到数组
  • 从arrays中删除项目
  • 索引数组中的项目
  • 调用Array.pop()
  • 调用Array.push()
  • 调用Array.shift()
  • 调用Array.unshift()
  • 调用Array.slice()

任何文章或链接的更多细节,也将受到赞赏。 🙂

编辑:我真的很想知道JavaScript数组和对象如何在引擎盖下工作。 另外,在什么情况下 V8引擎“知道”切换到另一个数据结构?

例如,假设我创build一个数组…

var arr = []; arr[10000000] = 20; arr.push(21); 

这里究竟发生了什么?

或者…这个怎么样?

 var arr = []; //Add lots of items for(var i = 0; i < 1000000; i++) arr[i] = Math.random(); //Now I use it like a queue... for(var i = 0; i < arr.length; i++) { var item = arr[i].shift(); //Do something with item... } 

对于传统arrays来说,性能会很糟糕; 而如果一个LinkedList被使用…并不是那么糟糕。

更新: 请注意,JSPref目前正在closures

(我已经保存了一个testing用例的副本,并且一旦JSPref被修复/发现后继者,就会更新答案)


嗯…也许是一个矫枉过正的答案…但我创build了一个testing套件,正是为了探讨这些问题(和更多) ( 存档副本 )。

从这个意义上讲,你可以在这个50多个testing用例testing器中看到性能问题(这将需要很长时间)。

也正如其名称所示,它探索了使用DOM结构的本地链表性质的用法。

(目前正在下载,正在进行重build)在我的博客上有关于此的更多详细信息 。

总结如下

  • V8arrays很快,非常快
  • arrays推/stream行/移位约比任何对象快约20倍。
  • 令人惊讶的是, Array.shift()速度比数组popup要快6倍左右,但比对象属性的删除快约100倍。
  • 有趣的是, Array.push( data );Array[nextIndex] = data快近20(dynamic数组)到10(固定数组)以上。
  • Array.unshift(data)如预期的那样慢,约比新的属性添加慢约5倍。
  • delete array[index] array[index] = nulldelete array[index]要快, delete array[index] (undefined)约快4倍左右。
  • 令人惊讶的是,在一个对象中删除一个值是obj[attr] = null null〜比删除属性delete obj[attr]约2倍慢delete obj[attr]
  • 毫不奇怪,中间数组Array.splice(index,0,data)很慢,非常慢。
  • 令人惊讶的是, Array.splice(index,1,data)已经被优化了(没有长度变化), Array.splice(index,0,data)Array.splice(index,0,data)快了100倍
  • 毫不奇怪,divLinkedList不如所有行业的数组,除了dll.splice(index,1)去除(它打破了testing系统)。
  • 令人惊讶的是,正如jjrv指出的那样,V8arrays的写入速度比V8稍快一点= O

注意:这些度量仅适用于v8没有“完全优化”的大型数组/对象。 对于数组/对象大小小于任意大小(24?),可以有非常孤立的优化性能情况。 更多的细节可以看到几个谷歌IOvideo广泛。

注2:这些精彩的性能结果不是跨浏览器共享的,特别是*cough* IE。 此外,testing是巨大的,因此我还没有完全分析和评估的结果:请编辑=)

更新说明(2012年12月):谷歌代表在video上描述了Chrome本身的内部工作(如从链表列表切换到固定arrays等),以及如何优化它们。 有关更多信息,请参阅GDC 2012:从控制台到Chrome 。

更新的注释(二月2013): Thx @badunk,在确切点提供video链接

更新了Note(2016年6月): Thx @Benedikt,关于arrays在固定/dynamic数组中的性能差异。

在JavaScript范围内的基本层面上,对象的属性是非常复杂的实体。 您可以使用setter / getters创build属性,具有不同的可枚举性,可写性和可configuration性。 数组中的项目不能以这种方式进行自定义:它存在或不存在。 在底层的引擎层次上,这允许在组织代表结构的内存方面进行更多的优化。

在从对象(字典)中识别数组方面,JS引擎总是在两者之间做出明确的界限。 这就是为什么有很多关于尝试制作类似半像素的类似数组的对象的文章,但是却允许其他function。 这种分离甚至存在的原因是因为JS引擎本身存储两个不同。

属性可以存储在一个数组对象上,但是这只是简单地演示了JavaScript如何坚持把所有东西都变成一个对象。 数组中的索引值与您决定在代表基础数组数据的数组对象上设置的任何属性不同。

每当你使用一个合法的数组对象,并使用其中一个操作该数组的标准方法,你将会碰到底层的数组数据。 特别是在V8中,它们基本上与C ++数组相同,因此这些规则将适用。 如果由于某种原因,你正在使用一个引擎无法自信地确定的数组,那么你就会变得更加生气。 有了最新版本的V8,还有更多的工作空间。 例如,可以创build一个以Array.prototype为原型的类,并且仍然可以高效地访问各种本地数组操作方法。 但这是最近的一个变化。

最近对数组操作的更改的具体链接可能在这里派上用场:

还有一点,这里是直接来自V8源代码的Array Pop和Array Push,都是在JS中实现的:

 function ArrayPop() { if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) { throw MakeTypeError("called_on_null_or_undefined", ["Array.prototype.pop"]); } var n = TO_UINT32(this.length); if (n == 0) { this.length = n; return; } n--; var value = this[n]; this.length = n; delete this[n]; return value; } function ArrayPush() { if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) { throw MakeTypeError("called_on_null_or_undefined", ["Array.prototype.push"]); } var n = TO_UINT32(this.length); var m = %_ArgumentsLength(); for (var i = 0; i < m; i++) { this[i+n] = %_Arguments(i); } this.length = n + m; return this.length; } 

在node.js 0.10(构build于v8)下运行时,我发现CPU的使用量似乎对于工作负载来说过大了。 我将一个性能问题追溯到检查数组中是否存在string的函数。 所以我跑了一些testing。

  • 加载了90822个主机
  • 加载configuration花了0.087秒(arrays)
  • 加载configuration花了0.152秒(对象)

将91k条目加载到数组中(使用validation和推送)比设置obj [key] = value更快。

在下一个testing中,我查找了列表中的每个主机名(91k次迭代,以平均查找时间):

  • searchconfiguration花了87.56秒(arrays)
  • searchconfiguration花了0.21秒(对象)

这里的应用程序是Haraka(一个SMTP服务器),它在启动时(和更改后)加载一次host_list,然后在操作过程中执行这个查找数百万次。 切换到一个对象是一个巨大的performance胜利。

我想补充现有的答案,以调查如何实现行为增长arrays的问题:如果他们实施他们的“通常”的方式,人们会看到很多快速推动罕见,穿插缓慢推动实施复制数组的内部表示从一个缓冲区到较大的一个。

你可以很好地看到这个效果,这是从Chrome浏览器:

 16: 4ms 40: 8ms 2.5 76: 20ms 1.9 130: 31ms 1.7105263157894737 211: 14ms 1.623076923076923 332: 55ms 1.5734597156398105 514: 44ms 1.5481927710843373 787: 61ms 1.5311284046692606 1196: 138ms 1.5196950444726811 1810: 139ms 1.5133779264214047 2731: 299ms 1.5088397790055248 4112: 341ms 1.5056755767118273 6184: 681ms 1.5038910505836576 9292: 1324ms 1.5025873221216042 

即使每次推送都被分析,但输出只包含那些需要超过特定阈值的时间。 对于每个testing,我都会自定义阈值以排除所有表示快速推送的推送。

所以第一个数字表示哪个元素被插入(第一行是第17个元素),第二个是需要多长时间(对于许多数组,基准是并行完成的),最后一个值是第一个数字是前一行的数字。

对于Chrome,排除所有执行时间less于2毫秒的行。

您可以看到Chrome以1.5的幂增加了数组大小,加上一些偏移量来解释小数组。

对于Firefox来说,这是一个强大的function:

 126: 284ms 254: 65ms 2.015873015873016 510: 28ms 2.0078740157480315 1022: 58ms 2.003921568627451 2046: 89ms 2.0019569471624266 4094: 191ms 2.0009775171065494 8190: 364ms 2.0004885197850513 

我不得不在Firefox中增加门槛,这就是为什么我们从#126开始。

与IE浏览器,我们得到了一个混合:

 256: 11ms 256 512: 26ms 2 1024: 77ms 2 1708: 113ms 1.66796875 2848: 154ms 1.6674473067915691 4748: 423ms 1.6671348314606742 7916: 944ms 1.6672283066554338 

起初,这是两个权力,然后移动到五分之三的权力。

因此,所有常见的实现都使用数组的“正常”方式(而不是像绳子一样疯狂)。

这里是基准代码, 这里是它的小提琴 。

 var arrayCount = 10000; var dynamicArrays = []; for(var j=0;j<arrayCount;j++) dynamicArrays[j] = []; var lastLongI = 1; for(var i=0;i<10000;i++) { var before = Date.now(); for(var j=0;j<arrayCount;j++) dynamicArrays[j][i] = i; var span = Date.now() - before; if (span > 10) { console.log(i + ": " + span + "ms" + " " + (i / lastLongI)); lastLongI = i; } }