Stack กับ Heap คืออะไรกันนะ?

ในการเขียนโค้ดในภาษาใดก็ตาม ไม่ว่าจะเป็น C, Python หรือภาษาอื่น ๆ แทบจะทุกภาษาล้วนจะต้องมีการประกาศตัวแปรชนิดต่าง ๆ เช่น int double หรือพวกตัวแปรที่มีความซับซ้อนมากขึ้นมาอย่าง array หรือ struct รวมถึงการจองพื้นที่ไว้ส่วนหนึ่งเพื่อเอาไว้เก็บค่าบางอย่างเพื่อใช้ในโปรแกรม โดยปกติแล้ว เมื่อประกาศตัวแปร พื้นที่ภายในหน่วยความจำจะถูกจองเอาไว้ใช้สำหรับตัวแปรที่เราประกาศ หรือพื้นที่ที่เราจองไว้ แต่ว่าพื้นที่ภายในหน่วยความจำพวกนี้ถูกจองอยู่ตรงไหนของระบบ แล้วถูกจองด้วยวิธีแบบไหนกันนะ?

มาทำความรู้จักส่วนประกอบภายในหน่วยความจำกันเล็กน้อยค่ะ

ภายในหนึ่งโปรแกรมจะมีการจองพื้นที่หนึ่ง ๆ ไว้สำหรับใช้งานภายในโปรแกรม โดยพื้นที่ตรงนี้จะมีโครงสร้างตามรูปด้างล่างนี้ค่ะ

Process Memory Layout สำหรับภาษา C

 

รูปนี้จะจำลองตัว Process Memory Layout ที่ใช้กันภายในภาษา C และ C++ ค่ะ ในภาษาอื่นๆ อาจจะแตกต่างกันเล็กน้อย

ภายใน Process Memory Layout จะแบ่งออกเป็นสองส่วนหลักๆ คือ Static Memory Layout โดยในส่วนนี้จะใช้ในการเก็บโค้ดที่มีการแปลงเป็น Machine Code เรียบร้อยแล้วไว้ในส่วนของ text รวมถึง Global Variable ก็จะนำมาเก็บไว้ในส่วนนี้เช่นกันค่ะ ซึ่งในส่วนของ Global Variable จะสามารถเข้าถึงได้จากทุกที่ภายในโปรแกรม ตราบใดที่โปรแกรมยังทำงานอยู่นั่นเองค่ะ

ส่วนต่อไปที่จะเป็นหัวข้อหลักในบทความนี้ก็คือ Dynamic Memory Layout นั่นเอง จากภายในรูปจะเห็นว่าในช่วงภายในของ Dynamic Memory Layout จะมี Heap และ Stack อยู่ภายในค่ะ รวมถึงมีพื้นที่ว่าง (Free Memory) ที่มีขนาดกว้างมาก ๆ สำหรับรองรับการเพิ่มและลดของขนาดของ Heap และ Stack ค่ะ

 

Stack Memory

ภายใน Stack Memory จะทำการเก็บค่าตัวแปรที่ถูกประกาศไว้ในทุกๆ function (ตัวแปรที่ถูกประกาศใน function main ก็จะถูกเก็บค่าไว้ที่นี่เช่นเดียวกัน) หรือที่เรียกกันว่า Local Variable ค่ะ โดยส่วนประกอบในการทำงานของ stack จะประกอบไปด้วย 2 ส่วน คือ พื้นที่ใน stack จะถูกจองไว้ให้สำหรับแต่ละ function พื้นที่ตรงนี้จะถูกเรียกว่า stack frame และมี pointer ที่ชี้ไปยัง function ล่าสุดที่ถูกเรียกใช้งาน โดย pointer ตัวนี้จะถูกเรียกว่า stack pointer

ลองมาดูตัวอย่างโค้ดกันค่ะ

ตัวอย่างโค้ดสำหรับอธิบายการทำงานของ Stack

ตามโค้ดโปรแกรมมีอยู่ 2 function หลักๆ ค่ะ ก็คือ function main และ twice

เมื่อเริ่มรันโปรแกรม stack frame จะถูกจองไว้ให้สำหรับ function แรกที่ทำงาน ซึ่งในที่นี้คือ function main ค่ะ ตามรูปด้านล่างนี้

 

โดยขนาดของ stack frame จะมีขนาดเท่ากับตัวแปรที่ถูกประกาศไว้ใน function นั้นๆ ค่ะ โดยสำหรับ stack frame นี้จะถูกจองไว้ทั้งหมด 8 byte เนื่องจากตัวแปร integer มีขนาด 4 byte ใน function มี 2 ตัวแปรนั่นก็คือ a และ b รวมเป็น 8 byte

ช่วงเวลานี้ function main เป็น function ล่าสุดที่ถูกใช้งาน stack pointer จึงชี้ไปยัง stack frame ของ function main นั่นเองค่ะ 

ในช่วงเวลาถัดมาที่โค้ดรันถึงบรรทัดที่ 13 ที่มีการเรียกใช้งาน function twice โดย stack frame จะถูกจองไว้ให้สำหรับ function twice นี้จะถูกจองไว้ทั้งหมด 8 byte (ตัวแปร integer 2 ตัวนั่นก็คือ num และ c)

 

เมื่อ function twice ทำงานเสร็จ stack pointer จะกลับไปชี้ stack frame ก่อนหน้า (stack frame ของ function main) และทำการ free ตัว stack frame ของ function twice ออกไป โดยส่วนนี้ภายในโปรแกรมจะทำงานโดยอัตโนมัติค่ะ

โค้ดด้านบนจะเห็นว่ายังมีการเรียกใช้ function printf อยู่ในรรทัดถัดไป เช่นเดียวกันค่ะ stack frame จะถูกจองไว้ให้สำหรับ function printf ตามจำนวนตัวแปรที่อยู่ภายใน function printf ค่ะ แล้วเมื่อทำงานเสร็จ stack frame ของ function printf ก็จะถูกกำจัดออกจาก stack memory ไปค่ะ 

stack frame สำหรับ printf() ถูกสร้างเมื่อ printf() ถูกเรียก (ซ้าย) และ stack frame ของ printf() ถูกลบออกจาก stack memory เมื่อ printf() ทำงานจบ

เมื่อจบโปรแกรม stack frame ของ function main ก็จะถูกลบออกจาก stack memory แล้วจบโปรแกรมค่ะ

stack frame ของ main() ถูกลบออกจาก stack memory

จากที่อธิบายไปข้างต้น จะเห็นว่า CPU จะจัดการเองทั้งหมดโดยที่เราไม่ต้องทำอะไรกับหน่วยความจำด้วยตัวเองเลย และ stack memory เองยังสามารถขยายได้เรื่อยๆ แต่อย่างไรก็ตามภายในระบบปฏิบัติการก็กันพื้นที่เพียงส่วนหนึ่งสำหรับ stack memory ด้วยเช่นกันค่ะ ถ้าภายโปรแกรมมีการสร้าง stack frame มากเกินไปจนเกินพื้นที่ที่ระบบปฏิบัติการกันพื้นที่ไว้ให้ จะทำให้เกิดอาการ stack overflow ได้ค่ะ

Heap Memory

Heap Memory เป็นพื้นที่หน่วยความจำที่ CPU จะทำการจองไว้เพียงคร่าวๆ เพื่อให้ผู้ใช้งานสามารถจองและใช้งานได้อย่างอิสระ โดยหน่วยความจำภายใน Heap เราสามารถเข้าถึงได้จากทุกที่ในโปรแกรม โดยในภาษา C เราสามารถจองพื้นที่ภายใน Heap Memory ได้ผ่าน function สำหรับการจองพื้นที่ เช่น malloc() เป็นต้นค่ะ

สำหรับ function malloc เวลาใช้งานนั้นจะใส่ขนาดที่เราต้องการจองไป และ function จะทำการ return ออกมาเป็น address แรกของบล็อกพื้นที่ที่ function malloc สามารถทำการจองไว้ได้ค่ะ โดยเราสามารถเข้าถึงหน่วยความจำที่ถูกจองไว้ภายใน Heap Memory ผ่าน address ตัวนี้นั่นเองค่ะ

พื้นที่ที่เราจองไปแล้ว ถ้าหากต้องการเปลี่ยนขนาดสามารถเรียกใช้ function realloc() ได้ค่ะ

มาดูตัวอย่างโค้ดกันค่ะ

ตัวอย่างโค้ดสำหรับอธิบายการทำการของ heap memory

ในโค้ดเมื่อเริ่มทำงาน stack memory จะทำการสร้าง stack frame สำหรับ function main โดยภายใน function main มีการประกาศตัวแปร pointer ไว้ ดังนั้น ขนาดของ stack frame จะมีขนาดเท่ากับขนาดของตัวแปร pointer นั่นเองค่ะ (ขนาดของ pointer ขึ้นอยู่กับสถาปัตยกรรมของอุปกรณ์ที่เราใช้ค่ะ อย่างเช่น สถาปัตยกรรมแบบ 32-bit pointer จะมีขนาด 4 byte ค่ะ)

ต่อไปเป็น infinite loop (ลูปแบบไม่รู้จบ) ซึ่งภายในลูปจะมีการจองพื้นที่โดยใช้ function malloc และ ptr จะทำการเก็บ address ที่จองได้มาจาก function malloc แล้ว print ออกมาค่ะ โดย address ที่เรา print ออกมานั่นก็คือเป็น address ของช่วงที่อยู่ภายใน heap นั่นเอง(หมายเลข 1) รวมถึงในบรรทัดถัดไปมีการเรียกใช้ printf() ภายใน stack memory ก็จะมีการสร้าง stack frame สำหรับ function printf ด้วยเช่นกันค่ะ (หมายเลข 2)

 

เมื่อ infinite loop ทำงานรอบถัดไป function malloc จะทำการจองพื้นที่ใหม่ โดยพื้นที่ที่ถูกจองไว้ภายใน Heap อันเดิมนั้นก็จะยังคงอยู่ภายใน Heap ค่ะ

 

พื้นที่ภายใน Heap memory นั้นเราสามารถทำการจองไปเรื่อยๆ จนกว่าพื้นที่ภายในระบบไม่มีให้จองเลยค่ะ ทำให้พอ infinite loop ตัวนี้ทำงานไปเรื่อยๆ พื้นที่ก็จะถูกจองไปเรื่อยๆ ทำให้เกิดอาการ Memory Leak ค่ะ

Memory Leak เป็นอาการของโปรแกรมที่จองพื้นที่หน่วยความจำโดยที่ไม่มีการคืนพื้นที่หน่วยความจำไปเรื่อยๆ ทำให้ CPU โดนแย่งพื้นที่หน่วยความจำลงไปเรื่อยๆ ทำให้โปรแกรมหรือระบบทำงานได้ช้าลงจนระบบค้างไปในที่สุดค่ะ

เพราะฉะนั้น สำหรับใครจะลองนำโค้ดข้างบนไปลองรันเล่นๆ นั้นไม่ควรเอาไปลองเป็นอย่างยิ่งค่ะ อันตรายมากๆ

ดังนั้น เมื่อเราทำการจองพื้นที่หน่วยความจำมา เมื่อไม่ใช้แล้วเราควรที่จะคืนพื้นที่ให้กับระบบโดยใช้ function free() ค่ะ 

ดังนั้นโค้ดจริงๆ ที่เราควรจะเขียนเพื่อหลีกเลี่ยงการเกิด memory leak จะเป็นแบบนี้นั่นเองค่ะ

และในอีกเคสหนึ่ง ถ้าหากเราพยายามเรียกใช้ free บนพื้นที่หน่วยความจำที่กำลังใช้งานอยู่ก็ทำให้การคืนพื้นที่หน่วยความจำล้มเหลว แล้วนำไปสู่ปัญหา memory leak ได้เช่นเดียวกันค่ะ เพราะฉะนั้นก่อนที่จะทำการคืนพื้นที่ให้ CPU ควรต้องทำให้มั่นใจว่าไม่มีการใช้งานบนพื้นที่ตรงนั้นแล้วจริงๆ นั่นเอง

และอีกเหตุการณ์นึงที่ทำให้ memory leak ได้เช่นกัน นั่นก็คือการที่มีการเรียกใช้ function ซ้อน function ทำให้มีการสร้าง stack ซ้อนกันไปเรื่อยๆ บวกกับการที่จองพื้นที่ใน heap เพิ่มขึ้นไปเรื่อยๆ จน stack และ heap ชนกัน หรือที่เรียกว่า stack-heap collision ค่ะ

สรุป Stack vs Heap

ความแตกต่างระหว่าง Stack และ Heap สรุปได้คร่าวๆ ตามตารางนี้ค่ะ

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

ขอบคุณข้อมูลดีๆ จาก mycodeschool ค่ะ

Reference: YOUTUBE LINK