Javascript Set vs Array 性能

这可能是因为 Set 对 Javascript 来说是相对较新的,但是我还没有找到一篇关于 StackO 或其他任何地方的文章来讨论它们在 Javascript 中的性能差异。那么,就性能而言,两者之间有什么区别呢?特别是,当涉及到删除、添加和迭代时。

89281 次浏览
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")

一万件物品的三次手术给了我:

set: 7.787ms
array: 2.388ms

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

向集合添加元素

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

迭代和修改集合中的元素

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

从集合中移除元素

这才是有趣的地方。我使用了 for循环和 .splice的组合来从数组中删除一些元素,并使用了 for of.delete来从集合中删除一些元素。对于“小”测试,从集合中移除项目的速度要快3倍(2.6毫秒 vs 7.1毫秒) ,但是对于“大”测试,情况发生了巨大的变化: 从数组中移除项目需要1955.1毫秒,而从集合中移除项目只需要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.")

意见 :

  • 集合操作可以理解为执行流中的快照。
  • 我们还没有找到最终的替代品。
  • 上课的元素没有可访问的索引。
  • Set 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:

数组/索引(0.281 ms) | Set/已经(0.053 ms)

// 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.612 ms) | Set/(0.006 ms)

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

数组/DeteFromArr(0.356 ms) | Set/拿开(0.019 ms)

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

阅读完整的文章 给你

我的观察结果是,对于大型数组,Set 总是更好,因为它有两个缺陷:

A)从数组中创建 Set 必须在一个带有预设长度的 for循环中完成。

慢(例如18ms) 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个元素

对于问题的迭代部分,我最近运行了这个测试,发现 Set 的性能远远超过10,000个项目的 Array (大约10倍的操作可以在同一时间段内进行)。并且取决于浏览器是否在 like for like test 中击败或丢失 Object.hasOwnProperty。另一个有趣的地方是 Object 没有官方保证的顺序,而 JavaScript 中的 Set是作为 OrderedSet 实现的,并且确实维护插入顺序。

Set 和 Object 都有他们的“ has”方法,看起来分摊到 O (1) ,但是取决于浏览器的实现,一个操作可能需要更长或更快的时间。似乎大多数浏览器在 Object 中实现键的速度都快于 Set.has ()。即使 Object.hasOwnProperty 包含对密钥的额外检查,也比 Set.has ()快5% 左右,至少对我在 Chrome v86上是这样。

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

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

如果您想在不同的浏览器/环境中运行自己的测试,可以使用。 在这段时间(测试运行时) : Chrome 的 V8显然只针对对象进行了优化: 下面是2020年11月 Chrome v86的一个快照。

  1. 循环: 104167.14次/秒 ± 0.22% 最慢
  2. Array.include: 111524.8 ops/s ± 0.24% 1.07 x more ops/s than for loop (两者都是9k 迭代)
  3. 对于反向循环: 218074.48 ops/s ± 0.59% 1.96 x 非反向 Array.include (9k 迭代)
  4. Set.has:154744804.61运算/秒 ± 1.88% 709.6倍比循环反向多运算/秒(由于目标在右侧,只有1k 次迭代)
  5. hasOwnProperty:161399953.02操作/秒 ± 1.81% 1.043 x 比 Set.has多的操作/秒
  6. key in myObject:883055194.54操作/秒 ± 2.08% ... 比 myObject.hasOwnProperty多5倍操作/秒。

更新: 2022年11月10日: 我今天在 Safari 和 Chrome 上重新运行了相同的测试(比我的原始图像晚了两年) ,并且得到了一些有趣的结果: TLDR Set 即使不比使用 key in Object快,也同样快,而且比使用 Object.hasOwnProperty快得多。Chrome 还在某种程度上极大地优化了 Array.includes,使其处于与 Object/Set 查找时间相同的速度领域(而 for 循环需要1000 + x 更长的时间才能完成)。

Safari Set 的速度明显快于 key in Object,Object.hasOwnProperty 几乎没有达到同样的速度。所有数组变体(用于循环/包含)都比集合/对象查找慢得多。

快照11/10/2022: 在 Safari v16.1上测试每秒的操作(越高 = 越快) :

  • mySet.has(key):1,550,924,292.31
  • key in myObject:942,192,599.63(39.25% 的速度减慢,也就是说,使用 Set 你每秒可以执行大约1.6倍的操作
  • myObject.hasOwnProperty(key):21,363,224.51(慢98.62%) ,也就是说,当 hasOwnProperty 在1秒内检查时,您可以执行大约72.6 x 更多的 Set.has 操作。
  • 对于循环619,876.17操作/秒(目标是10,000次中的9,000次-所以对于循环的反向意味着只迭代1,000次对9,000次)意味着即使你知道项目的位置是有利的,你也可以比循环检查多做2502倍的设置查找。
  • For loop: 137,434 ops/s: 正如预期的那样,速度甚至更慢,但令人惊讶的是并没有慢多少: 对于循环来说,反向循环只需要循环迭代的1/9,而循环迭代只比循环快4.5倍。
  • Array.includes(target)111,076操作/秒比 for 循环手动检查目标稍微慢一些,你可以对每次包含检查手动执行1.23 x 检查。

在 Chrome v107.0.5304.8711/10/2022上: Set 的性能明显低于 Object in操作已经不再是事实: 它们现在几乎打成平手。(尽管预期的行为是,由于集合与对象之间的选项可能性较小,以及这是 Safari 中的行为,因此集合的性能会优于 Object。)值得注意的是,Array.include 显然在 Chrome (v8)中针对这种类型的测试进行了显著的优化:

  • Object in最快完成792894327.81次/秒 ± 2.51%

  • Set.prototype.has完成790523864.11次/秒 ± 最快2.22%

  • Array.prototype.includes完成679373215.29次/秒 ± 1.82% 14.32%

  • Object.hasOwnProperty完成时间为154217006.71次/秒 ± 1.31% 80.55%

  • for loop收盘时间为103015.26次/秒,增幅为0.98% ,增幅为99.99%


Nov10-2022-Chrome-Test

只有属性查找,几乎没有写入

如果属性查找是您主要关心的问题,以下是一些数字。

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 减慢 <-1.29%
  • obj[target] <-最快
地图
  • map.has(target) 减慢 <-2.94%
2021年1月 Chrome 87的结果

enter image description here

其他浏览器的结果是最受欢迎的,请更新这个答案。
你可以使用 这个电子表格来做一个很好的截图

扎戈德的回答。分叉的 JSBench 测试