เคล็ดลับประสิทธิภาพสำหรับ JavaScript ใน V8

Chris Wilson
Chris Wilson

บทนำ

Daniel Clifford กล่าวยอดเยี่ยมในงาน Google I/O เกี่ยวกับกลเม็ดเคล็ดลับในการปรับปรุงประสิทธิภาพ JavaScript ใน V8 ภานุพงศ์กระตุ้นให้เรา "สั่งสินค้าเร็วขึ้น" - เพื่อวิเคราะห์ความแตกต่างด้านประสิทธิภาพระหว่าง C++ และ JavaScript อย่างรอบคอบ และเขียนโค้ดอย่างรอบคอบเกี่ยวกับวิธีการทำงานของ JavaScript สรุปประเด็นสำคัญจากการพูดคุยของ Daniel จะบันทึกไว้ในบทความนี้ และเราจะคอยอัปเดตบทความนี้เมื่อมีการเปลี่ยนแปลงคำแนะนำด้านประสิทธิภาพ

คำแนะนำที่สำคัญที่สุด

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

คำแนะนำเบื้องต้นที่ดีที่สุดเพื่อให้ได้ประสิทธิภาพที่ดีในเว็บแอปพลิเคชันคือ

  • เตรียมตัวให้พร้อมก่อนที่จะมีปัญหา (หรือแจ้งให้ทราบ)
  • จากนั้น ระบุและทำความเข้าใจปมปัญหา
  • สุดท้าย ให้แก้ปัญหาที่สำคัญ

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

ต่อไปคือเคล็ดลับของ V8

ชั้นเรียนที่ซ่อนอยู่

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

เช่น

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```

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

ดังนั้น

  • เริ่มต้นสมาชิกออบเจ็กต์ทั้งหมดในฟังก์ชันตัวสร้าง (เพื่อให้อินสแตนซ์ไม่เปลี่ยนประเภทในภายหลัง)
  • เริ่มต้นสมาชิกออบเจ็กต์ในลำดับเดียวกันเสมอ

Numbers

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

เช่น

var i = 42;  // this is a 31-bit signed integer
var j = 4.2;  // this is a double-precision floating point number```

ดังนั้น

  • เลือกใช้ค่าตัวเลขที่สามารถแสดงเป็นจำนวนเต็มที่มีเครื่องหมาย 31 บิตได้

อาร์เรย์

พื้นที่เก็บข้อมูลอาร์เรย์ภายในมี 2 ประเภทเพื่อจัดการอาร์เรย์ขนาดใหญ่และแบบกะทัดรัด

  • องค์ประกอบด่วน: การจัดเก็บเชิงเส้นสำหรับชุดคีย์กะทัดรัด
  • องค์ประกอบพจนานุกรม: พื้นที่เก็บข้อมูลตารางแฮช

ทางที่ดีไม่ควรทำให้พื้นที่เก็บข้อมูลอาร์เรย์สลับจากประเภทหนึ่งไปเป็นอีกประเภทหนึ่ง

ดังนั้น

  • ใช้คีย์ต่อเนื่องโดยเริ่มต้นที่ 0 สำหรับอาร์เรย์
  • อย่าจัดสรรอาร์เรย์ขนาดใหญ่ไว้ล่วงหน้า (เช่น องค์ประกอบ > 64K) ให้มีขนาดตามขนาดสูงสุด แต่ให้ขยายตามที่มีอยู่
  • ไม่ต้องลบองค์ประกอบในอาร์เรย์ โดยเฉพาะอาร์เรย์ตัวเลข
  • ห้ามโหลดองค์ประกอบที่ไม่ได้เริ่มต้นหรือถูกลบ
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
}

นอกจากนี้ อาร์เรย์แบบคู่ยังรวดเร็วกว่า นั่นคือประเภทเอลิเมนต์แทร็กแบบคลาสที่ซ่อนอยู่ของอาร์เรย์ และอาร์เรย์ที่มีเฉพาะคู่

var a = new Array();
a[0] = 77;   // Allocates
a[1] = 88;
a[2] = 0.5;   // Allocates, converts
a[3] = true; // Allocates, converts```

จะมีประสิทธิภาพน้อยกว่า

var a = [77, 88, 0.5, true];

เนื่องจากในตัวอย่างแรก ระบบจะดำเนินการทีละงาน และการกำหนด a[2] จะทำให้มีการแปลงอาร์เรย์เป็นอาร์เรย์ของคู่แบบกล่องเดียว แต่การกำหนด a[3] ทำให้มีการแปลงกลับไปเป็นอาร์เรย์ที่สามารถมีค่าใดก็ได้ (ตัวเลขหรือออบเจ็กต์) ในกรณีที่ 2 คอมไพเลอร์จะทราบถึงประเภทขององค์ประกอบทั้งหมดในลิเทอรัล และคลาสที่ซ่อนอยู่สามารถระบุได้ตั้งแต่แรก

  • เริ่มต้นโดยใช้ลิเทอรัลอาร์เรย์สำหรับอาร์เรย์ที่มีขนาดคงที่ขนาดเล็ก
  • จัดสรรอาร์เรย์ขนาดเล็กไว้ล่วงหน้า (<64k) เพื่อแก้ไขขนาดก่อนใช้งาน
  • ไม่จัดเก็บค่าที่ไม่ใช่ตัวเลข (วัตถุ) ในอาร์เรย์ตัวเลข
  • โปรดระวังอย่าทำให้เกิดการแปลงอาร์เรย์ขนาดเล็กซ้ำ ถ้าคุณเริ่มต้นโดยไม่มีลิเทอรัล

การคอมไพล์ JavaScript

แม้ว่า JavaScript จะเป็นภาษาที่มีการเปลี่ยนแปลงได้อย่างมาก และการใช้งานแบบดั้งเดิมของ JavaScript เป็นล่าม แต่เครื่องมือรันไทม์ของ JavaScript สมัยใหม่ก็ใช้การคอมไพล์ V8 (JavaScript ของ Chrome) มีคอมไพเลอร์ Just-In-Time (JIT) ที่แตกต่างกัน 2 ตัว ซึ่งได้แก่

  • "ทั้งหมด" คอมไพเลอร์ ซึ่งสามารถสร้างโค้ดที่ดีสำหรับ JavaScript ใดก็ได้
  • คอมไพเลอร์ เพิ่มประสิทธิภาพ ซึ่งสร้างโค้ดที่ดีสำหรับ JavaScript ส่วนใหญ่ แต่ใช้เวลาคอมไพเลอร์นานกว่า

คอมไพเลอร์เต็มรูปแบบ

ใน V8 คอมไพเลอร์ Full จะทำงานร่วมกับโค้ดทั้งหมด และเริ่มเรียกใช้โค้ดโดยเร็วที่สุดเท่าที่จะเป็นไปได้ โดยสร้างโค้ดที่ดีแต่ไม่ดีเยี่ยมได้อย่างรวดเร็ว คอมไพเลอร์นี้แทบจะไม่คิดว่าประเภทของตัวแปรในเวลาคอมไพล์ ระบบคาดหวังว่าตัวแปรประเภทต่างๆ สามารถเปลี่ยนแปลงขณะรันไทม์ได้ โค้ดที่สร้างโดย Full Compiler จะใช้แคชแบบอินไลน์ (IC) เพื่อปรับแต่งความรู้เกี่ยวกับประเภทต่างๆ ขณะที่โปรแกรมทำงาน ซึ่งทำให้มีประสิทธิภาพเพิ่มขึ้นอย่างรวดเร็ว

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

ดังนั้น

  • แนะนำให้ใช้การดำเนินการแบบโมโนโมฟิกมากกว่าการดำเนินการแบบโพลีมอร์ฟิก

การดำเนินการจะเป็นแบบโมโนโมฟิกหากคลาสอินพุตที่ซ่อนอยู่ของอินพุตเหมือนกันเสมอ มิเช่นนั้นจะเป็นแบบโพลีมอร์ฟิก ซึ่งหมายความว่าอาร์กิวเมนต์บางส่วนอาจเปลี่ยนประเภทในการเรียกใช้การดำเนินการต่างๆ ได้ ตัวอย่างเช่น การเรียก add() ครั้งที่ 2 ในตัวอย่างนี้ก่อให้เกิดพฤติกรรมโพลีมอร์ฟิส

function add(x, y) {
  return x + y;
}

add(1, 2);      // + in add is monomorphic
add("a", "b");  // + in add becomes polymorphic```

คอมไพเลอร์การเพิ่มประสิทธิภาพ

การทำงานควบคู่ไปกับคอมไพเลอร์เต็มรูปแบบ V8 จึงจะทำการคอมไพล์ "hot" ซ้ำอีกครั้ง (กล่าวคือเป็นฟังก์ชันที่ทำงานหลายครั้ง) ด้วยคอมไพเลอร์ที่มีการเพิ่มประสิทธิภาพ คอมไพเลอร์นี้ใช้ประเภทการตอบสนองเพื่อทำให้โค้ดที่คอมไพล์เร็วขึ้น ซึ่งอันที่จริงแล้วจะใช้ประเภทที่ได้จาก IC ที่เราพูดถึงไปเมื่อสักครู่

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

คุณสามารถบันทึกสิ่งที่ได้รับการเพิ่มประสิทธิภาพโดยใช้ "d8" แบบสแตนด์อโลน ของเครื่อง V8:

d8 --trace-opt primes.js

(ซึ่งจะบันทึกชื่อของฟังก์ชันที่เพิ่มประสิทธิภาพสำหรับ Stdout)

อย่างไรก็ตาม บางฟังก์ชันอาจเพิ่มประสิทธิภาพไม่ได้ ฟีเจอร์บางอย่างทำให้คอมไพเลอร์การเพิ่มประสิทธิภาพไม่ทำงานในฟังก์ชันที่กำหนด ("การประกันตัว") โดยเฉพาะอย่างยิ่ง คอมไพเลอร์ที่มีการเพิ่มประสิทธิภาพในปัจจุบันช่วยแก้ไขปัญหาเกี่ยวกับฟังก์ชันด้วย ให้ลอง {} ดู {} บล็อก!

ดังนั้น

  • ใส่โค้ดที่ไวต่อประสิทธิภาพลงในฟังก์ชันที่ซ้อนกันหากคุณลอง {} ดักจับ {} บล็อกแล้ว: ```js function perf_sensitive() { // ทำงานที่อ่อนไหวต่อประสิทธิภาพที่นี่ }

ลอง { perf_sensitive() } catch (e) { // จัดการข้อยกเว้นที่นี่ } ```

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

d8 --trace-opt primes.js

ไม่เพิ่มประสิทธิภาพ

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

ดังนั้น

  • หลีกเลี่ยงการเปลี่ยนแปลงคลาสที่ซ่อนอยู่ในฟังก์ชันหลังจากเพิ่มประสิทธิภาพแล้ว

เช่นเดียวกับการเพิ่มประสิทธิภาพอื่นๆ คุณจะได้รับบันทึกของฟังก์ชันที่ V8 ต้องยกเลิกการเพิ่มประสิทธิภาพด้วยแฟล็กการบันทึก ดังนี้

d8 --trace-deopt primes.js

เครื่องมือ V8 อื่นๆ

คุณยังส่งตัวเลือกการติดตาม V8 ไปยัง Chrome ได้เมื่อเริ่มต้นระบบได้ด้วย โดยทำดังนี้

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```

นอกจากการใช้การทำโปรไฟล์เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์แล้ว คุณยังสามารถใช้ d8 เพื่อทำโปรไฟล์ได้ด้วย โดยทำดังนี้

% out/ia32.release/d8 primes.js --prof

เครื่องมือนี้ใช้เครื่องมือสร้างโปรไฟล์การสุ่มตัวอย่างในตัว ซึ่งจะสุ่มตัวอย่างทุกมิลลิวินาทีและเขียน v8.log

ข้อมูลสรุป

คุณควรระบุและทำความเข้าใจวิธีที่เครื่องมือ V8 ทำงานกับโค้ดเพื่อเตรียมสร้าง JavaScript ที่มีประสิทธิภาพ ขอย้ำอีกครั้งว่าคำแนะนำเบื้องต้นคือ

  • เตรียมตัวให้พร้อมก่อนที่จะมีปัญหา (หรือแจ้งให้ทราบ)
  • จากนั้น ระบุและทำความเข้าใจปมปัญหา
  • สุดท้าย ให้แก้ปัญหาที่สำคัญ

ซึ่งหมายความว่าคุณควรตรวจสอบว่าปัญหาอยู่ใน JavaScript ของคุณโดยใช้เครื่องมืออื่นๆ เช่น PageSpeed ก่อน โดยอาจลดไปเป็น JavaScript บริสุทธิ์ (ไม่ใช่ DOM) ก่อนที่จะรวบรวมเมตริก แล้วใช้เมตริกเหล่านั้นเพื่อค้นหาจุดคอขวดและกำจัดจุดคอขวดที่สำคัญ หวังว่าการพูดคุยของ Daniel (และบทความนี้) จะช่วยให้คุณเข้าใจมากขึ้นว่า V8 เรียกใช้ JavaScript อย่างไร แต่โปรดมุ่งเน้นที่การเพิ่มประสิทธิภาพอัลกอริทึมของคุณเองด้วย

ข้อมูลอ้างอิง