Developer หลายๆคนคงเคยได้ยินคำว่า Garbage Collection บางคนลองอ่านดูแล้วก็งงๆ บางทีก็ปล่อยเลยตามเลย บทความชุดนี้จะพยายามอธิบายความหมายของมันแบบกระชับๆ(มั้ง) โดยจะยกตัวกระบวนการทำงานของเจ้า Garbage Collection อย่างจาก game engine สุดฮิตในสมัยนี้อย่าง Unity ว่าตั้งแต่เราเริ่มสร้างตัวแปรไปยันจบ scope ของ function(method) มันมีการบริหารหน่วยความจำยังไง เพราะจริงๆแล้วเรื่องนี้ถือเป็นเรื่องพื้นฐานของการเขียนโปรแกรมที่หลายๆคนยังคงมองข้ามกันไป โดยเฉพาะการทำเกมที่การบริหารทรัพยากรถือเป็นเรื่องค่อนข้างซีเรียสจริงจังฮาร์ดคอร์เกมมิ่งมาก
Garbage Collection คือชื่อของ process ที่ทำให้หน่วยความจำว่างสำหรับใช้งานอีกครั้ง
ย้อนความไปเมื่อ 10 ปีที่แล้ว ถ้าเป็นโปรแกรมเมอร์สาย native ที่เคยเขียน c++ หรือ objective-c (ตั้งแต่สมัยก่อน iOS 5) คงจะคุ้นเคยกับการจัดการบริหารหน่วยความจำ (ต่อไปจะเรียก memory) ด้วยตัวเองหรือที่เรียกว่า Manual memory management แอพไหนบริหารไม่ดีก็ดับกันไป สมัยก่อนเคยต้องละเมียดละไมกับการบริหาร memory มาก (แต่ตอนนี้ Objective-C กับ Swift มี ARC มาช่วยแล้วสบายใจ)
พอมาสมัยนี้ภาษาโปรแกรมมิ่งสมัยใหม่อย่าง Java, C# ใหม่เริ่มบริหาร memory ให้อัตโนมัติ (Automatic memory management) ซึ่งก็มีเจ้า Garbage Collection นี่แหละมาช่วยจัดการบริหารเรื่อง memory
ถ้าให้ผมนิยาม Garbage Collector เป็นภาษาไทยผมจะเรียกว่า รถเก็บขยะ ถ้าภาษาไทยครองโลก ชื่อนี้แหละเหมาะสุดๆแล้ว :) ถ้าอ่านไปถึงกระบวนการทำงานด้านล่างแล้วจะรู้ว่ามันเป็นรถเก็บขยะยังไง
โดยเจ้า automatic memory management นั้นการทำงานของมันเราไม่จำเป็นต้องไปเรียก function อะไรเพื่อบริหาร memory ที่เราจองไปทั้งนั้น code ทุกบรรทัดที่เราเขียนกันอย่างละเมียดละไม จอง memory กันหมดไปกี่ล้านไบท์ก็ตาม ตัวมันจะบริหารจัดการให้อัตโนมัติ สะดวกดี (ภายใต้ความสะดวกมันมักจะมีอะไรซ่อนอยู่ หึๆๆๆ)
โดยก่อนเริ่มอยากจะให้รู้จักกับคำศัพท์เหล่านี้ก่อน
บน Unity เราจะมี memory-pool สำหรับเก็บข้อมูลบน memory เปรียบได้กับสุกี้หม้อรวม โดยแบ่งออกเป็นสองประเภท
สมมติว่าเราเป็นพนักงานออฟฟิศที่งานล้นมือต้องปั่นให้ทันกำหนดการเลยโทรสั่งมื้อเที่ยงมากิน แน่นอนว่า fast food คือคำตอบสำหรับช่วงเวลานี้
ก่อนสั่งถัง memory-pool ทั้ง heap กับ stack ยังว่างอยู่
เราเลยโทรหาร้านอาหารจัดการสั่ง hamburger จำนวน 1 ชิ้น
ไม่รีรอสั่ง nugget อีก 4 ชิ้นไปนอนอยู่ในถัง stack เป็นเพื่อน
จังหวะนี้พอโทรสั่งเสร็จ พ่อครัวประกอบอาหารชุดแฮปปี้มีลพร้อมเสิร์ฟ โดยเจ้าสิ่งที่เก็บใน stack นั้นเป็นแค่ตัวอ้างอิง (reference) ของตัวแปร object ของ HappyMeal เท่านั้น (เปรียบเหมือนใบออเดอร์อาหารที่ระบุว่า HappyMeal ชุดนี้จะไปส่งให้คุณพนักงานออฟฟิศแถวย่านนานา)
ทีนี้เจ้า object HappyMeal ที่แท้จริงมันไปกองอยู่ที่ heap นะ เพราะตัวมันเองเป็นตัวแปรประเภท reference-type เปรียบได้กับอาหารโดนบรรจุหีบห่อเพื่อให้ messenger เอาไปส่งให้ถึงมือลูกค้านั้นเอง
ชุด HappyMeal ของเราส่งถึงมือเรียบร้อย หมด scope ของ function นี้แล้ว ทีนี้พ่อครัวเลยทำการเก็บกวาดของจากออเดอร์ที่พึ่งทำไป จะเห็นว่าตัวแปรทุกอย่างใน stack หายเกลี้ยงทันที แม้แต่ใบออเดอร์จากออฟฟิศย่านนานา ซึ่งขั้นตอนนี้เราเรียกว่า memory โดน deallocated
แต่เดี๋ยวก่อน? ทำไมถัง heap ยังมีขยะอยู่ละ มันไม่ได้โดนล้างทันทีหลังจากจบ scope ของ function หรอ?
คำตอบคือ ขยะใน heap จะไม่โดน deallocated ทันที เพราะมันต้องรอพระเอกของงานนี้มาเก็บ ซึ่งแน่นอนคือเข้า Garbage Collector รถขยะของเรานั้นเอง โดยเจ้า Garbage Collector จะรู้ว่า object HappyMeal เป็นขยะก็จากการที่ reference ของมันอย่างใบออเดอร์ที่เคยเก็บอยู่ใน stack โดนขยำทิ้งไปแล้วเรียบร้อยนั้นเอง
พระเอกของเรามาแล้ว งั้นมาดูขั้นตอนการทำงานของ Garbage Collector กันก่อนดีกว่า (ผมจะเรียกตัว worker process ที่ทำงานว่า Garbage Collector, ส่วน Garbage Collection คือชื่อกระบวนการ)
ต่อเนื่องจากข้อ 1.) ของหัวข้อก่อนหน้า การ allocated memory บน heap จะมีความซับซ้อนกว่า stack เนื่องจากตัวแปรที่ตัวมันเก็บความความยืดหยุ่น มีขนาด memory ที่เล็กบ้างใหญ่บ้าง เลยอยากจะขยายความเพิ่มเติม โดยเรามาดูขั้นตอนกันว่าเมื่อเราสร้างตัวแปรประเภท reference-type จะมีการขอ allocated memory บน heap กันยังไงบ้าง
สังเกตได้ว่าจังหวะที่ช้าและใช้ทรัพยากรเยอะจะมี 2 จังหวะคือ
เพราะฉะนั้นอธิบายได้ว่า ยิ่ง heap allocation ถี่แค่ไหน ก็มีความเสี่ยงที่จะทำให้เกิดการ Garbage Collection ได้ถี่มากขึ้นเท่านั้น และยิ่ง Garbage Collection ทำงานถี่แค่ไหนก็จะยิ่งมีโอกาศทำให้เกมเราใช้ทรัพยากรได้มากขึ้นเท่านั้น
การ deallocation นั้นก่อให้เกิดการคืน memory กลับไปให้ heap แต่พื้นที่ว่างที่คืนให้นั้นขนาดเล็กใหญ่ไม่เท่ากันตามโครงสร้างของข้อมูล เลยทำให้เกิดสิ่งที่เรียกว่า heap fragmentation ขึ้น
allocation memory กันอย่างเมามัน
Garbage Collector เก็บกวาดเรียบร้อย
มีการสร้างตัวแปรใหม่มาและทำการขอพื้นที่บน heap แต่พื้นที่ว่างที่มีดันเล็กไป
เมื่อเกมถูกดำเนินไปเรื่อยๆและมี heap allocation/deallocation เกิดขึ้นซ้ำไปซ้ำมาจะก่อให้เกิดพื้นที่ว่างของ memory จำนวนมาก และถ้าเกิดเหตุการณ์ตอน allocation แล้วที่ว่างเหลือไม่พอจนต้องทำการขยาย heap ออกไปเรื่อยๆ จนสุดท้ายแล้วตัวอุปกรณ์ก็ไม่มีพื้นที่เหลือให้กู้ยืมแล้วละก็ มันก็จะเกิดโศกนาฏกรรมกับเกมของเรา หรือที่เห็นกันบ่อยว่าเกมเด้ง เกมระเบิด ผู้ใช้งานก็จะมาแจก 1 ดาวบน store ให้เรากันรัวๆ T_T
จบแล้วครับสำหรับ part 1 กับการทำความรู้จักกับ Garbage Collection, heap, stack, memory allocation/deallocation
บทความหน้าจะพาทัวร์เรื่องการ optimization เราจะมาดูการ coding กันว่าแบบไหนที่ทำให้เกิด heap allocation และทำยังไงให้ลดผลกระทบจาก Garbage Collection ได้มากที่สุด รับรองว่าหนักหัวกว่านี้แน่นอน!!!