Javascript集与数组性能


87

可能是因为Sets是Java的相对较新的东西,但是我在StackO或其他任何地方都找不到关于Java两者之间的性能差异的文章。那么,就性能而言,两者之间有什么区别?具体地说,涉及删除,添加和迭代。


1
您不能互换使用它们。因此,将它们进行比较几乎没有任何意义。
zerkms

您是在谈论Setand和[]or之间的比较{}吗?
eithed

2
添加和迭代并没有多大区别,删除和-最重要的是-查找确实有区别。
Bergi


3
@zerkms —严格来说,数组也不是有序的,但是使用索引可以使它们被视为有序。;-)集合中的值序列按插入顺序保留。
RobG

Answers:


98

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

向集合中添加元素

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

遍历和修改集合中的元素

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

从集合中删除元素

现在,这变得有趣了。我使用了for循环的组合,.splice从数组中删除了一些元素,然后使用了for of.delete从集合中删除了一些元素。对于“小型”测试,从集合中删除项目的速度快了大约三倍(2.6 ms与7.1 ms),但是对于“大型”测试,情况发生了很大变化,在大型测试中,从阵列中删除项目的时间为1955.1 ms花了83.6毫秒将它们从设备集中删除,速度提高了23倍。

结论

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

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

数组代码:

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,1]数组的长度为6,而一个集合的大小为1。由于Sets的这一特性,看起来您的代码实际上每次生成的集合大小都与100,000个项目大不相同。您可能从未注意到过,因为直到运行整个脚本后才显示集合的大小。
KyleFarris '16

6
@KyleFarris除非我没有记错,否则如果集合中有重复项,这将是正确的,就像您的示例一样[1, 1, 1, 1, 1],但是由于集合中的每个项目实际上都是具有各种属性的对象,包括从列表中随机生成的名字和姓氏数百种可能的名字,一个随机产生的年龄,一个随机产生的性别以及其他随机产生的属性...在集合中拥有两个相同对象的几率很小。
snowfrogdev '16

3
实际上,在这种情况下您是对的,因为看起来集实际上并没有与集合中的对象区分开。因此,实际上,您甚至可以{foo: 'bar'}在集合中具有相同的对象10,000x,并且其大小为10,000。数组也是如此。似乎只有标量值(字符串,数字,布尔值等)才是唯一的。
KyleFarris

12
您可能在Set中多次具有相同对象的 完全相同的内容{foo: 'bar'},但没有完全相同的对象(参考)。值得指出的细微差别IMO
SimpleVar

14
您忘记了使用Set的最重要原因,即0(1)查找。hasIndexOf
Magnus

64

观察

  • 设置操作可以理解为执行流中的快照。
  • 我们不是最终的替代者。
  • Set类的元素没有可访问的索引。
  • Set类Array类的补充,在需要存储集合以应用基本加法,删除,检查和迭代操作的情况下很有用。

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

创建一个数组(125000)

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

1.查找索引

我们将Set的has方法与数组indexOf进行了比较:

Array / 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不会在相同条件下启动。数组没有本机方法,因此需要外部函数。

Array / 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' );

此处阅读全文


4
Array.indexOf应该是Array.includes,以使它们等效。我在Firefox上得到的数字大相径庭。
kagronick

2
我会对Object.includes与Set.has的比较感兴趣...
Leopold Kristjansson

1
@LeopoldKristjansson我没有编写比较测试,但是我们在生产站点中使用24k项的数组进行计时,并从Array.includes切换到Set.has极大地提高了性能!
sedot

3

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

a)从数组创建集合必须在 for具有预缓存长度循环中。

慢(例如18ms) new Set(largeArray)

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

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

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

与,和(+及其迭代的同伴等)的真实生活比较 difference(),其中包含40.000个元素intersection()union()uniq()


3

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

Set和Object都有它们的“ has”方法,这些方法似乎摊销到O(1),但是根据浏览器的实现,单个操作可能会花费更长或更长时间。似乎大多数浏览器在Object中实现键的速度都比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

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


类似地,我将添加一个基准,用于将项目添加到数组vs集和删除。


4
请不要在您的答案中使用链接(除非链接到官方图书馆),因为这些链接可能会损坏-如您的情况。您链接是404
吉尔Epshtain

我使用了链接,但在可用时也复制了输出。不幸的是,他们如此快速地更改了链接策略。
Zargold

现在用截图和新的JS性能网站更新了帖子:jsbench.me
Zargold

-5
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

1
@zerkms:定义“ work” :-)是的,数组在之后会为空forEach,但可能不是您期望的那样。如果人们想要可比的行为,也应该如此s.forEach(function(e) { s.clear(); })
Bergi

1
好吧,它做了一些事情,只是没有达到预期目的:它删除了索引i和末尾之间的所有元素。这delete与Set上的内容不符。
Trincot

@Bergi哦,对了,它只用了2次迭代就删除了所有内容。我的错。
zerkms

4
在1次迭代中。splice(0)清空数组。
Trincot
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.