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

Chris Wilson
Chris Wilson

เกริ่นนำ

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

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

คุณควรใส่คำแนะนำด้านประสิทธิภาพในบริบท การให้คำแนะนำเรื่องประสิทธิภาพเป็นสิ่งที่สื่อไปไม่ได้ และบางครั้งการมุ่งเน้นไปที่คำแนะนำที่เจาะลึกก่อนก็อาจทำให้เสียสมาธิจากปัญหาจริงได้ คุณต้องดูประสิทธิภาพของเว็บแอปพลิเคชันแบบองค์รวม ก่อนที่จะให้ความสำคัญกับเคล็ดลับด้านประสิทธิภาพเหล่านี้ คุณควรวิเคราะห์โค้ดของคุณด้วยเครื่องมือ เช่น 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] ทำให้ระบบแปลงค่าดังกล่าวเป็นอาร์เรย์ซึ่งมีค่าใดก็ได้ (ตัวเลขหรือวัตถุ) ในกรณีที่สอง คอมไพเลอร์จะรู้ประเภทขององค์ประกอบทั้งหมดในตัวอักษรและสามารถระบุคลาสที่ซ่อนได้ตั้งแต่แรก

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

คอมไพล์ JavaScript

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

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

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

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

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

ดังนั้น

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

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

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

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

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

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

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

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

d8 --trace-opt primes.js

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

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

ดังนั้น

  • ใส่โค้ดที่คำนึงถึง Perf ลงในฟังก์ชันที่ฝังไว้หากคุณลองใช้บล็อก {} ดักจับ {}: ```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 (และบทความนี้) จะช่วยให้คุณเข้าใจวิธีเรียกใช้ JavaScript ของ V8 ได้ดีขึ้น แต่อย่าลืม เน้นไปที่การเพิ่มประสิทธิภาพอัลกอริทึมของคุณด้วย

รายการอ้างอิง