Javascript Set เทียบกับประสิทธิภาพของ Array


90

อาจเป็นเพราะชุดค่อนข้างใหม่สำหรับ Javascript แต่ฉันไม่สามารถค้นหาบทความใน StackO หรือที่อื่นใดที่พูดถึงความแตกต่างของประสิทธิภาพระหว่างทั้งสองใน Javascript ดังนั้นความแตกต่างในแง่ของประสิทธิภาพระหว่างทั้งสองคืออะไร? โดยเฉพาะเมื่อต้องลบเพิ่มและทำซ้ำ


1
คุณไม่สามารถใช้แทนกันได้ ดังนั้นจึงไม่ค่อยมีเหตุผลที่จะเปรียบเทียบพวกเขา
zerkms

คุณกำลังพูดถึงการเปรียบเทียบระหว่างSetและ[]หรือ{}?
สร้าง

2
การเพิ่มและทำซ้ำไม่ได้สร้างความแตกต่างมากนักการลบและที่สำคัญที่สุดคือการค้นหาจะสร้างความแตกต่าง
Bergi


3
@ zerkms - อย่างเคร่งครัด Array ไม่ได้รับคำสั่งเช่นกัน แต่การใช้ดัชนีช่วยให้สามารถปฏิบัติได้ราวกับว่าเป็น ;-) ลำดับของค่าในชุดจะถูกเก็บไว้ในลำดับการแทรก
RobG

คำตอบ:


102

โอเคฉันได้ทดสอบการเพิ่มการทำซ้ำและการลบองค์ประกอบจากทั้งอาร์เรย์และชุด ฉันทำการทดสอบแบบ "เล็ก" โดยใช้องค์ประกอบ 10,000 รายการและการทดสอบ "ใหญ่" โดยใช้องค์ประกอบ 100000 รายการ นี่คือผลลัพธ์

การเพิ่มองค์ประกอบในคอลเลกชัน

ดูเหมือนว่า.pushเมธอดอาร์เรย์จะเร็วกว่า.addเมธอด set ประมาณ 4 เท่าไม่ว่าจะเพิ่มจำนวนองค์ประกอบก็ตาม

การทำซ้ำและแก้ไของค์ประกอบในคอลเล็กชัน

สำหรับการทดสอบส่วนนี้ฉันใช้การforวนซ้ำเพื่อวนซ้ำบนอาร์เรย์และfor ofวนซ้ำเพื่อวนซ้ำบนชุด อีกครั้งการทำซ้ำบนอาร์เรย์นั้นเร็วกว่า คราวนี้ดูเหมือนว่าจะเป็นแบบทวีคูณดังนั้นจึงใช้เวลานานกว่าสองเท่าในระหว่างการทดสอบ "เล็ก" และนานกว่าเกือบสี่เท่าในระหว่างการทดสอบ "ใหญ่"

การลบองค์ประกอบออกจากคอลเลกชัน

ตอนนี้เป็นที่ที่น่าสนใจ ฉันใช้การforวนซ้ำร่วมกันและ.spliceเพื่อลบองค์ประกอบบางอย่างออกจากอาร์เรย์และฉันใช้for ofและ.deleteเพื่อลบองค์ประกอบบางอย่างออกจากชุด สำหรับการทดสอบ "ขนาดเล็ก" การลบรายการออกจากชุดทำได้เร็วขึ้นประมาณสามเท่า (2.6 ms เทียบกับ 7.1 มิลลิวินาที) แต่สิ่งต่าง ๆ เปลี่ยนไปอย่างมากสำหรับการทดสอบ "ใหญ่" ซึ่งใช้เวลา 1955.1 มิลลิวินาทีในการลบรายการออกจากอาร์เรย์ในขณะที่มีเพียง ใช้เวลา 83.6 มิลลิวินาทีในการลบออกจากชุดเร็วขึ้น 23 เท่า

ข้อสรุป

ที่องค์ประกอบ 10k การทดสอบทั้งสองใช้เวลาเทียบเคียงกัน (อาร์เรย์: 16.6 มิลลิวินาที, ชุด: 20.7 มิลลิวินาที) แต่เมื่อจัดการกับองค์ประกอบ 100k ชุดนั้นเป็นผู้ชนะที่ชัดเจน (อาร์เรย์: 1974.8 ms, ชุด: 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,1]สำหรับอาร์เรย์จะมีความยาว 6 ชุดจะมีขนาด 1 ดูเหมือนว่าโค้ดของคุณสามารถสร้างชุดของขนาดที่แตกต่างกันอย่างมากในแต่ละครั้งที่มีขนาดมากกว่า 100,000 รายการในการรันแต่ละครั้งเนื่องจากลักษณะของชุดนี้ คุณอาจไม่เคยสังเกตเห็นเพราะคุณไม่ได้แสดงขนาดของชุดจนกว่าจะเรียกใช้สคริปต์ทั้งหมด
KyleFarris

6
@KyleFarris เว้นแต่ฉันจะเข้าใจผิดสิ่งนี้จะเป็นจริงหากมีรายการที่ซ้ำกันในชุดเช่นในตัวอย่างของคุณ[1, 1, 1, 1, 1]แต่เนื่องจากแต่ละรายการในชุดนั้นเป็นวัตถุที่มีคุณสมบัติต่าง ๆ รวมถึงชื่อและนามสกุลที่สร้างแบบสุ่มจากรายการ จากชื่อที่เป็นไปได้หลายร้อยชื่ออายุที่สร้างแบบสุ่มเพศที่สร้างขึ้นแบบสุ่มและคุณลักษณะที่สร้างขึ้นแบบสุ่มอื่น ๆ ... โอกาสที่จะมีวัตถุสองชิ้นที่เหมือนกันในชุดนั้นมีน้อยมาก
snowfrogdev

3
อันที่จริงคุณพูดถูกในกรณีนี้เพราะดูเหมือนว่าชุดจะไม่ได้แยกความแตกต่างจากวัตถุในชุด ดังนั้นคุณสามารถมีวัตถุที่แน่นอนเหมือนกัน{foo: 'bar'}10,000x ในชุดและมันจะมีขนาด 10,000 เช่นเดียวกันสำหรับอาร์เรย์ ดูเหมือนว่าจะไม่ซ้ำกับค่าสเกลาร์เท่านั้น (สตริงตัวเลขบูลีน ฯลฯ .. )
KyleFarris

13
คุณสามารถมีเนื้อหา เดียวกันของออบเจ็กต์ได้{foo: 'bar'}หลายครั้งใน Set แต่ไม่ใช่วัตถุเดียวกัน (การอ้างอิง) คุ้มค่าที่จะชี้ให้เห็นความแตกต่างที่ลึกซึ้ง IMO
SimpleVar

16
คุณลืมวัดเหตุผลที่สำคัญที่สุดในการใช้ชุดการค้นหา 0 (1) hasเทียบกับIndexOf.
Magnus

67

การตรวจสอบ :

  • การดำเนินการตั้งค่าสามารถเข้าใจได้ว่าเป็นสแน็ปช็อตภายในสตรีมการดำเนินการ
  • เราไม่ได้อยู่ก่อนการทดแทนที่ชัดเจน
  • องค์ประกอบของคลาส Setไม่มีดัชนีที่สามารถเข้าถึงได้
  • Set classเป็นส่วนเสริมคลาส Arrayซึ่งมีประโยชน์ในสถานการณ์เหล่านั้นที่เราต้องจัดเก็บคอลเลคชันที่จะใช้การเพิ่มขั้นพื้นฐานการลบการตรวจสอบและการทำซ้ำ

ฉันแบ่งปันการทดสอบประสิทธิภาพบางอย่าง ลองเปิดคอนโซลของคุณและคัดลอกโค้ดด้านล่างนี้

การสร้างอาร์เรย์ (125000)

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

1. การค้นหาดัชนี

เราเปรียบเทียบวิธี has ของ Set กับ Array 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 ตามลำดับ:

อาร์เรย์ / พุช (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' );

อ่านบทความฉบับเต็มได้ที่นี่


4
Array.indexOf ควรเป็น Array.inc รวมไว้เพื่อให้เทียบเท่ากัน ฉันได้รับหมายเลขที่แตกต่างกันมากใน Firefox
kagronick

2
ฉันสนใจ Object.inc รวมกับ Set.has การเปรียบเทียบ ...
Leopold Kristjansson

2
@LeopoldKristjansson ฉันไม่ได้เขียนการทดสอบเปรียบเทียบ แต่เราทำการกำหนดเวลาในไซต์การผลิตที่มีอาร์เรย์ที่มีรายการ 24k และเปลี่ยนจาก Array รวมถึง Set. มีการเพิ่มประสิทธิภาพอย่างมาก!
sedot

4

ข้อสังเกตของฉันคือ Set จะดีกว่าเสมอโดยมีข้อผิดพลาดสองประการสำหรับอาร์เรย์ขนาดใหญ่ในใจ:

ก) การสร้างชุดจากอาร์เรย์จะต้องทำใน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) การวนซ้ำสามารถทำได้ในลักษณะเดียวกันเพราะเร็วกว่าการfor ofวนซ้ำ ...

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

สำหรับการเปรียบเทียบชีวิตจริงเพื่อ difference(), intersection(), union()และuniq()(+ สหายของพวกเขา iteratee ฯลฯ ) ด้วย 40.000 องค์ประกอบ


3

ภาพหน้าจอของการทำซ้ำที่เปรียบเทียบสำหรับส่วนการทำซ้ำของคำถามของคุณฉันเพิ่งทำการทดสอบนี้และพบว่า Set มีประสิทธิภาพดีกว่าอาร์เรย์ 10,000 รายการ (ประมาณ 10 เท่าที่การดำเนินการอาจเกิดขึ้นในกรอบเวลาเดียวกัน) และขึ้นอยู่กับเบราว์เซอร์ที่เอาชนะหรือแพ้ Object.hasOwnProperty ในการทดสอบ like

ทั้ง Set และ Object มีวิธีการ "มี" ที่ทำงานในสิ่งที่ดูเหมือนจะถูกตัดจำหน่ายเป็น O (1) แต่การดำเนินการเดียวอาจใช้เวลานานขึ้นหรือเร็วขึ้นทั้งนี้ขึ้นอยู่กับการใช้งานเบราว์เซอร์ ดูเหมือนว่าเบราว์เซอร์ส่วนใหญ่จะใช้คีย์ใน Object เร็วกว่า Set.has () แม้แต่ Object.hasOwnProperty ซึ่งรวมถึงการตรวจสอบคีย์เพิ่มเติมก็เร็วกว่า Set.has () ประมาณ 5% อย่างน้อยสำหรับฉันใน Chrome v86

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

Update: 11/11/2020: https://jsbench.me/irkhdxnoqa/2

ในกรณีที่คุณต้องการทำการทดสอบของคุณเองด้วยเบราว์เซอร์ / สภาพแวดล้อมที่แตกต่างกัน


ในทำนองเดียวกันฉันจะเพิ่มเกณฑ์มาตรฐานสำหรับการเพิ่มรายการในอาร์เรย์เทียบกับชุดและการลบ


4
โปรดอย่าใช้ลิงก์ในคำตอบของคุณ (เว้นแต่จะเชื่อมโยงกับไลบรารีอย่างเป็นทางการ) เนื่องจากลิงก์เหล่านี้อาจใช้งานไม่ได้ - ดังที่เกิดขึ้นในกรณีของคุณ ลิงก์ของคุณคือ 404
Gil Epshtain

ฉันใช้ลิงค์ แต่ยังคัดลอกผลลัพธ์เมื่อพร้อมใช้งาน โชคไม่ดีที่พวกเขาเปลี่ยนกลยุทธ์การเชื่อมโยงอย่างรวดเร็ว
Zargold

อัปเดตโพสต์ทันทีด้วยภาพหน้าจอและเว็บไซต์ประสิทธิภาพ JS ใหม่: jsbench.me
Zargold

0

เพียงแค่การค้นหาคุณสมบัติเขียนเพียงเล็กน้อยหรือเป็นศูนย์

หากการค้นหาคุณสมบัติเป็นข้อกังวลหลักของคุณนี่คือตัวเลขบางส่วน

การทดสอบ JSBench https://jsbench.me/3pkjlwzhbr/1

อาร์เรย์
  • for วน
  • for วนซ้ำ (ย้อนกลับ)
  • array.includes(target)
ชุด
  • set.has(target)
วัตถุ
  • obj.hasOwnProperty(target)
  • target in obj <- ช้าลง 1.29%
  • obj[target] <- เร็วที่สุด
แผนที่
  • map.has(target) ช้ากว่า <- 2.94%
ผลลัพธ์ตั้งแต่เดือนมกราคม 2021 Chrome 87

ป้อนคำอธิบายภาพที่นี่

ยินดีต้อนรับผลลัพธ์จากเบราว์เซอร์อื่น ๆ โปรดอัปเดตคำตอบนี้
คุณสามารถใช้สเปรดชีตนี้เพื่อสร้างภาพหน้าจอที่สวยงาม

การทดสอบ JSBench แยกจากคำตอบของ 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")

การดำเนินการทั้งสามรายการในรายการ 10,000 รายการทำให้ฉัน:

set: 7.787ms
array: 2.388ms

@Bergi นั่นคือสิ่งที่ฉันคิดในตอนแรกเช่นกัน แต่มันก็เป็นเช่นนั้น
zerkms

1
@zerkms: กำหนด "งาน" :-) ใช่อาร์เรย์จะว่างเปล่าหลังจากวันที่forEachแต่อาจไม่เป็นไปตามที่คุณคาดไว้ หากต้องการพฤติกรรมที่เปรียบได้ก็ควรจะs.forEach(function(e) { s.clear(); })เป็นเช่นกัน
Bergi

1
มันทำอะไรบางอย่างไม่ใช่สิ่งที่ตั้งใจไว้: มันจะลบองค์ประกอบทั้งหมดระหว่างดัชนีiและจุดสิ้นสุด นั่นไม่ได้เปรียบเทียบกับสิ่งที่deleteทำในชุด
trincot

@Bergi โอ้ใช่มันลบทุกอย่างในการทำซ้ำเพียง 2 ครั้ง ความผิดฉันเอง.
zerkms

4
ในการทำซ้ำ 1 ครั้ง splice(0)ล้างอาร์เรย์
trincot
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.