【问题标题】:Why is array.push sometimes faster than array[n] = value?为什么 array.push 有时比 array[n] = value 快?
【发布时间】:2016-12-31 23:50:39
【问题描述】:

作为测试一些代码的附带结果,我编写了一个小函数来比较使用 array.push 方法与直接寻址 (array[n] = value) 的速度。令我惊讶的是,推送方法通常显示更快,尤其是在 Firefox 中,有时在 Chrome 中。只是出于好奇:有人对此有解释吗? 可以找到测试@this page(点击'数组方法比较')

【问题讨论】:

  • 如果 IE6 更新足够,应该支持。据我回忆,在 IE 5.5 左右的某个地方出现了一个支持推送的新 jscript 引擎(在此之前,我使用了 home brew Array augmentations)。
  • 当然你可以将 push 添加到 ie6 数组——但这可能会被实现为 function push(value) { this[this.length] = value } 所以你会测试相同的东西
  • IE6 总是至少有 JScript 5.6。只有 IE 5.0 的基本 JScript 实现不支持 Array.push();其他人都在祖先的 JavaScript 1.2 中恢复了它。

标签: javascript arrays performance firefox browser


【解决方案1】:

各种因素都会发挥作用,大多数 JS 实现都使用平面数组,如果以后有必要,它会转换为稀疏存储。

基本上,变得稀疏的决定是一种启发式方法,基于设置了哪些元素,以及为了保持平坦而浪费了多少空间。

在您的情况下,您首先设置最后一个元素,这意味着 JS 引擎将看到一个长度为 n 但只有一个元素的数组。如果n 足够大,这将立即使数组成为稀疏数组——在大多数引擎中,这意味着所有后续插入都将采用慢速稀疏数组的情况。

您应该添加一个额外的测试,在其中将数组从索引 0 填充到索引 n-1 - 它应该会快得多。

为了回应@Christoph 并出于拖延的愿望,这里描述了数组是如何(通常)在 JS 中实现的——具体情况因 JS 引擎而异,但一般原则是相同的。

所有 JS Objects(不是字符串、数字、true、false、undefinednull)都继承自基本对象类型——具体实现有所不同,可能是 C++ 继承,也可能是手动继承在 C 中(无论哪种方式都有好处)——基本 Object 类型定义了默认的属性访问方法,例如。

interface Object {
    put(propertyName, value)
    get(propertyName)
private:
    map properties; // a map (tree, hash table, whatever) from propertyName to value
}

此 Object 类型处理所有标准属性访问逻辑、原型链等。 那么Array的实现就变成了

interface Array : Object {
    override put(propertyName, value)
    override get(propertyName)
private:
    map sparseStorage; // a map between integer indices and values
    value[] flatStorage; // basically a native array of values with a 1:1
                         // correspondance between JS index and storage index
    value length; // The `length` of the js array
}

现在,当您在 JS 中创建数组时,引擎会创建类似于上述数据结构的内容。当您将对象插入 Array 实例时,Array 的 put 方法会检查属性名称是否为介于 0 和 2^32 之间的整数(或可以转换为整数,例如“121”、“2341”等) -1(或者可能是 2^31-1,我完全忘记了)。如果不是,则将 put 方法转发到基 Object 实现,并完成标准的 [[Put]] 逻辑。否则将值放入 Array 自己的存储中,如果数据足够紧凑,则引擎将使用平面数组存储,在这种情况下插入(和检索)只是标准的数组索引操作,否则引擎将转换数组稀疏存储,并 put/get 使用映射从 propertyName 获取到 value 位置。

老实说,我不确定当前是否有任何 JS 引擎在转换发生后从稀疏存储转换为平面存储。

Anyhoo,这是对所发生事情的一个相当高级的概述,并省略了一些更令人讨厌的细节,但这是一般的实现模式。附加存储的具体方式以及 put/get 的分派方式因引擎而异——但这是我能真正描述设计/实现的最清楚的内容。

一个小的补充点,虽然 ES 规范将 propertyName 称为字符串 JS 引擎也倾向于专注于整数查找,所以如果您正在查看一个字符串,someObject[someInteger] 不会将整数转换为字符串具有整数属性的对象,例如。数组、字符串和 DOM 类型(NodeLists 等)。

【讨论】:

  • @olliej:“大多数 JS 实现使用平面数组,如果以后有必要,它会转换为稀疏存储” - 很有趣。那么数组对象有两种存储方式:一种用于常规属性,一种用于数组条目?
  • @Christoph:是的——如果你愿意,我可以详细介绍,但它会偏向于 JavaScriptCore/Nitro 实现——SpiderMonkey、V8 和 KJS 中的通用模型是相同的,但我不知道他们的确切实施细节
  • @olliej:刚刚检查了 SpiderMonkey 来源:JSObject 结构包含一个 dslot 成员(d 表示动态),只要 JS 数组密集,它将保存一个实际数组;我没有检查稀疏数组或使用非数组索引属性名称时会发生什么
  • @olliej:谢谢,这很有意义。我在页面上添加了一个 [0..n] 测试,它更快,我明白为什么。与 push [0..n] 相比,在所有浏览器中都更快。
  • @Christoph:是的,它们是我在(过长)带注释的答案中提到的 C 风格的实现; JSC、V8 和 KJS 都是 C++ impls,JSC 和 V8 将属性哈希表与对象分开存储,iirc SM 使用树而不是哈希表 - 每个人做同样的事情都不同
【解决方案2】:

这些是我通过你的测试得到的结果

在 Safari 上:

  • Array.push(n) 1,000,000 个值:0.124 秒
  • 数组[n .. 0] = 值 (递减)1,000,000 个值:3.697 秒
  • Array[0 .. n] = 值(升序) 1,000,000 个值:0.073 秒

在火狐上:

  • Array.push(n) 1,000,000 个值:0.075 秒
  • Array[n .. 0] = 值(降序)1,000,000 个值:1.193 秒
  • Array[0 .. n] = 值(升序)1,000,000 个值:0.055 秒

在 IE7 上:

  • Array.push(n) 1,000,000 个值:2.828 秒
  • Array[n .. 0] = 值(降序)1,000,000 个值:1.141 秒
  • Array[0 .. n] = 值(升序)1,000,000 个值:7.984 秒

根据您的测试push 方法在 IE7 上似乎更好(巨大的差异),并且由于在其他浏览器上差异很小,它似乎是push 方法确实是向数组添加元素的最佳方法。

但是我创建了另一个simple test script 来检查什么方法可以快速将值附加到数组,结果真的让我感到惊讶,使用 Array.length 似乎比使用 Array.push 快得多,所以我真的不知道该说什么或想什么了,我一无所知。

顺便说一句:在我的 IE7 上,您的脚本停止并且浏览器询问我是否要让它继续运行(您知道典型的 IE 消息说:“停止运行此脚本?...”) 我建议减少一点循环。

【讨论】:

    【解决方案3】:

    push() 是更一般的 [[Put]] 的特例,因此可以进一步优化:

    在数组对象上调用 [[Put]] 时,必须首先将参数转换为无符号整数,因为所有属性名称(包括数组索引)都是字符串。然后必须将其与数组的长度属性进行比较,以确定是否必须增加长度。推送时,无需进行此类转换或比较:只需将当前长度作为数组索引并增加它即可。

    当然还有其他因素会影响运行时,例如调用push() 应该比通过[] 调用[[Put]] 慢,因为必须检查原型链是否存在前者。


    正如 olliej 所指出的:实际的 ECMAScript 实现将优化转换,即对于数字属性名称,不进行从字符串到 uint 的转换,而只是进行简单的类型检查。基本假设应该仍然成立,尽管它的影响会比我最初假设的要小。

    【讨论】:

    • 所有 JS 引擎实际上都优化了整数的 [[Put]],假设如果你使用一个整数,它可能是一个对整数属性名称有特殊处理程序的类型——例如。数组、字符串以及 DOM 类型(NodeLists、CanvasPixelArray 等)
    • 错误,完成最后一条评论 - 他们首先假设 Integer,然后通用对象回退会将 Integer 转换为字符串并使用字符串表示重试。
    【解决方案4】:

    这是一个很好的测试平台,它证实直接分配比推送快得多:http://jsperf.com/array-direct-assignment-vs-push

    编辑:显示累积结果数据似乎存在一些问题,但希望它很快得到修复。

    【讨论】:

    • 您的测试存在严重缺陷。在两个测试中,您预先分配了每个包含 1,000 个元素的数组。在您的push 测试中,您然后使用push 添加另外1,000 个元素。通过在您的第一个测试中将new Array(len) 简单地更改为[],我看到的结果更接近,实际上建议使用空数组中的push 稍微更快jsbin.com/epesed/22
    • 感谢您的评论!是的,你是对的。缓慢的部分是创建一个数组,而不是推送。我更新了答案。
    • 您为什么要发表评论“请忽略下面的测量表。请参阅编辑 2。”?为什么不直接删除我们应该忽略的表?你的回答写得很混乱。没有人关心编辑,他们关心写得好的答案。如果人们确实关心编辑历史,那么他们就可以使用。
    • 这是令人困惑的答案,我同意。这张桌子对我来说是新测量的基础。
    • 我找到了一个 jsperf 并用它替换了我的混乱表格。
    【解决方案5】:

    array[n] = value(升序时)总是比array.push 快​​,如果前一种情况下的数组先用长度初始化。

    通过检查your page 的javascript 源代码,您的Array[0 .. n] = value (ascending) 测试没有提前用长度初始化数组。

    所以Array.push(n) 有时会在第一次运行时领先,但在随后的测试运行中,Array[0 .. n] = value (ascending) 实际上始终表现最佳(在 Safari 和 Chrome 中)。

    如果代码被修改,因此它会提前初始化一个长度为 var array = new Array(n) 的数组,然后 Array[0 .. n] = value (ascending) 表明 array[n] = value 在我的此特定测试代码的基本运行。

    这与@Timo Kähkönen 报道的其他测试一致。具体看他提到的这个测试版本:https://jsperf.com/push-method-vs-setting-via-key/10

    修改后的代码,因此您可能会看到我如何编辑它并以公平的方式初始化数组(而不是不必要地使用 array.push 测试用例的长度对其进行初始化):

    function testArr(n, doPush){
    
      var now = new Date().getTime(),
                      duration,
                      report =  ['<b>.push(n)</b>',
                                 '<b>.splice(0,0,n)</b>',
                                 '<b>.splice(n-1,0,n)</b>',
                                 '<b>[0 .. n] = value</b> (ascending)',
                                 '<b>[n .. 0] = value</b> (descending)'];
      doPush = doPush || 5;
    
      if (doPush === 1) {
       var arr = [];
       while (--n) {
         arr.push(n);
       }
      } else if (doPush === 2) {
       var arr = [];
       while (--n) {
        arr.splice(0,0,n);
       }
      } else if (doPush === 3) {
       var arr = [];
       while (--n) {
        arr.splice(n-1,0,n);
       }
      } else if (doPush === 4) {
       var arr = new Array(n);
       for (var i = 0;i<n;i++) {
        arr[i] = i;
       }
      } else {
        while (--n) {
        var arr = [];
          arr[n] = n;
        }
      }
      /*console.log(report[doPush-1] + '...'+ arr.length || 'nopes');*/
      duration = ((new Date().getTime() - now)/1000);
      $('zebradinges').innerHTML +=  '<br>Array'+report[doPush-1]+' 1.000.000 values: '+duration+' sec' ;
      arr = null;
    }
    

    【讨论】:

      【解决方案6】:

      Push 将它添加到末尾,而 array[n] 必须遍历数组才能找到正确的位置。可能取决于浏览器及其处理数组的方式。

      【讨论】:

      • 如果测试 n 是已知的(它相当于 [array].length-1),所以没有进行搜索。
      • 如果你正在寻找第 n 个元素,它需要在数组中找到指向该位置的指针来填充值。
      • 在测试的情况下,n 是已知的。但是,Javascript 库是在完全不了解您的测试的情况下编写的,即使您完全知道它在哪里,也可能仍然让 [] 搜索数组以找到正确的位置。想象一个带有尾指针的链表。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-11-14
      • 1970-01-01
      • 1970-01-01
      • 2016-04-15
      • 1970-01-01
      • 2014-07-06
      相关资源
      最近更新 更多