บทนำ
Daniel Clifford ได้พูดคุยอย่างยอดเยี่ยมที่ Google I/O เกี่ยวกับเคล็ดลับในการปรับปรุงประสิทธิภาพ JavaScript ใน V8 Daniel สนับสนุนให้เรา "เรียกร้องให้เร็วขึ้น" ซึ่งก็คือให้วิเคราะห์ความแตกต่างด้านประสิทธิภาพระหว่าง 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 ประเภทเพื่อจัดการอาร์เรย์ขนาดใหญ่และอาร์เรย์ที่มีค่าไม่มากนัก ดังนี้
- Fast Elements: พื้นที่เก็บข้อมูลเชิงเส้นสำหรับชุดคีย์ที่กะทัดรัด
- องค์ประกอบของพจนานุกรม: พื้นที่เก็บข้อมูลตารางแฮช
คุณไม่ควรทําให้พื้นที่เก็บข้อมูลอาร์เรย์เปลี่ยนจากประเภทหนึ่งเป็นอีกประเภทหนึ่ง
ดังนั้น
- ใช้คีย์ต่อเนื่องที่เริ่มต้นที่ 0 สำหรับอาร์เรย์
- อย่าจัดสรรอาร์เรย์ขนาดใหญ่ (เช่น องค์ประกอบมากกว่า 64, 000 รายการ) ล่วงหน้าเป็นขนาดสูงสุด แต่ให้เพิ่มขนาดไปเรื่อยๆ
- อย่าลบองค์ประกอบในอาร์เรย์ โดยเฉพาะอาร์เรย์ตัวเลข
- อย่าโหลดองค์ประกอบที่ยังไม่ได้เริ่มต้นหรือลบไปแล้ว
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 สมัยใหม่ใช้การคอมไพล์ V8 (JavaScript ของ Chrome) มีคอมไพเลอร์ Just-In-Time (JIT) 2 แบบด้วยกัน ได้แก่
- คอมไพเลอร์ "แบบสมบูรณ์" ซึ่งสามารถสร้างโค้ดที่มีประสิทธิภาพสำหรับ JavaScript ใดก็ได้
- คอมไพเลอร์เพิ่มประสิทธิภาพ ซึ่งจะสร้างโค้ดที่ยอดเยี่ยมสำหรับ JavaScript ส่วนใหญ่ แต่ใช้เวลาในการคอมไพล์นานกว่า
The Full Compiler
ใน V8 คอมไพเลอร์แบบสมบูรณ์จะทำงานกับโค้ดทั้งหมด และเริ่มดำเนินการกับโค้ดโดยเร็วที่สุด ซึ่งจะสร้างโค้ดที่ดีมากแต่ยังไม่ยอดเยี่ยม คอมไพเลอร์นี้แทบไม่ใช้ข้อมูลเกี่ยวกับประเภทใดๆ เลยเมื่อถึงเวลาคอมไพล์ โดยคาดหวังว่าประเภทตัวแปรจะเปลี่ยนแปลงได้และจะเปลี่ยนแปลงเมื่อรันไทม์ โค้ดที่คอมไพเลอร์แบบสมบูรณ์สร้างขึ้นจะใช้แคชอินไลน์ (IC) เพื่อปรับแต่งความรู้เกี่ยวกับประเภทขณะที่โปรแกรมทำงานอยู่ ซึ่งจะปรับปรุงประสิทธิภาพได้ทันที
เป้าหมายของแคชในบรรทัดคือจัดการประเภทอย่างมีประสิทธิภาพโดยการแคชโค้ดที่ขึ้นอยู่กับประเภทสําหรับการดำเนินการ เมื่อโค้ดทํางาน โค้ดจะตรวจสอบการคาดคะเนประเภทก่อน จากนั้นจะใช้แคชในบรรทัดเพื่อตัดขั้นตอนการทำงาน อย่างไรก็ตาม การดำเนินการที่ยอมรับหลายประเภทจะมีประสิทธิภาพลดลง
ดังนั้น
- แนะนำให้ใช้การดำเนินการแบบโมโนโมร์ฟิคมากกว่าการดำเนินการแบบโพลีมอร์ฟิก
การดำเนินการจะเป็นแบบโมโนโมร์ฟิคหากคลาสที่ซ่อนของอินพุตเหมือนกันเสมอ ไม่เช่นนั้นจะเป็นแบบโพลีโมร์ฟิค ซึ่งหมายความว่าอาร์กิวเมนต์บางรายการอาจเปลี่ยนประเภทในการเรียกใช้การดำเนินการที่แตกต่างกัน เช่น การเรียก add() ครั้งที่สองในตัวอย่างนี้ทำให้เกิดหลายรูปแบบ
function add(x, y) {
return x + y;
}
add(1, 2); // + in add is monomorphic
add("a", "b"); // + in add becomes polymorphic```
คอมไพเลอร์เพิ่มประสิทธิภาพ
ควบคู่ไปกับคอมไพเลอร์แบบสมบูรณ์ V8 จะคอมไพล์ฟังก์ชัน "ที่ทำงานอยู่" (นั่นคือ ฟังก์ชันที่ทำงานหลายครั้ง) อีกครั้งด้วยคอมไพเลอร์เพิ่มประสิทธิภาพ คอมไพเลอร์นี้ใช้ความคิดเห็นเกี่ยวกับประเภทเพื่อให้โค้ดที่คอมไพล์เร็วขึ้น และจะใช้ประเภทที่มาจาก IC ที่เราเพิ่งพูดถึง
ในคอมไพเลอร์การเพิ่มประสิทธิภาพ ระบบจะใส่การดำเนินการไว้บรรทัดเดียวกับที่เรียกใช้ (วางไว้โดยตรงที่เรียกใช้) ซึ่งจะเพิ่มความเร็วในการดำเนินการ (แต่จะใช้พื้นที่หน่วยความจำมากขึ้น) และเปิดใช้การเพิ่มประสิทธิภาพอื่นๆ ด้วย ฟังก์ชันและตัวสร้างแบบโมโนโมร์ฟิคสามารถฝังไว้ในโค้ดได้ทั้งหมด (นี่เป็นเหตุผลอีกประการหนึ่งที่โมโนโมร์ฟิคเป็นแนวคิดที่ดีใน V8)
คุณสามารถบันทึกสิ่งที่ได้รับการเพิ่มประสิทธิภาพโดยใช้เครื่องมือ V8 เวอร์ชัน "d8" แบบสแตนด์อโลนได้ ดังนี้
d8 --trace-opt primes.js
(การดำเนินการนี้จะบันทึกชื่อของฟังก์ชันที่ได้รับการเพิ่มประสิทธิภาพไปยัง stdout)
อย่างไรก็ตาม ฟังก์ชันบางรายการไม่สามารถเพิ่มประสิทธิภาพได้ เนื่องจากฟีเจอร์บางอย่างป้องกันไม่ให้คอมไพเลอร์เพิ่มประสิทธิภาพทำงานในฟังก์ชันหนึ่งๆ ("การหยุดทำงาน") โดยเฉพาะอย่างยิ่ง คอมไพเลอร์การเพิ่มประสิทธิภาพจะหยุดทำงานในฟังก์ชันที่มีบล็อก try {} catch {}
ดังนั้น
- ใส่โค้ดที่ละเอียดอ่อนต่อประสิทธิภาพไว้ในฟังก์ชันที่ฝังอยู่หากคุณมีบล็อก try {} catch {} ดังนี้ ```js function perf_sensitive() { // Do performance-sensitive work here }
try { perf_sensitive() } catch (e) { // จัดการข้อยกเว้นที่นี่ } ```
คำแนะนำนี้อาจมีการเปลี่ยนแปลงในอนาคต เนื่องจากเราเปิดใช้บล็อก try/catch ในคอมไพเลอร์การเพิ่มประสิทธิภาพ คุณสามารถตรวจสอบวิธีที่คอมไพเลอร์การเพิ่มประสิทธิภาพยกเลิกการดำเนินการในฟังก์ชันต่างๆ โดยใช้ตัวเลือก "--trace-opt" กับ d8 ดังที่แสดงด้านบน ซึ่งจะให้ข้อมูลเพิ่มเติมเกี่ยวกับฟังก์ชันที่ถูกยกเลิกการดำเนินการ
d8 --trace-opt primes.js
การเพิ่มประสิทธิภาพ
สุดท้าย การเพิ่มประสิทธิภาพที่คอมไพเลอร์นี้ทําเป็นการเพิ่มประสิทธิภาพโดยประมาณ ซึ่งบางครั้งอาจไม่ได้ผลและเราจึงหยุดการเพิ่มประสิทธิภาพ กระบวนการ "ยกเลิกการเพิ่มประสิทธิภาพ" จะทิ้งโค้ดที่เพิ่มประสิทธิภาพแล้ว และดําเนินการต่อที่ตําแหน่งที่เหมาะสมในโค้ดคอมไพเลอร์ "แบบเต็ม" ระบบอาจเรียกใช้การเพิ่มประสิทธิภาพอีกครั้งในภายหลัง แต่ในระยะสั้น การดำเนินการจะช้าลง โดยเฉพาะอย่างยิ่ง การเปลี่ยนแปลงคลาสที่ซ่อนของตัวแปรหลังจากที่เพิ่มประสิทธิภาพฟังก์ชันแล้วจะทำให้การเพิ่มประสิทธิภาพลดลง
ดังนั้น
- หลีกเลี่ยงการเปลี่ยนแปลงคลาสที่ซ่อนอยู่ในฟังก์ชันหลังจากเพิ่มประสิทธิภาพแล้ว
คุณสามารถดูบันทึกของฟังก์ชันที่ V8 ต้องยกเลิกการเพิ่มประสิทธิภาพด้วย Flag การบันทึก เช่นเดียวกับการเพิ่มประสิทธิภาพอื่นๆ ดังนี้
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 ได้ดียิ่งขึ้น แต่อย่าลืมมุ่งเน้นที่การเพิ่มประสิทธิภาพอัลกอริทึมของคุณเองด้วย