การจัดการหน่วยความจำในภาษาต่างๆ มันจะจัดสรร memory ได้ทั้งแบบ stack และ heap ทั้ง 2 อย่างนี้แตกต่างกันอย่างไร บทความนี้จะพาทำความเข้าใจแบบคร่าวๆ

ก่อนอื่นต้องบอกเลยว่า... โปรแกรมหลายภาษาที่เราเขียนๆ กันอยู่กัน เราแทบไม่จำเป็นต้องสนใจเรื่องของ stack และ heap เลย เพราะตัวภาษาเหล่านั้นมันเป็นคนจัดการให้เราทั้งหมด

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

Stack

เป็นวิธีการเก็บค่าลง memory แบบตามลำดับ และดึงข้อมูลล่าสุดออกมาก่อน เราเรียกว่ามันว่า First In, Last Out เราสามารถอธิบายการทำงานของ stack แบบง่ายๆ คือ การวางจานซ้อนกัน เมื่อเราเพิ่มจาน เราจะต้องวางมันไว้บนสุด และเมื่อเราต้องการใช้จาน เราก็แค่หยิบจานจากด้านบนออกมาใช้งาน การเพิ่มหรือหยิบจานออก จากตรงกลางหรือด้านล่างก็ไม่ได้ผลผลัพธ์ไม่ต่างกัน เราเรียกการเพิ่มและลบข้อมูลออกนั้นว่า push และ pop

push to stack
pop out of stack

ข้อมูลทั้งหมดที่จัดเก็บไว้ใน stack จะต้องมีขนาดที่แน่นอนและคงที่ ข้อมูลที่ไม่ทราบขนาด ณ เวลาคอมไพล์หรือขนาดที่อาจเปลี่ยนแปลงจะต้องจัดเก็บไว้ใน heap แทน


Heap

การเก็บข้อมูลแบบ Heap นั้น จะมีขั้นตอนในการเก็บที่เพิ่มเข้ามา เมื่อเราเพิ่มข้อมูลลงใน heap มันจะมีการขอพื้นที่จำนวนหนึ่ง ตัวจัดสรรหน่วยความจำ (memory allocator) จะค้นหาที่ว่างใน heap ที่มีขนาดใหญ่เพียงพอกับข้อมูลที่เราจะใส่ลงไป หลังจากนั้นจะทำเครื่องหมายว่าพื้นที่ตรงนั้นมีการใช้งานอยู่ และส่งกลับ pointer ซึ่งเป็นที่อยู่ของตำแหน่งนั้นกลับมา กระบวนการนี้เรียกว่าการจัดสรรบน heap (allocating on the heap) หรือเรียกสั้นๆ ว่า allocating (การ push ค่าลงบน stack ไม่ถือเป็นการ allocating)

มาถึงตรงนี้ สิ่งที่เราได้คือ pointer ซึ่งทำหน้าที่ชี้ไปยังข้อมูลที่เก็บไว้ ซึ่งตัว pointer นั้นเป็นข้อมูลที่มีขนาดคงที่ เราจึงสามารถนำเอาค่าของ pointer ไปเก็บไว้ใน stack ได้ เมื่อเราต้องการข้อมูลนั้น ก็จะใช้ pointer ในการอ้างอิง เพื่อเข้าถึงข้อมูลที่เก็บไว้จริงๆ

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

Simplified representation of values in heap

การ push ข้อมูลเข้า stack นั้นเร็วกว่า การจัดสรรข้อมูลบน heap เนื่องจากตัว allocator ไม่ต้องคอยค้นหาที่ที่ใช้จัดเก็บข้อมูลใหม่ เพราะที่ที่เก็บข้อมูลได้ จะอยู่บนสุดของ stack เสมอ

เมื่อเปรียบเทียบกันแล้ว การจัดสรรพื้นที่บน heap ต้องอาศัยการทำงานมากขึ้น เพราะ allocator จะต้องค้นหาพื้นที่ ที่ใหญ่พอที่จะเก็บข้อมูลก่อน จากนั้นจึงดำเนินการจัดทำ bookkeeping เพื่อเตรียมสำหรับการจัดสรรครั้งถัดไป การเข้าถึงข้อมูลใน heap จึงช้ากว่าการเข้าถึงข้อมูลบน stack เนื่องจากเราต้องไปหาตัว pointer ก่อน เพื่อให้มันบอกว่าให้เราไปเอาข้อมูลที่ต้องการได้จากตรงไหน

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

อีกอย่างถ้าโค้ดที่เราเขียนมีการเรียกใช้ฟังก์ชัน ค่าที่ส่งผ่านไปยังฟังก์ชัน รวมถึง pointer ที่ชี้ไปยังข้อมูลบน heap และตัวแปรภายในของฟังก์ชันจะถูก pushed เข้า stack และเมื่อการทำงานในฟังก์ชันนั้นสิ้นสุดลง ค่าเหล่านั้นจะถูก popped ออกจาก stack

เพราะฉนั้นหากเราต้องการที่จะลดการสร้างข้อมูลใหม่ใน stack และ heap เราต้องมาดูว่าโค๊ดตรงไหนที่เราเขียนนั้นมีจำนวนข้อมูลซ้ำกันบน heap บ้าง และพยายามลดมันให้เหลือน้อยที่สุด และ clear ข้อมูลที่ไม่ได้ใช้บน heap ออกไปเพื่อให้ยังคงมีพื้นที่ว่างไว้ให้ได้เยอะที่สุด

ดังนั้นเมื่อเราเข้าใจเรื่องของความเป็นเจ้าของ (Ownership) ในภาษา Rust แล้ว เราก็ไม่จำเป็นต้องคิดถึงเรื่อง stack และ heap เท่าไหร่ แค่ต้องรู้ว่าวัตถุประสงค์หลักการของ ownership นั้น มันคือการจัดการข้อมูล ซึ่งมันช่วยอธิบายเราได้ว่า heap มันจึงทำงานยังไง