ใช้การพิสูจน์หลักฐานและการทำงานเชิงสืบสวนเพื่อไขปริศนาประสิทธิภาพของ JavaScript

บทนำ

ในช่วงไม่กี่ปีที่ผ่านมา เว็บแอปพลิเคชันมีความเร็วเพิ่มขึ้นอย่างมาก ปัจจุบันแอปพลิเคชันจำนวนมากทำงานได้เร็วพอจนฉันได้ยินนักพัฒนาซอฟต์แวร์บางรายสงสัยว่า "เว็บเร็วพอหรือยัง" ซึ่งอาจเพียงพอสำหรับบางแอปพลิเคชัน แต่สำหรับนักพัฒนาแอปที่ทำงานกับแอปพลิเคชันประสิทธิภาพสูง เราทราบดีว่ายังเร็วไม่พอ แม้ว่าเทคโนโลยีเครื่องเสมือน JavaScript จะมีความก้าวหน้าอย่างน่าทึ่ง แต่การศึกษาล่าสุดแสดงให้เห็นว่าแอปพลิเคชัน Google ใช้เวลา 50% ถึง 70% ภายใน V8 แอปพลิเคชันของคุณมีเวลาจำกัด การลดรอบการทำงานของระบบหนึ่งหมายความว่าอีกระบบหนึ่งจะทำงานได้มากขึ้น โปรดทราบว่าแอปพลิเคชันที่ทำงานที่ 60 FPS จะมีเวลาเพียง 16 มิลลิวินาทีต่อเฟรมเท่านั้น ไม่เช่นนั้นจะเกิดอาการกระตุก อ่านต่อเพื่อดูข้อมูลเกี่ยวกับการเพิ่มประสิทธิภาพ JavaScript และแอปพลิเคชัน JavaScript ของโปรไฟล์ในเรื่องราวจากสนามรบของนักสืบประสิทธิภาพในทีม V8 ที่ติดตามปัญหาด้านประสิทธิภาพที่คลุมเครือใน Find Your Way to Oz

เซสชัน Google I/O 2013

ฉันนำเสนอเนื้อหานี้ในงาน Google I/O 2013 ดูวิดีโอด้านล่าง

เหตุใดประสิทธิภาพจึงมีความสำคัญ

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

การแก้ปัญหาด้านประสิทธิภาพ

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

V8 CSI: ออสเตรเลีย

นักพัฒนาที่น่าทึ่งซึ่งสร้าง Find Your Way to Oz ได้ติดต่อทีม V8 เกี่ยวกับปัญหาด้านประสิทธิภาพที่พวกเขาแก้เองไม่ได้ บางครั้ง Oz จะค้าง ทำให้เกิดอาการกระตุก นักพัฒนาแอป Oz ได้ทำการตรวจสอบเบื้องต้นโดยใช้แผงไทม์ไลน์ในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เมื่อดูการใช้งานหน่วยความจำ พวกเขาพบกราฟฟันเลื่อยที่น่ากลัว ทุกๆ 1 วินาที เครื่องมือเก็บขยะจะเก็บขยะ 10 MB และการหยุดชั่วคราวของเครื่องมือเก็บขยะจะสอดคล้องกับอาการกระตุก คล้ายกับภาพหน้าจอต่อไปนี้จากไทม์ไลน์ในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

ไทม์ไลน์เครื่องมือสำหรับนักพัฒนาเว็บ

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

หลักฐาน

ขั้นตอนแรกคือการรวบรวมและศึกษาหลักฐานเบื้องต้น

เรากำลังดูแอปพลิเคชันประเภทใดอยู่

เดโม Oz เป็นแอปพลิเคชัน 3 มิติแบบอินเทอร์แอกทีฟ ด้วยเหตุนี้ จึงมีความไวต่อการหยุดชั่วคราวที่เกิดจากการเก็บขยะอย่างมาก โปรดทราบว่าแอปพลิเคชันแบบอินเทอร์แอกทีฟที่ทำงานที่ 60fps มีเวลา 16 มิลลิวินาทีในการทำงาน JavaScript ทั้งหมด และต้องเหลือเวลาส่วนหนึ่งไว้ให้ Chrome ประมวลผลการเรียกใช้กราฟิกและวาดหน้าจอ

Oz ทำการคำนวณทางคณิตศาสตร์จำนวนมากเกี่ยวกับค่า Double และเรียกใช้ WebAudio และ WebGL บ่อยครั้ง

ปัญหาด้านประสิทธิภาพที่พบคืออะไร

เราพบการหยุดชั่วคราวหรือที่เรียกว่าเฟรมดร็อปหรืออาการกระตุก การหยุดชั่วคราวเหล่านี้สัมพันธ์กับการเรียกใช้การเก็บขยะ

นักพัฒนาแอปทำตามแนวทางปฏิบัติแนะนำหรือไม่

ใช่ นักพัฒนา Oz มีความเชี่ยวชาญด้านประสิทธิภาพของ JavaScript VM และเทคนิคการเพิ่มประสิทธิภาพ โปรดทราบว่านักพัฒนา Oz ใช้ CoffeeScript เป็นภาษาต้นทางและสร้างโค้ด JavaScript ผ่านคอมไพเลอร์ CoffeeScript ซึ่งทำให้การตรวจสอบบางอย่างทำได้ยากขึ้นเนื่องจากโค้ดที่นักพัฒนา Oz เขียนและโค้ดที่ V8 ใช้งานไม่เชื่อมโยงกัน ตอนนี้เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome รองรับซอร์สแมป ซึ่งจะช่วยให้การดำเนินการนี้ง่ายขึ้น

เหตุใดจึงมีการเรียกใช้ Garbage Collector

VM จะจัดการหน่วยความจำใน JavaScript ให้กับนักพัฒนาซอฟต์แวร์โดยอัตโนมัติ V8 ใช้ระบบจัดการหน่วยความจำที่ไม่ใช้แล้วทั่วไป โดยจะแบ่งหน่วยความจำออกเป็น 2 (หรือมากกว่า) รุ่น Young Generation จะเก็บออบเจ็กต์ที่เพิ่งได้รับการจัดสรร หากออบเจ็กต์อยู่รอดนานพอ ระบบจะย้ายออบเจ็กต์ไปยังรุ่นเก่า

ระบบจะรวบรวมข้อมูลรุ่นใหม่ด้วยความถี่ที่สูงกว่ารุ่นเก่ามาก ซึ่งเป็นไปตามที่ออกแบบไว้ เนื่องจากคอลเล็กชันรุ่นใหม่มีราคาถูกกว่ามาก โดยทั่วไปแล้ว การหยุดชั่วคราวของ GC ที่เกิดขึ้นบ่อยครั้งมักเกิดจากการรวบรวมข้อมูลของ Young Generation

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

หน่วยความจำ Young ของ V8

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

คุณควรเข้าใจโดยสัญชาตญาณว่าทุกครั้งที่มีการจัดสรรออบเจ็กต์ ไม่ว่าจะโดยนัยหรือโดยชัดแจ้ง (ผ่านการเรียกใช้ new, [] หรือ {}) แอปพลิเคชันของคุณจะเข้าใกล้การเก็บขยะและการหยุดแอปพลิเคชันชั่วคราวที่น่ากลัวมากขึ้นเรื่อยๆ

แอปพลิเคชันนี้ควรมีขยะ 10 MB/วินาทีใช่ไหม

สรุปสั้นๆ คือไม่ นักพัฒนาแอปไม่ได้ทำอะไรที่คาดว่าจะทำให้เกิดขยะ 10 MB/วินาที

ผู้ต้องสงสัย

ขั้นตอนถัดไปของการตรวจสอบคือการระบุผู้ต้องสงสัยที่อาจเป็นไปได้ แล้วคัดออกให้เหลือน้อยลง

ผู้ต้องสงสัย #1

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

ผู้ต้องสงสัย #2

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

ผู้ต้องสงสัย #3

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

var a = p * d;
var b = c + 3;
var c = 3.3 * dt;
point.x = a * b * c;

ส่งผลให้มีการสร้างออบเจ็กต์ HeapNumber 5 รายการ 3 รายการแรกมีไว้สำหรับตัวแปร a, b และ c รายการที่ 4 มีไว้สำหรับค่าที่ไม่ระบุตัวตน (a * b) และรายการที่ 5 มาจาก #4 * c โดยรายการที่ 5 จะกำหนดให้กับ point.x ในท้ายที่สุด

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

ผู้ต้องสงสัย #4

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

sprite.position.x += 0.5 * (dt);

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

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

ผู้ต้องสงสัยคนที่ 4 มีความเป็นไปได้

การสืบค้นเบาะแสเกี่ยวกับนิติวิทยาศาสตร์

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

การทดสอบ #1

กำลังตรวจสอบผู้ต้องสงสัยรายที่ 3 (การคำนวณทางคณิตศาสตร์ภายในฟังก์ชันที่ไม่ได้เพิ่มประสิทธิภาพ) เครื่องมือ V8 JavaScript มีระบบการบันทึกในตัวซึ่งให้ข้อมูลเชิงลึกที่ยอดเยี่ยมเกี่ยวกับสิ่งที่เกิดขึ้นเบื้องหลัง

เริ่มจาก Chrome ไม่ทำงานเลย ให้เปิด Chrome ด้วยแฟล็กต่อไปนี้

--no-sandbox --js-flags="--prof --noprof-lazy --log-timer-events"

จากนั้นการออกจาก Chrome โดยสมบูรณ์จะทำให้เกิดไฟล์บันทึก v8.log ในไดเรกทอรีปัจจุบัน

หากต้องการตีความเนื้อหาของ v8.log คุณต้องดาวน์โหลด v8 เวอร์ชันเดียวกับที่ Chrome ใช้อยู่ (ตรวจสอบ about:version) และสร้าง

หลังจากสร้าง v8 สำเร็จแล้ว คุณจะประมวลผลบันทึกโดยใช้ Tick Processor ได้ดังนี้

$ tools/linux-tick-processor /path/to/v8.log

(แทนที่ mac หรือ windows ด้วย linux โดยขึ้นอยู่กับแพลตฟอร์มของคุณ) (ต้องเรียกใช้เครื่องมือนี้จากไดเรกทอรีต้นทางระดับบนสุดใน v8)

เครื่องมือประมวลผลขีดจะแสดงตารางแบบข้อความของฟังก์ชัน JavaScript ที่มีขีดมากที่สุด

[JavaScript]:
ticks  total  nonlib   name
167   61.2%   61.2%  LazyCompile: *opt demo.js:12
 40   14.7%   14.7%  LazyCompile: unopt demo.js:20
 15    5.5%    5.5%  Stub: KeyedLoadElementStub
 13    4.8%    4.8%  Stub: BinaryOpStub_MUL_Alloc_Number+Smi
  6    2.2%    2.2%  Stub: BinaryOpStub_ADD_OverwriteRight_Number+Number
  4    1.5%    1.5%  Stub: KeyedStoreElementStub
  4    1.5%    1.5%  KeyedLoadIC:  {12}
  2    0.7%    0.7%  KeyedStoreIC:  {13}
  1    0.4%    0.4%  LazyCompile: ~main demo.js:30

คุณจะเห็นว่า demo.js มีฟังก์ชัน 3 รายการ ได้แก่ opt, unopt และ main ฟังก์ชันที่เพิ่มประสิทธิภาพจะมีเครื่องหมายดอกจัน (*) อยู่ข้างชื่อ สังเกตว่าฟังก์ชัน opt ได้รับการเพิ่มประสิทธิภาพแล้ว และ unopt ยังไม่ได้รับการเพิ่มประสิทธิภาพ

เครื่องมือสำคัญอีกอย่างหนึ่งในกระเป๋าเครื่องมือของนักสืบ V8 คือ plot-timer-event ซึ่งสามารถดำเนินการได้ดังนี้

$ tools/plot-timer-event /path/to/v8.log

หลังจากเรียกใช้แล้ว ไฟล์ png ชื่อ timer-events.png จะอยู่ในไดเรกทอรีปัจจุบัน เมื่อเปิดขึ้นมา คุณจะเห็นหน้าตาประมาณนี้

เหตุการณ์ในตัวจับเวลา

นอกจากกราฟที่ด้านล่างแล้ว ข้อมูลจะแสดงเป็นแถว แกน X คือเวลา (มิลลิวินาที) ด้านซ้ายมือจะมีป้ายกำกับสำหรับแต่ละแถว ดังนี้

แกน Y ของเหตุการณ์ในตัวจับเวลา

แถว V8.Execute มีเส้นแนวตั้งสีดำที่ขีดไว้ที่ขีดโปรไฟล์แต่ละขีดที่ V8 เรียกใช้โค้ด JavaScript V8.GCScavenger มีเส้นแนวตั้งสีน้ำเงินที่วาดไว้ที่ขีดโปรไฟล์แต่ละขีดที่ V8 ทำการรวบรวมข้อมูลรุ่นใหม่ และสถานะ V8 ที่เหลือก็เป็นไปในลักษณะเดียวกัน

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

ประเภทโค้ดที่กำลังเรียกใช้

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

หากคุณทำตามขั้นตอนเหล่านี้แล้ว โปรดทราบว่าคุณจะทำงานได้เร็วขึ้นมากด้วยการเปลี่ยนโครงสร้างภายในโค้ดของแอปพลิเคชันเพื่อให้ทำงานใน Shell การแก้ไขข้อบกพร่อง v8: d8 ได้ การใช้ d8 ช่วยให้คุณทำซ้ำได้เร็วขึ้นด้วยเครื่องมือ tick-processor และ plot-timer-event ผลข้างเคียงอีกอย่างหนึ่งของการใช้ d8 คือช่วยให้แยกปัญหาที่แท้จริงได้ง่ายขึ้น ซึ่งจะช่วยลดปริมาณข้อมูลรบกวนในข้อมูล

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

ผังเหตุการณ์ในตัวจับเวลา

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

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

function updateSprites(dt) {
    for (var sprite in sprites) {
        sprite.position.x += 0.5 * dt;
        // 20 more lines of arithmetic computation.
    }
}

ด้วยความที่รู้จัก V8 เป็นอย่างดี พวกเขาจึงทราบทันทีว่าบางครั้ง V8 ไม่ได้เพิ่มประสิทธิภาพโครงสร้างการวนซ้ำ for-i-in กล่าวคือ หากฟังก์ชันมีโครงสร้างการวนซ้ำ for-i-in ระบบอาจไม่เพิ่มประสิทธิภาพฟังก์ชันดังกล่าว กรณีนี้เป็นกรณีพิเศษในปัจจุบัน และมีแนวโน้มที่จะเปลี่ยนแปลงในอนาคต กล่าวคือ V8 อาจเพิ่มประสิทธิภาพโครงสร้างการวนซ้ำนี้ได้ในวันหนึ่ง เนื่องจากเราไม่ใช่นักสืบ V8 และไม่รู้จัก V8 อย่างทะลุปรุโปร่ง เราจะระบุได้อย่างไรว่าเหตุใด updateSprites จึงไม่ได้รับการเพิ่มประสิทธิภาพ

การทดสอบ #2

การเรียกใช้ Chrome ด้วย Flag นี้

--js-flags="--trace-deopt --trace-opt-verbose"

แสดงบันทึกแบบละเอียดของข้อมูลการเพิ่มประสิทธิภาพและการยกเลิกการเพิ่มประสิทธิภาพ เมื่อค้นหาข้อมูล updateSprites เราจะพบข้อมูลต่อไปนี้

[disabled optimization for updateSprites, reason: ForInStatement is not fast case]

โครงสร้างการวนซ้ำ for-i-in เป็นสาเหตุของปัญหาตามที่นักสืบตั้งสมมติฐานไว้จริงๆ

ปิดเคสแล้ว

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

function updateSprite(sprite, dt) {
    sprite.position.x += 0.5 * dt;
    // 20 more lines of arithmetic computation.
}

function updateSprites(dt) {
    for (var sprite in sprites) {
        updateSprite(sprite, dt);
    }
}

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

บทส่งท้าย

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

ออกไปไขคดีอาชญากรรมด้านประสิทธิภาพกันเลย