บทนำ
คุณได้รับอีเมลที่ระบุว่าเกมบนเว็บ/เว็บแอปของคุณทำงานได้ไม่ดีหลังจากผ่านไประยะเวลาหนึ่ง คุณจึงเจาะลึกโค้ดแต่ไม่เห็นสิ่งใดที่โดดเด่น จนกระทั่งเปิดเครื่องมือประสิทธิภาพหน่วยความจำของ Chrome และเห็นสิ่งนี้
เพื่อนร่วมงานคนหนึ่งหัวเราะเบาๆ เพราะรู้ว่าคุณมีปัญหาด้านประสิทธิภาพที่เกี่ยวข้องกับหน่วยความจำ
ในมุมมองกราฟหน่วยความจำ รูปแบบฟันเลื่อยนี้บ่งบอกถึงปัญหาด้านประสิทธิภาพที่อาจร้ายแรงได้เป็นอย่างดี เมื่อการใช้งานหน่วยความจำเพิ่มขึ้น คุณจะเห็นพื้นที่แผนภูมิเพิ่มขึ้นด้วยในการจับภาพไทม์ไลน์ เมื่อแผนภูมิลดลงอย่างกะทันหัน แสดงว่ามีอินสแตนซ์ที่ Garbage Collector ทำงานและล้างออบเจ็กต์หน่วยความจำที่อ้างอิง
ในกราฟแบบนี้ คุณจะเห็นว่ามีเหตุการณ์ Garbage Collection เกิดขึ้นมากมาย ซึ่งอาจส่งผลเสียต่อประสิทธิภาพของเว็บแอป บทความนี้จะพูดถึงวิธีควบคุมการใช้งานหน่วยความจำเพื่อลดผลกระทบต่อประสิทธิภาพ
การจัดการหน่วยความจำที่ไม่ใช้แล้วและค่าใช้จ่ายด้านประสิทธิภาพ
โมเดลหน่วยความจำของ JavaScript สร้างขึ้นจากเทคโนโลยีที่เรียกว่า Garbage Collector ในหลายๆ ภาษา โปรแกรมเมอร์มีหน้าที่รับผิดชอบโดยตรงในการจัดสรรและเพิ่มพื้นที่ว่างในหน่วยความจำจาก Memory Heap ของระบบ อย่างไรก็ตาม ระบบ Garbage Collector จะจัดการงานนี้ในนามของโปรแกรมเมอร์ ซึ่งหมายความว่าออบเจ็กต์จะไม่ถูกปล่อยออกจากหน่วยความจำโดยตรงเมื่อโปรแกรมเมอร์ยกเลิกการอ้างอิง แต่จะปล่อยในภายหลังเมื่อการเรียนรู้ของ GC ตัดสินว่าการทำเช่นนั้นจะเป็นประโยชน์ กระบวนการตัดสินใจนี้กำหนดให้ GC ทำการวิเคราะห์ทางสถิติเกี่ยวกับออบเจ็กต์ที่ใช้งานอยู่และไม่ได้ใช้งาน ซึ่งใช้เวลาในการดำเนินการ
การเก็บขยะมักถูกมองว่าตรงกันข้ามกับการจัดการหน่วยความจำด้วยตนเอง ซึ่งกำหนดให้โปรแกรมเมอร์ระบุออบเจ็กต์ที่จะยกเลิกการจัดสรรและส่งคืนไปยังระบบหน่วยความจำ
กระบวนการที่ GC เรียกคืนหน่วยความจำนั้นไม่ฟรี โดยปกติแล้วจะลดประสิทธิภาพที่พร้อมใช้งานโดยใช้บล็อกเวลาในการทำงาน นอกจากนี้ ระบบเองจะตัดสินใจว่าจะเรียกใช้เมื่อใด คุณไม่สามารถควบคุมการดำเนินการนี้ได้ เนื่องจาก GC Pulse อาจเกิดขึ้นได้ทุกเมื่อระหว่างการเรียกใช้โค้ด ซึ่งจะบล็อกการเรียกใช้โค้ดจนกว่าจะเสร็จสมบูรณ์ โดยทั่วไปคุณจะไม่ทราบระยะเวลาของพัลส์นี้ ซึ่งจะใช้เวลาในการทำงานระยะหนึ่ง โดยขึ้นอยู่กับวิธีที่โปรแกรมใช้หน่วยความจำ ณ จุดใดจุดหนึ่ง
แอปพลิเคชันประสิทธิภาพสูงอาศัยขอบเขตประสิทธิภาพที่สม่ำเสมอเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่น ระบบ Garbage Collector อาจทำให้เป้าหมายนี้ไม่สำเร็จ เนื่องจากระบบอาจทำงานแบบสุ่มเวลาและสุ่มระยะเวลา ซึ่งจะกินเวลาที่แอปพลิเคชันต้องใช้เพื่อให้บรรลุเป้าหมายด้านประสิทธิภาพ
ลดการเสียหน่วยความจำ ลดภาระการเก็บขยะ
ตามที่ระบุไว้ พัลส์ GC จะเกิดขึ้นเมื่อชุดการเรียนรู้ของระบบระบุว่ามีออบเจ็กต์ที่ไม่มีการใช้งานมากพอที่พัลส์จะเป็นประโยชน์ ดังนั้นกุญแจสำคัญในการลดระยะเวลาที่ Garbage Collector ใช้จากแอปพลิเคชันของคุณคือการกำจัดกรณีการสร้างและการปล่อยออบเจ็กต์ที่มากเกินไปให้ได้มากที่สุด กระบวนการสร้าง/ปล่อยออบเจ็กต์บ่อยๆ นี้เรียกว่า "การเสียหน่วยความจำ" หากลดการเสียหน่วยความจำระหว่างอายุการใช้งานของแอปพลิเคชันได้ คุณก็จะลดระยะเวลาที่ GC ใช้ในการดำเนินการด้วย ซึ่งหมายความว่าคุณต้องนำออก/ลดจำนวนออบเจ็กต์ที่สร้างและทำลายลง กล่าวคือ คุณต้องหยุดจัดสรรหน่วยความจำ
กระบวนการนี้จะย้ายกราฟความทรงจำของคุณจากกราฟนี้
เป็นแบบนี้
ในโมเดลนี้ คุณจะเห็นว่ากราฟไม่มีรูปแบบฟันเลื่อยอีกต่อไป แต่จะเพิ่มขึ้นอย่างมากในช่วงเริ่มต้น แล้วค่อยๆ เพิ่มขึ้นเมื่อเวลาผ่านไป หากคุณพบปัญหาด้านประสิทธิภาพเนื่องจากการเสียหน่วยความจำ คุณควรสร้างกราฟประเภทนี้
การเปลี่ยนไปใช้ JavaScript แบบหน่วยความจำคงที่
JavaScript หน่วยความจำแบบคงที่เป็นเทคนิคที่เกี่ยวข้องกับการจัดสรรล่วงหน้าเมื่อเริ่มแอป หน่วยความจำทั้งหมดที่จำเป็นสำหรับอายุการใช้งาน และการจัดการหน่วยความจำนั้นระหว่างการดำเนินการเมื่อไม่จำเป็นต้องใช้ออบเจ็กต์อีกต่อไป เราสามารถบรรลุเป้าหมายนี้ได้ในไม่กี่ขั้นตอนง่ายๆ ดังนี้
- ใช้แอปพลิเคชันเป็นเครื่องมือในการระบุจำนวนสูงสุดของออบเจ็กต์หน่วยความจำแบบสดที่จำเป็น (ต่อประเภท) สำหรับสถานการณ์การใช้งานที่หลากหลาย
- ใช้โค้ดอีกครั้งเพื่อจัดสรรจำนวนสูงสุดนั้นล่วงหน้า แล้วดึงข้อมูล/ปล่อยข้อมูลด้วยตนเองแทนที่จะไปที่หน่วยความจำหลัก
ในความเป็นจริง การจะทำข้อ 1 ให้สำเร็จได้นั้นเราต้องทำข้อ 2 ด้วยเล็กน้อย ดังนั้นเรามาเริ่มกันที่ข้อ 2 ก่อน
พูลออบเจ็กต์
พูดง่ายๆ ก็คือ การรวมออบเจ็กต์คือกระบวนการเก็บรักษาชุดออบเจ็กต์ที่ไม่ได้ใช้ซึ่งมีประเภทร่วมกัน เมื่อต้องการออบเจ็กต์ใหม่สำหรับโค้ด แทนที่จะจัดสรรออบเจ็กต์ใหม่จาก Memory Heap ของระบบ คุณจะรีไซเคิลออบเจ็กต์ที่ไม่ได้ใช้จากพูลแทน เมื่อโค้ดภายนอกทำงานกับออบเจ็กต์เสร็จแล้ว ระบบจะส่งออบเจ็กต์กลับไปยังพูลแทนที่จะปล่อยไปยังหน่วยความจำหลัก เนื่องจากออบเจ็กต์ไม่เคยถูกยกเลิกการอ้างอิง (หรือที่เรียกว่าลบ) จากโค้ด จึงไม่มีการเก็บขยะ การใช้พูลออบเจ็กต์จะช่วยให้โปรแกรมเมอร์ควบคุมหน่วยความจำได้อีกครั้ง ซึ่งจะช่วยลดผลกระทบของ Garbage Collector ต่อประสิทธิภาพ
เนื่องจากมีชุดประเภทออบเจ็กต์ที่แตกต่างกันซึ่งแอปพลิเคชันดูแลรักษา การใช้พูลออบเจ็กต์อย่างเหมาะสมจึงกำหนดให้คุณต้องมีพูล 1 พูลต่อประเภทที่มีการเลิกใช้งานสูงในระหว่างรันไทม์ของแอปพลิเคชัน
var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};
//..... do some stuff with the object that we need to do
gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference
สำหรับแอปพลิเคชันส่วนใหญ่ ในที่สุดคุณจะพบว่าความจำเป็นในการจัดสรรออบเจ็กต์ใหม่จะคงที่ในระดับหนึ่ง เมื่อเรียกใช้แอปพลิเคชันหลายครั้ง คุณควรจะทราบขีดจำกัดสูงสุดนี้ และสามารถจัดสรรออบเจ็กต์จำนวนดังกล่าวไว้ล่วงหน้าเมื่อเริ่มแอปพลิเคชัน
การจัดสรรออบเจ็กต์ล่วงหน้า
การใช้พูลออบเจ็กต์ในโปรเจ็กต์จะช่วยให้คุณทราบจำนวนออบเจ็กต์สูงสุดตามทฤษฎีที่จำเป็นในระหว่างรันไทม์ของแอปพลิเคชัน เมื่อเรียกใช้เว็บไซต์ผ่านสถานการณ์การทดสอบต่างๆ คุณจะเข้าใจประเภทข้อกำหนดด้านหน่วยความจำที่จำเป็น และสามารถจัดหมวดหมู่ข้อมูลนั้นไว้ที่ใดที่หนึ่ง และวิเคราะห์ข้อมูลเพื่อทำความเข้าใจขีดจำกัดสูงสุดของข้อกำหนดด้านหน่วยความจำสำหรับแอปพลิเคชันของคุณ
จากนั้นในแอปเวอร์ชันที่เผยแพร่ คุณสามารถตั้งค่าระยะการเริ่มต้นเพื่อกรอกข้อมูลล่วงหน้าในพูลออบเจ็กต์ทั้งหมดตามจำนวนที่ระบุ การดำเนินการนี้จะพุชการเริ่มต้นออบเจ็กต์ทั้งหมดไปที่ด้านหน้าของแอป และลดจำนวนการจัดสรรที่เกิดขึ้นแบบไดนามิกระหว่างการดำเนินการ
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
จำนวนที่คุณเลือกมีผลอย่างมากต่อลักษณะการทำงานของแอปพลิเคชัน บางครั้งจำนวนสูงสุดตามทฤษฎีก็ไม่ใช่ตัวเลือกที่ดีที่สุด เช่น การเลือกค่าเฉลี่ยสูงสุดอาจทำให้หน่วยความจำที่ใช้มีขนาดเล็กลงสำหรับผู้ใช้ทั่วไป
ไม่ใช่ทางออกที่สมบูรณ์แบบ
มีแอปหลายประเภทที่รูปแบบการเติบโตของหน่วยความจำแบบคงที่อาจเป็นประโยชน์ อย่างไรก็ตาม Renato Mangini ซึ่งเป็น DevRel ของ Chrome ได้ชี้ให้เห็นว่ามีข้อเสียอยู่ 2-3 ข้อ
บทสรุป
เหตุผลหนึ่งที่ทำให้ JavaScript เหมาะสำหรับเว็บคือเป็นภาษาที่เริ่มต้นใช้งานได้ง่าย สนุก และรวดเร็ว สาเหตุหลักมาจากข้อจำกัดด้านไวยากรณ์ที่ต่ำและการจัดการปัญหาเกี่ยวกับหน่วยความจำในนามของคุณ คุณสามารถเขียนโค้ดและปล่อยให้ Gemini จัดการงานที่ยุ่งยากได้ อย่างไรก็ตาม สำหรับเว็บแอปพลิเคชันที่มีประสิทธิภาพสูง เช่น เกม HTML5 GC มักจะลดอัตราเฟรมที่จำเป็นอย่างยิ่ง ซึ่งจะลดประสบการณ์การใช้งานของผู้ใช้ปลายทาง การใช้เครื่องมืออย่างระมัดระวังและการใช้พูลออบเจ็กต์จะช่วยลดภาระของอัตราเฟรม และใช้เวลาดังกล่าวไปกับสิ่งอื่นๆ ที่ยอดเยี่ยมกว่าได้
ซอร์สโค้ด
มีพูลออบเจ็กต์จำนวนมากที่ใช้งานอยู่บนเว็บ ดังนั้นเราจะไม่ทำให้คุณเบื่อด้วยพูลออบเจ็กต์อีกรายการ แต่เราจะแนะนำให้คุณดูแหล่งข้อมูลเหล่านี้ ซึ่งแต่ละแหล่งข้อมูลจะมีรายละเอียดปลีกย่อยในการติดตั้งใช้งานที่เฉพาะเจาะจง ซึ่งเป็นสิ่งสำคัญเนื่องจากการใช้งานแอปพลิเคชันแต่ละรายการอาจมีความต้องการในการติดตั้งใช้งานที่เฉพาะเจาะจง
- พูลออบเจ็กต์ของ Gamecore.js
- Beej’s Object Pools
- พูลออบเจ็กต์ที่เรียบง่ายของ Emehrkay
- พูลออบเจ็กต์ที่เน้นเกมของ Steven Lambert
- การตั้งค่า objectPool ของ RenderEngine