JavaScript中对象/数组的性能如何?(专门针对Google V8)


105

与JavaScript(特别是Google V8)中的数组和对象相关的性能非常有趣。我在Internet上的任何地方都找不到有关此主题的详尽文章。

我了解某些对象使用类作为其基础数据结构。如果有很多属性,有时将其视为哈希表?

我也了解有时将数组像C ++数组一样对待(即快速随机索引,缓慢删除和调整大小)。而且,有时将它们更像对象(快速索引,快速插入/删除,更多内存)。而且,也许有时它们以链接列表的形式存储(即慢速随机索引,开始/结尾处的快速删除/插入)

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

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

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

任何文章或更多细节的链接也将不胜感激。:)

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

例如,假设我用...创建一个数组

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...
}

对于常规阵列,性能将很糟糕。相反,如果使用了LinkedList……还不错。


2
访问jsperf.com并创建测试用例。
罗布W

2
@RobW除了简单的测试可以说明问题之外,还有更多的事情在起作用,这需要了解JIT编译器的工作方式以及数据的处理方式。如果我有时间,我会添加一个答案,但希望其他人有时间来了解这些细节。我也想在此保留此链接
隐身

我正在谈论的JIT就是对象的“形状”,或者在定义的元素之间具有未定义值的数组,以及最近试验过的类型专用功能...特定于数组的方法可能取决于使用以及原型是否已被操纵。没有“知道”切换到另一个数据类型AFAIK的事情。
隐身

1
Google代表讨论了各种优化器和内部系统如何工作。以及如何针对它们进行优化。(用于游戏!)youtube.com/watch?v=XAqIpGU8ZZk
PicoCreator 2012年

Answers:


279

我创建了一个测试套件,正是为了探索这些问题(以及更多)归档副本)。

从这个意义上讲,您可以在此50多个测试用例测试器中看到性能问题(这将花费很长时间)。

顾名思义,它还探索了使用DOM结构的本机链接列表性质的用法。

(当前关闭,正在重建中)有关此的更多详细信息,请参见我的博客

总结如下

  • V8阵列非常快
  • 数组推入/弹出/移位比任何等效对象快约20倍以上。
  • 令人惊讶的Array.shift()是,它快于数组弹出的速度约6倍,但快于对象属性删除的速度约100倍。
  • 有趣的是,Array.push( data );它的速度快Array[nextIndex] = data了将近20倍(动态数组)到10倍(固定数组)。
  • Array.unshift(data) 比预期的要慢,并且比新添加的属性要慢大约5倍。
  • 清空值array[index] = null比删除delete array[index]数组中的值(未定义)快约4倍++。
  • 令人惊讶的是,将对象中的值清空obj[attr] = null比仅删除属性要慢大约2倍delete obj[attr]
  • 毫不奇怪,中间阵列Array.splice(index,0,data)很慢,非常慢。
  • 出人意料的是,它Array.splice(index,1,data)已经过优化(无长度变化),比仅接头快100倍Array.splice(index,0,data)
  • 毫不奇怪,divLinkedList在所有扇区上都比阵列要差,除去时除外dll.splice(index,1)(它破坏了测试系统)。
  • 这一切的最大惊喜 [正如jjrv指出的],V8阵列的写入速度比V8读取的速度稍快= O

注意:这些指标仅适用于v8不会“完全优化”的大型阵列/对象。对于数组/对象大小小于任意大小(24?)的情况,可能存在非常孤立的优化性能情况。更多细节可以在多个Google IO视频中广泛看到。

注意2:这些出色的性能结果并未在浏览器(尤其是*cough*IE)之间共享 。测试也是巨大的,因此我尚未完全分析和评估结果:请在=)中进行编辑

更新的说明(2012年12月): Google代表在youtube上播放了一些视频,描述了chrome本身的内部工作原理(例如,当Chrome浏览器从链表列表切换为固定数组等时),以及如何对其进行优化。有关更多信息,请参见GDC 2012:从控制台到Chrome


2
其中一些结果看起来很奇怪。例如,在Chrome阵列中,写入速度大约是读取速度的10倍,而在Firefox中则相反。您确定浏览器JIT在某些情况下不能优化整个测试吗?
jjrv 2012年

1
@jjrv good gosh = O你是对的...我什至将每个写案例都更新为增量唯一,以防止JIT ...说实话,除非JIT优化那么好(我很难相信),可能只是读取优化不佳或写入严重优化(写入立即缓冲区?)的情况。。。值得研究:大声笑
PicoCreator

2
只是想在视频中讨论增加对数组的确切地点:youtube.com/...
badunk

1
JsPerf网站不再存在:(
JustGoscha 2014年

1
@JustGoscha好的,谢谢你的信息:我通过从Google缓存重新创建它来修复了它。
PicoCreator

5

在JavaScript范围内的基本层次上,对象的属性是更为复杂的实体。您可以使用setters / getters创建具有不同可枚举性,可写性和可配置性的属性。数组中的项目无法通过这种方式进行自定义:它存在或不存在。在底层引擎级别,这可以在组织表示结构的内存方面进行更多优化。

在从对象(字典)中识别数组方面,JS引擎始终在两者之间做出明确的区分。这就是为什么有很多文章尝试创建类似半伪数组的对象的方法,这些对象的行为类似于一个但允许其他功能。甚至存在这种分离的原因是因为JS引擎本身将二者存储的方式不同。

属性可以存储在数组对象上,但这仅说明了JavaScript如何坚持将所有内容制作为对象。数组中的索引值与您决定在代表基础数组数据的数组对象上设置的所有属性的存储方式不同。

每当您使用合法的数组对象并使用操纵该数组的标准方法之一时,您都将访问基础数组数据。特别是在V8中,这些基本上与C ++数组相同,因此将应用这些规则。如果由于某种原因您正在使用一个引擎无法确定其是否为数组的数组,那么您将处于更加不稳定的境地。在最新版本的V8中,还有更多的工作空间。例如,可以创建一个以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;
}

1

我想通过调查实现对增长数组的行为的问题来补充现有的答案:如果他们以“通常”的方式实现它们,人们会看到许多快速推送,而很少的,散布的缓慢推送会在实现复制时复制从一个缓冲区到更大缓冲区的数组的内部表示。

您可以很好地看到这种效果,这来自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

即使对每个推送进行了概要分析,输出也只包含那些花费时间超过某个阈值的推送。对于每个测试,我都自定义了阈值,以排除所有看起来代表快速推送的推送。

因此,第一个数字代表已插入哪个元素(第一行代表第17个元素),第二个代表花费了多长时间(对于许多数组,基准并行完成),最后一个值是第一个数字以前一行中的数字为准。

Chrome排除所有执行时间少于2毫秒的行。

您会看到Chrome以1.5的幂数增加了数组大小,加上一些偏移量以解决小型数组的问题。

对于Firefox,它是两个的幂:

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

首先是2的幂,然后是三分之五的幂。

因此,所有常见的实现都对数组使用“常规”方式(例如,不要为绳索而疯狂)。

这是基准代码,这是其中的小提琴

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;
    }
}

0

在node.js 0.10(基于v8构建)下运行时,我发现CPU使用率似乎过高。我将一个性能问题追溯到一个函数,该函数正在检查数组中是否存在字符串。所以我进行了一些测试。

  • 已加载90,822位主机
  • 加载配置花费了0.087秒(数组)
  • 加载配置花费了0.152秒(对象)

(通过validate&push)将91k条目加载到数组中比设置obj [key] = value更快。

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

  • 搜索配置花费了87.56秒(数组)
  • 搜索配置花费了0.21秒(对象)

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

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.