ประสิทธิภาพของ Objects / Arrays ใน JavaScript เป็นอย่างไร? (เฉพาะสำหรับ Google V8)


105

ประสิทธิภาพที่เกี่ยวข้องกับ Arrays และ Objects ใน JavaScript (โดยเฉพาะ Google V8) น่าสนใจมากสำหรับเอกสาร ฉันไม่พบบทความที่ครอบคลุมเกี่ยวกับหัวข้อนี้ที่ใดก็ได้บนอินเทอร์เน็ต

ฉันเข้าใจว่าออบเจ็กต์บางตัวใช้คลาสเป็นโครงสร้างข้อมูลพื้นฐาน หากมีคุณสมบัติจำนวนมากบางครั้งถือว่าเป็นตารางแฮช?

ฉันเข้าใจด้วยว่าบางครั้ง Arrays จะถือว่าเหมือนกับอาร์เรย์ C ++ (เช่นการจัดทำดัชนีแบบสุ่มอย่างรวดเร็วการลบและการปรับขนาดอย่างช้าๆ) และในบางครั้งพวกมันจะได้รับการปฏิบัติเหมือน Objects มากขึ้น (การจัดทำดัชนีที่รวดเร็วการแทรก / ลบอย่างรวดเร็วหน่วยความจำที่มากขึ้น) และบางครั้งอาจถูกจัดเก็บเป็นรายการที่เชื่อมโยง (เช่นการจัดทำดัชนีแบบสุ่มช้าการลบ / การแทรกอย่างรวดเร็วที่จุดเริ่มต้น / จุดสิ้นสุด)

ประสิทธิภาพที่แม่นยำของการดึงข้อมูล Array / Object และการปรับแต่งใน JavaScript คืออะไร? (เฉพาะสำหรับ Google V8)

โดยเฉพาะอย่างยิ่งผลกระทบด้านประสิทธิภาพของ:

  • การเพิ่มคุณสมบัติให้กับวัตถุ
  • การลบคุณสมบัติออกจากวัตถุ
  • การสร้างดัชนีคุณสมบัติในวัตถุ
  • การเพิ่มรายการใน Array
  • การลบรายการออกจาก Array
  • การสร้างดัชนีรายการใน Array
  • กำลังโทรหา Array.pop ()
  • เรียก Array.push ()
  • กำลังโทรหา Array.shift ()
  • การเรียก Array.unshift ()
  • เรียก Array.slice ()

บทความหรือลิงค์สำหรับรายละเอียดเพิ่มเติมจะได้รับการชื่นชมเช่นกัน :)

แก้ไข:ฉันสงสัยจริงๆว่าอาร์เรย์และวัตถุ JavaScript ทำงานอย่างไรภายใต้ประทุน นอกจากนี้เครื่องยนต์ V8 "รู้" ในบริบทใดที่ "เปลี่ยนไปใช้" ไปยังโครงสร้างข้อมูลอื่น

ตัวอย่างเช่นสมมติว่าฉันสร้างอาร์เรย์ด้วย ...

var arr = [];
arr[10000000] = 20;
arr.push(21);

เกิดอะไรขึ้นที่นี่?

หรือ ... อะไรประมาณนี้ ... ???

var arr = [];
//Add lots of items
for(var i = 0; i < 1000000; i++)
    arr[i] = Math.random();
//Now I use it like a queue...
for(var i = 0; i < arr.length; i++)
{
    var item = arr[i].shift();
    //Do something with item...
}

สำหรับอาร์เรย์ทั่วไปประสิทธิภาพจะแย่มาก ในขณะที่หากมีการใช้ LinkedList ... ก็ไม่เลวร้ายนัก


2
ไปที่jsperf.comและสร้างกรณีทดสอบ
Rob W

2
@RobW มีอะไรมากกว่าการทดสอบแบบง่ายๆที่สามารถอธิบายได้ซึ่งต้องใช้ความรู้เกี่ยวกับวิธีการทำงานของคอมไพเลอร์ JIT และสิ่งที่กำลังทำกับข้อมูล ถ้าฉันหาเวลาได้ฉันจะเพิ่มคำตอบ แต่หวังว่าคนอื่นจะมีเวลาเข้าสู่ประเด็นสำคัญ นอกจากนี้ฉันต้องการเพียงแค่ทิ้งลิงก์นี้ไว้ที่นี่
ไม่ระบุตัวตน

สิ่งที่ฉันกำลังพูดถึง JIT คือสิ่งต่างๆเช่น "รูปร่าง" ของวัตถุหรืออาร์เรย์ที่มีค่าที่ไม่ได้กำหนดระหว่างองค์ประกอบที่กำหนดไว้เช่นเดียวกับการทดลองเมื่อเร็ว ๆ นี้กับคุณลักษณะเฉพาะประเภท ... วิธีการเฉพาะอาร์เรย์อาจขึ้นอยู่กับการใช้งานเป็น เช่นเดียวกับว่าต้นแบบได้รับการปรับแต่งหรือไม่ ไม่มีสิ่งที่เรียกว่า "รู้ที่จะ" เปลี่ยนไปใช้ AFAIK ประเภทข้อมูลอื่น
ไม่ระบุตัวตน

1
มีตัวแทนของ Google พูดคุยเกี่ยวกับวิธีการทำงานของ Optimizer และระบบภายในต่างๆ และวิธีเพิ่มประสิทธิภาพสำหรับพวกเขา (สำหรับเกม!) youtube.com/watch?v=XAqIpGU8ZZk
PicoCreator

คำตอบ:


280

ฉันสร้างชุดทดสอบอย่างแม่นยำเพื่อสำรวจปัญหาเหล่านี้ (และอื่น ๆ ) ( สำเนาที่เก็บถาวร )

และด้วยเหตุนี้คุณจะเห็นปัญหาด้านประสิทธิภาพในเครื่องทดสอบกรณีทดสอบมากกว่า 50+ (จะใช้เวลานาน)

ตามที่ชื่อแนะนำจะสำรวจการใช้งานโดยใช้ลักษณะรายการที่เชื่อมโยงแบบเนทีฟของโครงสร้าง DOM

(ปัจจุบันลงสร้างขึ้นมาใหม่ในความคืบหน้า) รายละเอียดเพิ่มเติมในบล็อกของฉันเกี่ยวกับเรื่องนี้

สรุปเป็นดังนี้

  • V8 Array เร็วทันใจมาก
  • Array push / pop / shift เร็วกว่าวัตถุใด ๆ ที่เทียบเท่าประมาณ 20x +
  • น่าแปลกใจที่Array.shift()เร็วกว่าอาร์เรย์ป๊อปประมาณ 6 เท่า แต่เร็วกว่าการลบแอตทริบิวต์ออบเจ็กต์ประมาณ 100 เท่า
  • น่าขบขันArray.push( data );เร็วกว่าArray[nextIndex] = dataเกือบ 20 (อาร์เรย์แบบไดนามิก) ถึง 10 (อาร์เรย์คงที่) มากกว่า
  • Array.unshift(data) ช้ากว่าที่คาดไว้และช้ากว่าคุณสมบัติใหม่ที่เพิ่มเข้ามาประมาณ 5 เท่า
  • การarray[index] = nullลบค่าจะเร็วกว่าการลบdelete array[index](ไม่ได้กำหนด) ในอาร์เรย์โดยเร็วกว่าประมาณ 4x ++
  • น่าแปลกที่การลบค่าในวัตถุobj[attr] = nullช้ากว่าการลบแอตทริบิวต์ประมาณ 2 เท่าdelete obj[attr]
  • ไม่น่าแปลกใจเลยที่ mid array Array.splice(index,0,data)ช้าช้ามาก
  • น่าแปลกใจที่Array.splice(index,1,data)ได้รับการปรับให้เหมาะสมที่สุด (ไม่มีการเปลี่ยนแปลงความยาว) และเร็วกว่าการประกบกัน 100 เท่าArray.splice(index,0,data)
  • ไม่น่าแปลกใจที่ divLinkedList นั้นด้อยกว่าอาร์เรย์ในทุกเซกเตอร์ยกเว้นdll.splice(index,1)การลบ (โดยที่ระบบทดสอบขัดข้อง)
  • ความประหลาดใจที่ยิ่งใหญ่ที่สุดของมันทั้งหมด [ตามที่ jjrv ชี้ให้เห็น] การเขียนอาร์เรย์ V8 นั้นเร็วกว่า V8 เล็กน้อยที่อ่านได้ = O

หมายเหตุ:เมตริกเหล่านี้ใช้กับอาร์เรย์ / ออบเจ็กต์ขนาดใหญ่เท่านั้นซึ่ง v8 ไม่ได้ "ปรับให้เหมาะสมทั้งหมด" อาจมีกรณีประสิทธิภาพที่ได้รับการปรับให้เหมาะสมที่แยกได้มากสำหรับขนาดอาร์เรย์ / วัตถุน้อยกว่าขนาดตามอำเภอใจ (24?) สามารถดูรายละเอียดเพิ่มเติมได้อย่างกว้างขวางในวิดีโอ Google IO หลายรายการ

หมายเหตุ 2:ผลการดำเนินงานที่ยอดเยี่ยมเหล่านี้จะไม่ถูกแชร์ระหว่างเบราว์เซอร์โดยเฉพาะ *cough*IE การทดสอบมีขนาดใหญ่มากด้วยเหตุนี้ฉันจึงยังวิเคราะห์และประเมินผลลัพธ์ไม่ครบถ้วน: โปรดแก้ไขใน =)

อัปเดตหมายเหตุ (ธ.ค. 2555):ตัวแทนของ Google มีวิดีโอบน YouTube ที่อธิบายการทำงานภายในของ Chrome เอง (เช่นเมื่อเปลี่ยนจากอาร์เรย์ที่เชื่อมโยงไปเป็นอาร์เรย์คงที่เป็นต้น) และวิธีการเพิ่มประสิทธิภาพ ดูGDC 2012: จากคอนโซลถึง Chromeสำหรับข้อมูลเพิ่มเติม


2
ผลลัพธ์บางอย่างดูแปลกมาก ตัวอย่างเช่นในการเขียนอาร์เรย์ของ Chrome จะเร็วกว่าการอ่านประมาณ 10 เท่าในขณะที่ใน Firefox จะตรงกันข้าม คุณแน่ใจหรือไม่ว่าเบราว์เซอร์ JIT ไม่ได้เพิ่มประสิทธิภาพการทดสอบทั้งหมดของคุณในบางกรณี
jjrv

1
@jjrv good gosh = O คุณพูดถูก ... ฉันยังอัปเดตแต่ละกรณีการเขียนให้มีความพิเศษเพิ่มขึ้นเพื่อป้องกัน JIT ... และโดยสุจริตเว้นแต่การเพิ่มประสิทธิภาพ JIT นั้นดี (ซึ่งฉันคิดว่ามันยากที่จะเชื่อ) อาจเป็นเพียงกรณีของการอ่านที่ปรับให้เหมาะสมไม่ดีหรือการเขียนที่ปรับให้เหมาะสมอย่างมาก (เขียนลงในบัฟเฟอร์ทันที?) ... ซึ่งควรค่าแก่การตรวจสอบ: lol
PicoCreator

2
แค่อยากจะเพิ่มประเด็นที่แน่นอนในการสนทนาวิดีโอในอาร์เรย์: youtube.com/…
badunk

1
ไซต์ JsPerf ไม่มีอีกต่อไป :(
JustGoscha

1
@JustGoscha ตกลงขอบคุณสำหรับข้อมูล: ฉันแก้ไขมันสำรองโดยการสร้างใหม่จาก Google แคช
PicoCreator

5

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

ในแง่ของการระบุอาร์เรย์จากออบเจ็กต์ (พจนานุกรม) เอ็นจิน JS มักจะสร้างเส้นที่ชัดเจนระหว่างทั้งสอง นั่นเป็นเหตุผลว่าทำไมจึงมีบทความมากมายเกี่ยวกับวิธีการพยายามสร้างวัตถุคล้าย Array กึ่งปลอมที่มีพฤติกรรมเหมือนหนึ่ง แต่อนุญาตให้ใช้ฟังก์ชันอื่นได้ เหตุผลที่การแยกนี้ยังคงมีอยู่เนื่องจากเอ็นจิน JS เก็บทั้งสองอย่างแตกต่างกัน

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

เมื่อใดก็ตามที่คุณใช้วัตถุอาร์เรย์ที่ถูกต้องและใช้วิธีมาตรฐานอย่างใดอย่างหนึ่งในการจัดการอาร์เรย์นั้นคุณจะต้องกดปุ่มข้อมูลอาร์เรย์พื้นฐาน โดยเฉพาะอย่างยิ่งใน V8 สิ่งเหล่านี้จะเหมือนกับอาร์เรย์ C ++ เป็นหลักดังนั้นจึงจะนำกฎเหล่านั้นไปใช้ หากด้วยเหตุผลบางอย่างคุณกำลังทำงานกับอาร์เรย์ที่เอ็นจิ้นไม่สามารถระบุได้ด้วยความมั่นใจว่าอาร์เรย์แสดงว่าคุณอยู่ในพื้นที่ที่สั่นคลอนมากขึ้น ด้วย V8 เวอร์ชันล่าสุดมีพื้นที่ในการทำงานมากขึ้น ตัวอย่างเช่นเป็นไปได้ที่จะสร้างคลาสที่มี Array.prototype เป็นต้นแบบและยังสามารถเข้าถึงวิธีการจัดการอาร์เรย์เนทีฟต่างๆได้อย่างมีประสิทธิภาพ แต่นี่เป็นการเปลี่ยนแปลงล่าสุด

ลิงก์เฉพาะไปยังการเปลี่ยนแปลงล่าสุดในการจัดการอาร์เรย์อาจมีประโยชน์ที่นี่:

เพิ่มเติมเล็กน้อยนี่คือ Array Pop และ Array Push โดยตรงจากแหล่งที่มาของ V8 ซึ่งทั้งสองใช้งานใน JS เอง:

function ArrayPop() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.pop"]);
  }

  var n = TO_UINT32(this.length);
  if (n == 0) {
    this.length = n;
    return;
  }
  n--;
  var value = this[n];
  this.length = n;
  delete this[n];
  return value;
}


function ArrayPush() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.push"]);
  }

  var n = TO_UINT32(this.length);
  var m = %_ArgumentsLength();
  for (var i = 0; i < m; i++) {
    this[i+n] = %_Arguments(i);
  }
  this.length = n + m;
  return this.length;
}

1

ฉันต้องการเสริมคำตอบที่มีอยู่ด้วยการตรวจสอบคำถามที่ว่าการใช้งานมีพฤติกรรมอย่างไรเกี่ยวกับอาร์เรย์ที่เพิ่มขึ้น: หากพวกเขานำไปใช้ในลักษณะ "ปกติ" เราจะเห็นการผลักดันอย่างรวดเร็วจำนวนมากโดยมีการผลักแบบช้าสลับที่หายากซึ่งเป็นจุดที่สำเนาการนำไปใช้งาน การเป็นตัวแทนภายในของอาร์เรย์จากบัฟเฟอร์หนึ่งไปยังหนึ่งที่ใหญ่กว่า

คุณสามารถเห็นเอฟเฟกต์นี้ได้เป็นอย่างดีซึ่งมาจาก Chrome:

16: 4ms
40: 8ms 2.5
76: 20ms 1.9
130: 31ms 1.7105263157894737
211: 14ms 1.623076923076923
332: 55ms 1.5734597156398105
514: 44ms 1.5481927710843373
787: 61ms 1.5311284046692606
1196: 138ms 1.5196950444726811
1810: 139ms 1.5133779264214047
2731: 299ms 1.5088397790055248
4112: 341ms 1.5056755767118273
6184: 681ms 1.5038910505836576
9292: 1324ms 1.5025873221216042

แม้ว่าแต่ละพุชจะมีโปรไฟล์ แต่เอาต์พุตจะมีเฉพาะที่ใช้เวลาเกินเกณฑ์ที่กำหนดเท่านั้น สำหรับการทดสอบแต่ละครั้งฉันได้ปรับแต่งเกณฑ์เพื่อยกเว้นการพุชทั้งหมดที่ดูเหมือนจะแสดงถึงการพุชแบบเร็ว

ดังนั้นตัวเลขแรกแสดงถึงองค์ประกอบที่ถูกแทรก (บรรทัดแรกใช้สำหรับองค์ประกอบที่ 17) อันดับที่สองคือระยะเวลาที่ใช้ (สำหรับอาร์เรย์จำนวนมากการเปรียบเทียบจะทำแบบขนาน) และค่าสุดท้ายคือการหารของ หมายเลขแรกตามจำนวนในบรรทัดเดิม

ทุกบรรทัดที่มีเวลาดำเนินการน้อยกว่า 2ms จะไม่รวมอยู่ใน Chrome

คุณจะเห็นได้ว่า Chrome เพิ่มขนาดอาร์เรย์เป็น 1.5 บวกกับออฟเซ็ตบางส่วนเพื่อรองรับอาร์เรย์ขนาดเล็ก

สำหรับ Firefox มันมีสองอย่าง:

126: 284ms
254: 65ms 2.015873015873016
510: 28ms 2.0078740157480315
1022: 58ms 2.003921568627451
2046: 89ms 2.0019569471624266
4094: 191ms 2.0009775171065494
8190: 364ms 2.0004885197850513

ฉันต้องวางเกณฑ์ใน Firefox ไว้เล็กน้อยนั่นคือเหตุผลที่เราเริ่มต้นที่ # 126

ด้วย IE เราได้รับการผสมผสาน:

256: 11ms 256
512: 26ms 2
1024: 77ms 2
1708: 113ms 1.66796875
2848: 154ms 1.6674473067915691
4748: 423ms 1.6671348314606742
7916: 944ms 1.6672283066554338

มันเป็นพลังของสองในตอนแรกจากนั้นมันจะเคลื่อนไปสู่พลังห้าในสาม

ดังนั้นการใช้งานทั่วไปทั้งหมดจึงใช้วิธี "ปกติ" สำหรับอาร์เรย์ (เช่นแทนที่จะคลั่งไคล้เชือก )

นี่คือรหัสมาตรฐานและนี่คือซอที่อยู่ใน

var arrayCount = 10000;

var dynamicArrays = [];

for(var j=0;j<arrayCount;j++)
    dynamicArrays[j] = [];

var lastLongI = 1;

for(var i=0;i<10000;i++)
{
    var before = Date.now();
    for(var j=0;j<arrayCount;j++)
        dynamicArrays[j][i] = i;
    var span = Date.now() - before;
    if (span > 10)
    {
      console.log(i + ": " + span + "ms" + " " + (i / lastLongI));
      lastLongI = i;
    }
}

0

ในขณะที่ทำงานภายใต้ node.js 0.10 (สร้างบน v8) ฉันเห็นการใช้งาน CPU ที่ดูเหมือนจะมากเกินไปสำหรับปริมาณงาน ฉันตรวจสอบปัญหาประสิทธิภาพการทำงานหนึ่งกับฟังก์ชันที่กำลังตรวจสอบการมีอยู่ของสตริงในอาร์เรย์ ดังนั้นฉันจึงทำการทดสอบบางอย่าง

  • โหลด 90,822 โฮสต์
  • การโหลด config ใช้เวลา 0.087 วินาที (อาร์เรย์)
  • การโหลด config ใช้เวลา 0.152 วินาที (object)

การโหลด 91k รายการลงในอาร์เรย์ (ด้วยการตรวจสอบความถูกต้อง & พุช) นั้นเร็วกว่าการตั้งค่า obj [key] = value

ในการทดสอบครั้งต่อไปฉันค้นหาชื่อโฮสต์ทั้งหมดในรายการหนึ่งครั้ง (การทำซ้ำ 91k เพื่อเฉลี่ยเวลาในการค้นหา):

  • การค้นหา config ใช้เวลา 87.56 วินาที (อาร์เรย์)
  • การค้นหา config ใช้เวลา 0.21 วินาที (object)

แอปพลิเคชันในที่นี้คือ Haraka (เซิร์ฟเวอร์ SMTP) และจะโหลด host_list หนึ่งครั้งเมื่อเริ่มต้น (และหลังจากการเปลี่ยนแปลง) และทำการค้นหานี้หลายล้านครั้งในระหว่างการดำเนินการ การเปลี่ยนไปใช้วัตถุถือเป็นการชนะอย่างมาก

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