วันอังคารที่ 26 เมษายน พ.ศ. 2559

เรื่องของ เมโมรี่ Stack กับ Heap

เรื่องของ เมโมรี่ Stack กับ Heap

เรื่องของ เมโมรี่ Stack กับ Heap ต่างกันยังไง

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

Stack คืออะไร?

นั่นสิ Stack คืออะไร? มันคือพื้นที่พิเศษใน memory (หรือบางคนแรียก RAM ก็ไม่ผิดซะทีเดียว) บนคอมพิวเตอร์ของคุณ ที่จะเก็บค่าของตัวแปรที่ถูกสร้างไว้ในทุกๆ function (รวมถึง function main() ในภาษา C ด้วย) แบบชั่วคราว (ชั่วคราวหมายถึงเดี๋ยวค่าของตัวแปรก็หายไปได้)
Stack นั้นเป็นโครงสร้างข้อมูลที่เรียกว่า “FILO” (first in, last out) ที่ถูกจัดการด้วย CPU อย่างรัดกุมสุดๆ
ทุกๆตัวแปร และทุกๆครั้งที่ตัวแปรถูกประกาศขึ้นภายใต้ function จะถูกเสียบเข้าไปใน stack ตามลำดับการสร้าง
และเมื่อฟังชั่นทำงานจบ ทุกๆตัวแปรใน function นั้นจะถูกปล่อยให้เป็นอิสระ หรือ freed (ตามที่เราเข้าใจว่าตัวแปรถูกลบไป ถูกทำให้หายไป แต่จริงๆแล้วมันคือการปล่อยให้เป็นอิสระ) เมื่อตัวแปรใน stack ถูกปลดปล่อย พื้นที่บน memory ก็พร้อมที่จะให้ stack อื่นๆ ได้หยิบไปใช้งานต่อไป
ข้อได้เปรียบของระบบที่ใช้งาน stack ในการเก็บตัวแปรก็คือ memory จะถูกจัดการ โดยที่คุณไม่ต้องเหนื่อยมาก
เวลาจะใช้งานมันคุณก็ไม่ต้องมานั่งเขียนโค้ดจัดสรร หาพื้นที่ว่างบน memory หรือต้องคอยมานั่งลบตัวแปรที่ไม่ได้ใช้ด้วยตัวคุณเอง
และที่สำคัญกว่านั่นก็คือ เราบอกไปแล้วว่า CPU จะเป็นคนจัดการ memory ดังนั้นมันจึงมีประสิทธิภาพสูง การอ่าน และการบันทึกตัวแปรลงใน memory จึงมีความเร็วมากๆ
ถ้าจะหาวิธีจำการทำงานของ stack หละก็ ให้คิดว่า stack คือหลอดเมนทอส เมื่อ function สร้างตัวแปรขึ้นมา ตัวแปรก็จะถูกเสียบลงในหลอดเหมือนเมนทอสถูกยัดลงไปในหลอดทีละเม็ดๆ เรียงลำดับกันไป  และ เมื่อ function ทำงานจบ ตัวแปรที่เคยเสียบลงไปใน stack ทั้งหมด จะเด้งออกมาจากหลอด (และตัวแปรก็หายไปตลอดกาล)
ดังนั้นตัวแปรใน stack จะเป็นตัวแปร local โดยปริยาย
ซึ่งมันตรงกับคอนเซ็ปท์ของ variable scope ที่เราอาจได้เคยเรียนในหนังสือหลายๆเล่ม ในเนื้อหาของ ตัวแปรแบบ local vs ตัวแปรแบบ global
ซึ่งเป็น Bug ยอดนิยมสำหรับโปรแกรมเมอร์มือใหม่ทั้งหลายที่พยายามจะเข้าถึงตัวแปรบน stack ที่ได้ถูกประกาศใน function จากโค้ดบรรทัดที่อยู่นอก function นั้น
ลักษณะที่ควรจำไว้ให้ขึ้นใจของ stack ก็คือ มันมีขีดจำกัดของ size (หรือขนาด หน่วยเป็นไบท์) ของตัวแปรที่จะถูกบรรจุลงใน stack (ขึ้นอยู่กับ OS ของเครื่อง)
ข้อจำกัดนี้ไม่มีผลกับตัวแปรที่บันทึกลงในส่วนที่เรียกว่า Heap
สรุปเรื่องของ stack:
  • stack โตขึ้น ลดหดตัวลง ตามตัวแปรใน functions ที่เสียบเข้ามา และเด้งออกไป
  • คุณไม่จำเป็นต้องมานั่งเขียนโค้ดจัดการ memory เองเพราะตัวแปรจะสร้างและถูกปล่อยให้เป็นอิสระอัตโนมัติ
  • stack มีข้อจำกัดเรื่อง size ที่จะเก็บลงไป
  • ตัวแปรใน stack จะมีชีวิตอยู่ และถูกเรียกใช้ได้ภายใต้ function ที่สร้างมันขึ้นมา เมื่อจบฟังชั่น ตัวแปรจะหายไป

Heap คืออะไร?

heap คือพื้นที่ของ memory ในคอมพิวเตอร์ของคุณ ที่จะถูกจัดสรร/จัดการด้วย CPU อย่างคร่าวๆ ไม่รัดกุมสักเท่าไหร่ โดยพื้นที่ของ heap ส่วนใหญ่มันจะเป็นพื้นที่ว่างขนาดกว้างๆบน memory
ในการจองหน่วยความจำแบบ heap คุณต้องเรียกใช้งาน  malloc() หรือ calloc() ซึ่งเป็นฟังชั่นมาตรฐานใน ภาษา C  เมื่อใดก็ตามที่คุณใช้งานพื้นที่บน heap ในการเก็บข้อมูล คุณต้องแบกรับภาระในการเรียก free() เพื่อเลิกจับจอง หรือเลิกใช้งานพื้นที่บน heap เมื่อคุณไม่ต้องการใช้งานมันอีกต่อไป
ถ้าคุณลืมที่จะ free() หลังจากเลิกใช้งานมันแล้ว เราเรียกมันว่า memory leak ทำให้ memory บน heap ยังถูกเก็บข้อมูลขยะไว้ (และ process อื่นๆ ก็จะใช้งานมันไม่ได้)
โดยในภาษา C เราจะสามารถเห็นมันในส่วนของ debugging โดยใช้เครื่องมือที่เรียกว่า valgrind มันจะช่วยคุณตรวจสอบหาโค้ดที่ทำให้ เกิด memory leak
ต่างจาก stack โดยที่ heap จะไม่มีข้อจำกัดเรื่อง size ของตัวแปร (แต่อย่าลืมว่า มันไม่สามารถเก็บข้อมูลได้เกิน Hardware ที่เราใช้รันโค้ดอยู่) หน่วยความจำแบบ Heap จะช้ากว่า stack เพียงเล็กน้อย ในการอ่านและเขียน เพราะจริงๆแล้วเราใช้ pointers ในการเข้าถึง heap เดี๋ยวเราค่อยมาพูดถึงเรื่องนี้อีกที
ความต่างจาก stack อีกข้อหนึ่ง ตัวแปรที่ถูกสร้างบน heap จะสามารถเข้าถึงได้จากทุกๆ function และทุกๆที่บนโปรแกรมของคุณ ตัวแปรบน Heap มักจะเห็นว่าถูกใช้งานบน global scope

Stack vs Heap ข้อดี ข้อเสีย

Stack

  • เข้าถึงข้อมูลได้เร็วมาก
  • ไม่มีความจำเป็นต้อง สร้าง และลบตัวแปรแต่อย่างใด
  • พื้นที่บนหน่วยความจำ จะถูกจัดการอย่างรัดกุมด้วย CPU และพื้นที่บนหน่วยความจำ จะเป็นระเบียบ ไม่แบ่งเป็นส่วนๆแบบใน heap
  • เป็นตัวแปรในระกับ local เท่านั้น
  • มีข้อจำกัดเรื่อง size ของตัวแปร (ขึ้นอยู่กับระบบ OS)
  • ตัวแปรใน stack จะมี size คงที่ เพิ่มหรือลดไม่ได้

Heap

  • ตัวแปรเข้าถึงและใช้งานจากที่ไหนก็ได้ เป็นระดับ Global
  • ไม่มีข้อจำกัดเรื่อง size ของตัวแปร
  • เข้าถึงได้ช้ากว่า stack
  • ไม่การันตี ว่าจะเป็นการใช้งาน memory อย่างมีประสิทธิภาพ และมันจะถูกแยกเป็นส่วนๆ
    เพราะพื้นที่จะถูกจอง และปล่อยว่าง อยู่ตลอดเวลา เช่นเมื่อคุณต้องการพื้นที่ว่างสักที่ CPU จะหาตรงไหนก็ได้
    ที่ใหญ่พอจะบรรจุข้อมูลของคุณ
  • คุณจะต้องจัดการ จัดสรร memory เอง
  • ตัวแปรสามารถ resized ด้วยการเรียก realloc()

ตัวอย่าง

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

















#include <stdio.h>
 
double multiplyByTwo (double input) {
  double twice = input * 2.0;
  return twice;
}
 
int main (int argc, char *argv[])
{
  int age = 30;
  double salary = 12345.67;
  double myList[3] = {1.2, 2.3, 3.4};
 
  printf("double your salary is %.3f\n", multiplyByTwo(salary));
 
  return 0;
}
ผลลัพธ์
double your salary is 24691.340
บรรทัดที่ 10, 11 และ 12 เราได้สร้างตัวแปร int และ double และอาเรย์ของ doubles สามตัว
ตัวแปรเหล่านี้จะถูกเสียบลงใน stack ตามลำดับ ในตอนที่ฟังชั่น main() ได้สร้างมันขึ้นมา
และเมื่อ main() ทำงานจบลง  (โปรแกรมก็จะหยุดทำงานเช่นกัน) ตัวแปรเหล่านั้นจะถูกเด้งออกจาก stack คล้ายกันในฟังชั่น multiplyByTwo() ที่มีตัวแปร twice เป็นชนิด double ที่จะถูกเสียบลงใน stack เมื่อฟังชั่น multiplyByTwo() ได้สร้างตัวแปรขึ้นมา และต่อจากนั้น ทันทีที่ multiplyByTwo() ทำงานจบลง ตัวแปร twice จะถูกเด้งออกจาก stack และข้อมูลก็จะหายไปตลอดกาล
แต่ให้จำไว้ว่า หากต้องการเขียนภาษา C ให้เก็บตัวแปรไว้ แม้ function ที่สร้างตัวแปรนั้นทำงานจบไปแล้ว ให้เราใช้คีย์เวิร์ด static ในตอนประกาศตัวแปรนั้น
ตัวแปรใดที่ถูกประกาศพร้อมกับคีย์เวิร์ด static ตัวแปรนั้นจะหลายเป็นตัวแปรที่ คล้ายๆกับ global variable คือสามารถใช้งานได้นอกขอบเขตของ function เพียงแต่เมื่อ function ที่สร้างมันขึ้นมาทำงานจบแล้ว ตัวแปรนี้ก็จะถูกเด้งออกจาก stack แล้วหายไปอยู่ดี
มันอาจจะดูเป็นโครงสร้างที่แปลกๆสักหน่อย แต่ในบางสถานการณ์ คุณก็อาจจะได้ใช้ประโยชน์กับมัน
ข้างล่างนี้เป็นโค้ดอีกเวอร์ชั่นของโปรแกรมตัวอย่างแรกซึ่งเราจะมาลองใช้ heap แทน stack กันดู































#include <stdio.h>
#include <stdlib.h>
 
double *multiplyByTwo (double *input) {
   double *twice = malloc(sizeof(double));
   *twice = *input * 2.0;
   return twice;
}
 
int main (int argc, char *argv[])
{
   int *age = malloc(sizeof(int));
   *age = 30;
   double *salary = malloc(sizeof(double));
   *salary = 12345.67;
   double *myList = malloc(3 * sizeof(double));
   myList[0] = 1.2;
   myList[1] = 2.3;
   myList[2] = 3.4;
 
   double *twiceSalary = multiplyByTwo(salary);
 
   printf("double your salary is %.3f\n", *twiceSalary);
 
   free(age);
   free(salary);
   free(myList);
   free(twiceSalary);
 
   return 0;
}
ตามโค้ดข้างบนจะใช้ malloc() เพื่อจอง memory บน heap และจากนั้นก็ใช้ free() เมื่อไม่ต้องการใช้ตัวแปรอีกแล้ว จะเห็นว่ามันไม่ใช่เรื่องยากอะไรเลย แต่มันเป็นภาระที่ต้อง free() หลังจากเลิกใช้ตัวแปรแล้ว
ให้สังเกตุอีกข้อหนี่งคือจะมีเครื่องหมายดอกจันทน์ * ตามตัวแปรไปทุกๆจุด ถ้าสงสัยว่ามันคืออะไร? คำตอบคือ มันเป็นเครื่องหมายของ pointers
โดยคำสั่ง malloc() (และ calloc() กับ free() ) เป็นการทำงานกับ pointers เสมอ ไม่ใช่การทำงานกับค่าตัวแปร เหมือนตัวแปร type พื้นฐานทั่วไป เราจะพูดถึงเรื่อง pointers ในบทอื่น
ให้อธิบายคร่าวๆคือ pointers เป็นตัวแปรชนิดพิเศษในภาษา C ที่จะเก็บ addresses ของ memory แทนที่จะเก็บค่าตัวตัวแปรตรงๆ แบบตัวแปรชนิดอื่น ให้สังเกตุบรรทัดที่ 5 จากตัวอย่างข้างบน ตัวแปร twice ไม่ได้เป็น double  แต่มันเป็น pointer ที่เก็บตำแหน่ง หรือชี้เป้าไปยังของข้อมูลที่เป็น double ซึ่งข้อมูลที่  pointer ตัวนี้เก็บไว้ก็คือ ตัวเลขที่อยู่ (หรือ Address) ของ memory ที่ข้อมูล double ได้บันทึกไว้

เราควรใช้ Heap ตอนไหน?

คำถามคือคุณควรใช้ heap ตอนไหน และใช้ stack ตอนไหน?
ก็ถ้าเมื่อคุณต้องการใช้ memory ขนาดใหญ่ๆ เพื่อเก็บข้อมูลใหญ่ๆ (เช่น array ที่มีสมาชิกเยอะๆ หรือ struct ที่เก็บข้อมูลมหาศาล) และเมื่อคุณต้องการให้ตัวแปรนั้น คงอยู่ในระบบนานๆ (เหมือน global veriable) ไม่หายไปหลังจบ function นั่นแหละที่คุณควรใช้ heap
และถ้าคุณต้องการใช้งานตัวแปรเล็กๆ และใช้งานภายใต้ function เฉยๆ คุณก็ควรจะใช้งาน stack ถูกไหม เพราะนอกจากจะตอบโจทย์ที่ต้องการแล้ว อย่าลืมว่า stack มันง่าย และเร็ว ไม่มี memory leak อีกด้วย
หรือถ้าคุณใช้ตัวแปรอย่าง arrays หรือ structs ที่สามารถ resize ได้ตลอดเวลาแบบ dynamic (ก็เช่น arrays ที่เพิ่ม และลดจำนวนเมื่อไหร่ก็ได้ที่ต้องการ)  คุณก็ต้องใช้ heap อยู่แล้ว
และคุณก็ต้องเรียกใช้ malloc(), calloc(), realloc() และ free() เพื่อจัดการ จัดสรรหน่วยความจำให้ดี ไม่ให้เกิดตัวแปรขยะสะสมในระบบ
เดี๋ยวเราจะมีการพูดถึงการประกาศตัวแปรแบบ dynamically หลังจากบทของ pointers

ไม่มีความคิดเห็น:

แสดงความคิดเห็น

Set MongoDB in the windows path environment

  Let’s set MongoDB in the windows environment in just a few steps. Step 1: First download a suitable MongoDB version according to your mach...