【问题标题】:Javascript Set vs. Array performanceJavascript 集合与数组性能
【发布时间】:2016-12-24 18:15:57
【问题描述】:

这可能是因为 Sets 对 Javascript 来说相对较新,但我无法在 StackO 或其他任何地方找到一篇关于 Javascript 中两者之间的性能差异的文章。那么,就性能而言,两者之间有什么区别?具体来说,在删除、添加和迭代方面。

【问题讨论】:

  • 您不能互换使用它们。所以比较它们是没有意义的。
  • 您是在谈论Set[]{} 之间的比较吗?
  • 添加和迭代没有太大区别,删除和 - 最重要的是 - 查找确实有所作为。
  • @zerkms — 严格来说,数组也不是有序的,但它们使用 index 可以让它们被视为是有序的。 ;-) Set 中的值序列按插入顺序保存。
  • @zerkms 说比较它们“没有什么意义”是荒谬的。它们都是收藏品。数组绝对可以用来代替集合,并且在 JavaScript 中不存在 Set 的 20 年里。

标签: javascript arrays performance set iteration


【解决方案1】:

好的,我已经测试了从数组和集合中添加、迭代和删除元素。我运行了一个“小”测试,使用 10 000 个元素和一个“大”测试,使用 100 000 个元素。这是结果。

向集合中添加元素

无论添加多少元素,.push 数组方法似乎比.add set 方法快大约 4 倍。

迭代和修改集合中的元素

在这部分测试中,我使用for 循环遍历数组,并使用for of 循环遍历集合。同样,遍历数组更快。这一次似乎是指数级的,因为在“小”测试期间花费了两倍的时间,在“大”测试期间花费了几乎四倍的时间。

从集合中移除元素

这就是有趣的地方。我使用for 循环和.splice 的组合从数组中删除一些元素,我使用for of.delete 从集合中删除一些元素。对于“小”测试,从集合中删除项目的速度大约快三倍(2.6 毫秒对 7.1 毫秒),但“大”测试的情况发生了巨大变化,从数组中删除项目需要 1955.1 毫秒,而它只将它们从集合中移除需要 83.6 毫秒,快 23 倍。

结论

在 10k 个元素时,两个测试的运行时间相当(数组:16.6 毫秒,集合:20.7 毫秒),但在处理 100k 个元素时,集合是明显的赢家(数组:1974.8 毫秒,集合:83.6 毫秒),但这仅仅是因为的删除操作。否则数组会更快。我不能确切地说这是为什么。

我尝试了一些混合场景,其中创建并填充了一个数组,然后将其转换为一个集合,其中一些元素将被删除,然后该集合将被重新转换为一个数组。尽管这样做会比删除数组中的元素提供更好的性能,但是与集合之间传输所需的额外处理时间超过了填充数组而不是集合的收益。最后,只处理一组会更快。尽管如此,这是一个有趣的想法,如果一个人选择使用数组作为一些没有重复的大数据的数据集合,那么如果需要在一个中删除许多元素,这可能是有利的性能明智的操作,将数组转换为集合,执行删除操作,并将集合转换回数组。

数组代码:

var timer = function(name) {
  var start = new Date();
  return {
    stop: function() {
      var end = new Date();
      var time = end.getTime() - start.getTime();
      console.log('Timer:', name, 'finished in', time, 'ms');
    }
  }
};

var getRandom = function(min, max) {
  return Math.random() * (max - min) + min;
};

var lastNames = ['SMITH', 'JOHNSON', 'WILLIAMS', 'JONES', 'BROWN', 'DAVIS', 'MILLER', 'WILSON', 'MOORE', 'TAYLOR', 'ANDERSON', 'THOMAS'];

var genLastName = function() {
  var index = Math.round(getRandom(0, lastNames.length - 1));
  return lastNames[index];
};

var sex = ["Male", "Female"];

var genSex = function() {
  var index = Math.round(getRandom(0, sex.length - 1));
  return sex[index];
};

var Person = function() {
  this.name = genLastName();
  this.age = Math.round(getRandom(0, 100))
  this.sex = "Male"
};

var genPersons = function() {
  for (var i = 0; i < 100000; i++)
    personArray.push(new Person());
};

var changeSex = function() {
  for (var i = 0; i < personArray.length; i++) {
    personArray[i].sex = genSex();
  }
};

var deleteMale = function() {
  for (var i = 0; i < personArray.length; i++) {
    if (personArray[i].sex === "Male") {
      personArray.splice(i, 1)
      i--
    }
  }
};

var t = timer("Array");

var personArray = [];

genPersons();

changeSex();

deleteMale();

t.stop();

console.log("Done! There are " + personArray.length + " persons.")

设置代码:

var timer = function(name) {
    var start = new Date();
    return {
        stop: function() {
            var end  = new Date();
            var time = end.getTime() - start.getTime();
            console.log('Timer:', name, 'finished in', time, 'ms');
        }
    }
};

var getRandom = function (min, max) {
  return Math.random() * (max - min) + min;
};

var lastNames = ['SMITH','JOHNSON','WILLIAMS','JONES','BROWN','DAVIS','MILLER','WILSON','MOORE','TAYLOR','ANDERSON','THOMAS'];

var genLastName = function() {
    var index = Math.round(getRandom(0, lastNames.length - 1));
    return lastNames[index];
};

var sex = ["Male", "Female"];

var genSex = function() {
    var index = Math.round(getRandom(0, sex.length - 1));
    return sex[index];
};

var Person = function() {
	this.name = genLastName();
	this.age = Math.round(getRandom(0,100))
	this.sex = "Male"
};

var genPersons = function() {
for (var i = 0; i < 100000; i++)
	personSet.add(new Person());
};

var changeSex = function() {
	for (var key of personSet) {
		key.sex = genSex();
	}
};

var deleteMale = function() {
	for (var key of personSet) {
		if (key.sex === "Male") {
			personSet.delete(key)
		}
	}
};

var t = timer("Set");

var personSet = new Set();

genPersons();

changeSex();

deleteMale();

t.stop();

console.log("Done! There are " + personSet.size + " persons.")

【讨论】:

  • 请记住,默认情况下,集合的值是唯一的。因此,当数组的 [1,1,1,1,1,1] 的长度为 6 时,一个集合的大小为 1。看起来您的代码实际上可能会生成大小差异很大的集合,因为集合的这一特性,每次运行时的大小可能超过 100,000 个项目。您可能从未注意到,因为直到整个脚本运行之后您才显示集合的大小。
  • @KyleFarris 除非我弄错了,否则如果集合中有重复项,例如您的示例 [1, 1, 1, 1, 1],这将是正确的,但由于集合中的每个项目实际上都是具有各种属性的对象,包括从数百个可能的名字列表中随机生成的名字和姓氏、随机生成的年龄、随机生成的性别和其他随机生成的属性......在集合中拥有两个相同对象的几率微乎其微。
  • 实际上,在这种情况下你是对的,因为看起来集合实际上并没有与集合中的对象区分开来。所以,实际上你甚至可以在集合中有相同的对象{foo: 'bar'} 10,000x,它的大小为 10,000。数组也是如此。它似乎只有标量值(字符串、数字、布尔值等)是唯一的。
  • 您可以在 Set 中多次拥有相同的对象的内容 {foo: 'bar'},但不是完全相同的对象(参考) .值得指出 IMO 的细微差别
  • 您忘记了使用 Set 的最重要原因是度量,即 0(1) 查找。 hasIndexOf.
【解决方案2】:

观察

  • 设置操作可以理解为执行流中的快照。
  • 我们没有找到明确的替代品。
  • Set 类 的元素没有可访问的索引。
  • Set classArray class 的补充,在我们需要存储一个集合来应用基本加法的场景中很有用, 删除、检查和迭代操作。

我分享一些性能测试。尝试打开您的控制台并复制粘贴以下代码。

创建数组 (125000)

var n = 125000;
var arr = Array.apply( null, Array( n ) ).map( ( x, i ) => i );
console.info( arr.length ); // 125000

1.查找索引

我们比较了 Set 的 has 方法和 Array indexOf:

数组/indexOf (0.281ms) |设置/ (0.053ms)

// Helpers
var checkArr = ( arr, item ) => arr.indexOf( item ) !== -1;
var checkSet = ( set, item ) => set.has( item );

// Vars
var set, result;

console.time( 'timeTest' );
result = checkArr( arr, 123123 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
checkSet( set, 123123 );
console.timeEnd( 'timeTest' );

2。添加新元素

我们分别比较Set和Array对象的add和push方法:

数组/推送 (1.612ms) |设置/添加 (0.006ms)

console.time( 'timeTest' );
arr.push( n + 1 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
set.add( n + 1 );
console.timeEnd( 'timeTest' );

console.info( arr.length ); // 125001
console.info( set.size ); // 125001

3.删除元素

在删除元素时,我们要记住 Array 和 Set 不是在相等的条件下开始的。数组没有原生方法,所以需要一个外部函数。

数组/deleteFromArr (0.356ms) |设置/删除 (0.019ms)

var deleteFromArr = ( arr, item ) => {
    var i = arr.indexOf( item );
    i !== -1 && arr.splice( i, 1 );
};

console.time( 'timeTest' );
deleteFromArr( arr, 123123 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
set.delete( 123123 );
console.timeEnd( 'timeTest' );

阅读全文here

【讨论】:

  • Array.indexOf 应该是 Array.includes 以使它们等效。我在 Firefox 上得到的数字非常不同。
  • 我会对 Object.includes 与 Set.has 的比较感兴趣...
  • @LeopoldKristjansson 我没有编写比较测试,但是我们在一个包含 24k 项的数组的生产站点中进行了计时,从 Array.includes 切换到 Set.has 是一个巨大的性能提升!跨度>
【解决方案3】:

我的观察是,考虑到大型数组的两个陷阱,Set 总是更好:

a) 从数组创建 Set 必须在具有预缓存长度的 for 循环中完成。

慢(例如 18 毫秒) new Set(largeArray)

快速(例如 6 毫秒) const SET = new Set(); const L = largeArray.length; for(var i = 0; i<L; i++) { SET.add(largeArray[i]) }

b) 迭代可以以相同的方式完成,因为它也比 for of 循环更快...

https://jsfiddle.net/0j2gkae7/5/

现实生活中的比较 difference()intersection()union()uniq()(+ 他们的迭代伙伴等)具有 40.000 个元素

【讨论】:

    【解决方案4】:

    对于您问题的迭代部分,我最近运行了这个测试,发现 Set 的性能远远优于 10,000 个项目的数组(大约 10 倍的操作可能在同一时间范围内发生)。并且取决于浏览器在类似测试中击败或输给 Object.hasOwnProperty。

    Set 和 Object 都有它们的“has”方法,执行的时间似乎已摊销到 O(1),但根据浏览器的实现,单个操作可能需要更长或更快的时间。似乎大多数浏览器在 Object 中实现 key 的速度比 Set.has() 快。至少对我而言,在 Chrome v86 上,即使是对密钥进行额外检查的 Object.hasOwnProperty 也比 Set.has() 快约 5%。

    https://jsperf.com/set-has-vs-object-hasownproperty-vs-array-includes/1

    更新:2020 年 11 月 11 日:https://jsbench.me/irkhdxnoqa/2

    如果您想使用不同的浏览器/环境运行自己的测试。


    同样,我将添加一个基准,用于将项目添加到数组与设置和删除。

    【讨论】:

    • 请不要在你的答案中使用链接(除非链接到官方图书馆),因为这些链接可能会被破坏——就像你的情况一样。您的链接是 404。
    • 我使用了一个链接,但也复制了可用的输出。不幸的是,他们如此迅速地改变了他们的链接策略。
    • 现在更新了帖子截图和新的 JS 性能网站:jsbench.me
    • 我在这里写了为什么 Set.has() 更慢:stackoverflow.com/a/69338420/1474113 TL;DR:因为 V8 并没有优化 Set.has()。
    【解决方案5】:

    只是属性查找,很少或零写入

    如果属性查找是您的主要关注点,这里有一些数字。

    JSBench 测试https://jsbench.me/3pkjlwzhbr/1

    // https://jsbench.me/3pkjlwzhbr/1
    // https://docs.google.com/spreadsheets/d/1WucECh5uHlKGCCGYvEKn6ORrQ_9RS6BubO208nXkozk/edit?usp=sharing
    // JSBench forked from https://jsbench.me/irkhdxnoqa/2
    
    var theArr = Array.from({ length: 10000 }, (_, el) => el)
    var theSet = new Set(theArr)
    var theObject = Object.assign({}, ...theArr.map(num => ({ [num]: true })))
    var theMap = new Map(theArr.map(num => [num, true]))
    
    var theTarget = 9000
    
    
    // Array
    
    function isTargetThereFor(arr, target) {
      const len = arr.length
      for (let i = 0; i < len; i++) {
        if (arr[i] === target) {
          return true
        }
      }
      return false
    }
    function isTargetThereForReverse(arr, target) {
      const len = arr.length
      for (let i = len; i > 0; i--) {
        if (arr[i] === target) {
          return true
        }
      }
      return false
    }
    
    function isTargetThereIncludes(arr, target) {
      return arr.includes(target)
    }
    
    // Set
    
    function isTargetThereSet(numberSet, target) {
      return numberSet.has(target)
    }
    
    // Object 
    
    function isTargetThereHasOwnProperty(obj, target) {
      return obj.hasOwnProperty(target)
    }
    function isTargetThereIn(obj, target) {
      return target in obj
    }
    function isTargetThereSelectKey(obj, target) {
      return obj[target]
    }
    
    // Map
    
    function isTargetThereMap(numberMap, target) {
      return numberMap.has(target)
    }
    大批
    • for循环
    • for 循环(反向)
    • array.includes(target)
    • set.has(target)
    目的
    • obj.hasOwnProperty(target)
    • target in obj
    • obj[target]
    地图
    • map.has(target)
    2021 年 1 月,Chrome 87 的结果

    欢迎来自其他浏览器的结果,请更新此答案。
    您可以使用this spreadsheet 制作精美的屏幕截图。

    JSBench 测试从 Zargold's answer. 分叉

    【讨论】:

      【解决方案6】:
      console.time("set")
      var s = new Set()
      for(var i = 0; i < 10000; i++)
        s.add(Math.random())
      s.forEach(function(e){
        s.delete(e)
      })
      console.timeEnd("set")
      console.time("array")
      var s = new Array()
      for(var i = 0; i < 10000; i++)
        s.push(Math.random())
      s.forEach(function(e,i){
        s.splice(i)
      })
      console.timeEnd("array")
      

      对 10K 个项目的这三个操作给了我:

      set: 7.787ms
      array: 2.388ms
      

      【讨论】:

      • @Bergi 我最初也是这么想的,但确实如此。
      • @zerkms: 定义“工作” :-) 是的,forEach 之后的数组将为空,但可能与您预期的方式不同。如果一个人想要类似的行为,它也应该是s.forEach(function(e) { s.clear(); })
      • 嗯,它做了一些事情,但不是预期的:它删除了索引 i 和结尾之间的所有元素。这与delete 在片场的所作所为不同。
      • @Bergi 哦,对了,它在 2 次迭代中删除了所有内容。我的错。
      • 在 1 次迭代中。 splice(0) 清空一个数组。
      猜你喜欢
      • 2018-11-19
      • 2018-06-03
      • 2020-01-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-04
      相关资源
      最近更新 更多