ใน Kotlin หรือ Java เมื่อคุณสร้าง Instance ของ Class ขึ้นมา:
val runner = Runner(bib = 101, checkpoint = 1)
สัญชาตญาณที่คุณคุ้นเคยคือ: ตัวแปรทุกตัวที่เป็น Object คือ Reference Type
Object Runner จะถูกจัดสรรลงบน JVM Heap เสมอ โดยมี Object Header ขนาด 12 ถึง 16 Bytes (Mark Word 8B เก็บสถานะ Lock/GC + Klass Word 4-8B ชี้ไปยัง Class Metadata) แปะหัวอยู่ข้างหน้าข้อมูลจริง
ตัวแปร runner ใน Stack Frame เก็บเพียง Pointer ขนาด 4 หรือ 8 Bytes ชี้ไปยังก้อน Object บน Heap และเมื่อเลิกใช้งาน มันจะกลายเป็นขยะ (Garbage) ที่ต้องรอให้ Garbage Collector (G1, ZGC) เข้ามาสแกนและตามกวาดล้าง
ในภาษา Go เมื่อคุณประกาศ Struct:
runner := RunnerRecord{Bib: 101, Checkpoint: 1}
runner จะถูกจัดสรรลงบน Stack Frame ของฟังก์ชันนั้นทันทีSUBQ $16, SP)| คุณสมบัติ | Stack Frame (Go Default) | Heap (JVM Default / Go Escape) |
|---|---|---|
| ต้นทุนเวลาการจอง (Allocation) | ~0 ns (แค่ขยับ CPU Stack Pointer) | ~20-50 ns (ค้นหา Free Chunk ใน Runtime) |
| การคืนพื้นที่ (Deallocation) | ทันทีเมื่อจบฟังก์ชัน (0 ns, Zero GC) | พึ่งพา Garbage Collector (Tricolor Marking) |
| CPU Cache Locality | สูงมาก (Contiguous Memory บน Stack) | เสี่ยง Cache Miss จากการกระโดดตาม Pointer |
| Metadata Header | 0 Byte (ไม่มี Header แปะหน้า Struct) | มี Runtime Allocation Metadata กำกับ |
ลองพิจารณาระบบเช็คพอยต์งานวิ่ง เมื่อนักวิ่งแตะชิป RFID ที่พรมจับเวลา การส่งข้อมูลระหว่างฟังก์ชันใน Go จะส่งผลต่อ Memory แตกต่างกันอย่างสิ้นเชิง 2 รูปแบบ:
type RunnerRecord struct {
Bib int64 // 8 Bytes
Checkpoint int64 // 8 Bytes
}
func UpdateCheckpoint(r RunnerRecord, newCP int64) {
r.Checkpoint = newCP // แก้ไขเฉพาะก้อน Copy บน Stack ชั่วคราว!
}
func UpdateCheckpointByPtr(r *RunnerRecord, newCP int64) {
r.Checkpoint = newCP // Dereference กลับไปแก้ไข RAM ต้นขั้วโดยตรง!
}
&struct
คำถามสำคัญ: ถ้า Go นิยมเก็บตัวแปรไว้บน Stack แล้วเมื่อไหร่ที่มันจะย้ายลง Heap?
คำตอบคือ: ขึ้นอยู่กับการวิเคราะห์ของ Compiler ในขั้นตอน Build เรียกว่า Escape Analysis
func NewRunnerRecord(bib int64) *RunnerRecord {
r := RunnerRecord{Bib: bib, Checkpoint: 0}
return &r // <--- ส่ง Memory Address ของตัวแปรท้องถิ่นออกไปข้างนอก!
}
ถ้าเป็นภาษา C โค้ดลักษณะนี้จะเกิดหายนะ Dangling Pointer ทันที เพราะ Stack Frame ถูกทำลาย แต่ Pointer ชี้ไปยังพื้นที่ที่ไม่มีอยู่จริง
ในภาษา Go ตัว Compiler จะตรวจจับการไหลของข้อมูล (Flow Analysis):
r ถูกสร้างขึ้นในฟังก์ชัน NewRunnerRecord&r ออกไปให้ Caller ด้านนอกNewRunnerRecordr ไปจองบน Heap ตั้งแต่วินาทีแรกผ่านคำสั่ง runtime.newobject เพื่อความปลอดภัยคุณสามารถใช้คำสั่งคอมไพเลอร์ของ Go เพื่อส่องดูผลลัพธ์ของ Escape Analysis ได้โดยตรง:
$ go build -gcflags="-m" ./cmd/timing/
# chiptiming/engine/cmd/timing
./main.go:12:2: moved to heap: r
./main.go:13:9: &r escapes to heap
เมื่อเห็นข้อความ moved to heap แสดงว่าตัวแปรนั้นไม่ได้อยู่บน Stack อีกต่อไป แต่ถูกส่งไปให้ Heap และ Garbage Collector ดูแลแทน
CheckpointPing ประกอบด้วย BibNumber int64, CheckpointID int64, และ Timestamp int64NewPing(bib int64, cp int64, ts int64) CheckpointPing ต้อง Return by Value 100% เพื่อให้ Struct อยู่บน Stack (ห้าม Return Pointer)UpdateCheckpoint(p *CheckpointPing, newCP int64) ต้องรับพารามิเตอร์เป็น Pointer เพื่อแก้ไขค่า CheckpointID บน Memory เดิมโดยตรงCalculateSplitPace(p CheckpointPing, distanceKm float64) float64 ต้องรับพารามิเตอร์แบบ Pass by Value (Read-only Calculation)