memory
แตกต่างจาก c/c++ แบบดั้งเดิม go เป็นภาษา gc โดยส่วนใหญ่แล้วการจัดสรรและการทำลายหน่วยความจำจะถูกจัดการโดย go โดยอัตโนมัติ การที่หน่วยความจำของออบเจกต์ควรจะถูกจัดสรรบน stack หรือ heap นั้นจะถูกกำหนดโดย compiler โดยพื้นฐานแล้วไม่จำเป็นต้องให้ผู้ใช้มีส่วนร่วมในการจัดการหน่วยความจำ สิ่งที่ผู้ใช้ต้องทำก็มีเพียงการใช้หน่วยความจำเท่านั้น ใน go การจัดการ heap memory มีองค์ประกอบหลักสองส่วน คือ memory allocator ทำหน้าที่จัดสรร heap memory และ garbage collector ทำหน้าที่回收และปลดปล่อย heap memory ที่ไม่มีประโยชน์ บทความนี้จะพูดถึงหลักการทำงานของ memory allocator เป็นหลัก go memory allocator ได้รับอิทธิพลอย่างมากจาก TCMalloc memory allocator ของ google
ตัวจัดสรร
ใน go มี memory allocator สองประเภท คือ linear allocator และ chain allocator
การจัดสรรแบบเชิงเส้น
Linear allocator สอดคล้องกับโครงสร้าง runtime.linearAlloc ดังที่แสดงด้านล่าง
type linearAlloc struct {
next uintptr // next free byte
mapped uintptr // one byte past end of mapped space
end uintptr // end of reserved space
mapMemory bool // transition memory from Reserved to Ready if true
}Allocator นี้จะขอพื้นที่หน่วยความจำต่อเนื่องจาก operating system ล่วงหน้า next ชี้ไปยังที่อยู่หน่วยความจำที่สามารถใช้งานได้ end ชี้ไปยังที่อยู่สุดท้ายของพื้นที่หน่วยความจำ โดยสามารถเข้าใจได้คร่าวๆ ตามภาพด้านล่าง

วิธีการจัดสรรหน่วยความจำของ linear allocator นั้นเข้าใจได้ง่ายมาก โดยตรวจสอบตามขนาดของหน่วยความจำที่ต้องการขอว่ามีพื้นที่เหลือเพียงพอที่จะรองรับหรือไม่ หากเพียงพอแล้วก็จะอัปเดตฟิลด์ next และคืนที่อยู่เริ่มต้นของพื้นที่ที่เหลือ โค้ดมีดังนี้
func (l *linearAlloc) alloc(size, align uintptr, sysStat *sysMemStat) unsafe.Pointer {
p := alignUp(l.next, align)
if p+size > l.end {
return nil
}
l.next = p + size
return unsafe.Pointer(p)
}ข้อดีของวิธีการจัดสรรแบบนี้คือรวดเร็วและง่าย แต่ข้อเสียก็ค่อนข้างชัดเจนเช่นกัน คือไม่สามารถนำหน่วยความจำที่ถูกปลดปล่อยแล้วกลับมาใช้ใหม่ได้ เนื่องจากฟิลด์ next จะชี้ไปยังที่อยู่หน่วยความจำที่เหลือเท่านั้น สำหรับพื้นที่หน่วยความจำที่ถูกใช้งานแล้วและถูกปลดปล่อยในภายหลังนั้นไม่สามารถรับรู้ได้ การทำเช่นนี้จะทำให้สูญเสียพื้นที่หน่วยความจำจำนวนมาก ดังที่แสดงในภาพด้านล่าง

ดังนั้น linear allocation จึงไม่ใช่วิธีการหลักใน go มันถูกใช้เฉพาะบนเครื่อง 32 บิตเป็นฟังก์ชันการจัดสรรหน่วยความจำล่วงหน้าเท่านั้น
การจัดสรรแบบโซ่
Chain allocator สอดคล้องกับโครงสร้าง runtime.fixalloc หน่วยความจำที่จัดสรรโดย chain allocator นั้นไม่ต่อเนื่อง มีอยู่ในรูปแบบของ singly linked list Chain allocator ประกอบด้วย memory block ขนาดคงที่จำนวนหนึ่ง และแต่ละ memory block ประกอบด้วย memory slice ขนาดคงที่จำนวนหนึ่ง ทุกครั้งที่ทำการจัดสรรหน่วยความจำ จะใช้ memory slice ขนาดคงที่หนึ่งหน่วย
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // called first time p is returned
arg unsafe.Pointer
list *mlink
chunk uintptr // use uintptr instead of unsafe.Pointer to avoid write barriers
nchunk uint32 // bytes remaining in current chunk
nalloc uint32 // size of new chunks in bytes
inuse uintptr // in-use bytes now
stat *sysMemStat
zero bool // zero allocations
}
type mlink struct {
_ sys.NotInHeap
next *mlink
}ฟิลด์ของมันไม่เข้าใจง่ายเหมือน linear allocator ที่นี่จะแนะนำฟิลด์สำคัญอย่างง่าย ๆ
sizeหมายถึงจำนวนหน่วยความจำที่ใช้ทุกครั้งในการจัดสรรหน่วยความจำlistชี้ไปยัง head node ของ memory slice ที่สามารถนำกลับมาใช้ใหม่ได้ ขนาดของพื้นที่หน่วยความจำแต่ละหน่วยถูกกำหนดโดยsizechunkชี้ไปยังที่อยู่空闲ของ memory block ที่กำลังใช้งานอยู่ปัจจุบันnchunkจำนวนไบต์ที่ใช้ได้เหลืออยู่ของ memory block ปัจจุบันnallocขนาดของ memory block ซึ่งคงที่ที่ 16KBinuseจำนวนไบต์ที่ถูกใช้งานทั้งหมดzeroว่าต้องล้างหน่วยความจำให้เป็นศูนย์หรือไม่เมื่อนำ memory block กลับมาใช้ใหม่
Chain allocator ถือการอ้างอิงไปยัง memory block ปัจจุบันและ memory slice ที่สามารถนำกลับมาใช้ใหม่ได้ ขนาดของแต่ละ memory block คงที่ที่ 16KB ค่านี้ถูกตั้งค่าไว้ในช่วงเริ่มต้น
const _FixAllocChunk = 16 << 10
func (f *fixalloc) init(size uintptr, first func(arg, p unsafe.Pointer), arg unsafe.Pointer, stat *sysMemStat) {
if size > _FixAllocChunk {
throw("runtime: fixalloc size too large")
}
if min := unsafe.Sizeof(mlink{}); size < min {
size = min
}
f.size = size
f.first = first
f.arg = arg
f.list = nil
f.chunk = 0
f.nchunk = 0
f.nalloc = uint32(_FixAllocChunk / size * size)
f.inuse = 0
f.stat = stat
f.zero = true
}การกระจายของ memory block แสดงดังภาพด้านล่าง ในภาพ memory block เรียงตามลำดับเวลาการสร้าง ที่จริงแล้วที่อยู่ของพวกมันไม่ต่อเนื่อง

ขนาดของหน่วยความจำที่ chain allocator จัดสรรในแต่ละครั้งก็คงที่เช่นกัน ถูกกำหนดโดย fixalloc.size ในการจัดสรรจะตรวจสอบก่อนว่ามี memory block ที่สามารถนำกลับมาใช้ใหม่ได้หรือไม่ หากมีก็จะใช้ memory block ที่นำกลับมาใช้ใหม่ก่อน จากนั้นจึงใช้ memory block ปัจจุบัน หากพื้นที่ที่เหลือของ memory block ปัจจุบันไม่เพียงพอที่จะรองรับ ก็จะสร้าง memory block ใหม่ โลจิกส่วนนี้สอดคล้องกับโค้ดด้านล่าง
func (f *fixalloc) alloc() unsafe.Pointer {
if f.size == 0 {
print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n")
throw("runtime: internal error")
}
if f.list != nil {
v := unsafe.Pointer(f.list)
f.list = f.list.next
f.inuse += f.size
if f.zero {
memclrNoHeapPointers(v, f.size)
}
return v
}
if uintptr(f.nchunk) < f.size {
f.chunk = uintptr(persistentalloc(uintptr(f.nalloc), 0, f.stat))
f.nchunk = f.nalloc
}
v := unsafe.Pointer(f.chunk)
if f.first != nil {
f.first(f.arg, v)
}
f.chunk = f.chunk + f.size
f.nchunk -= uint32(f.size)
f.inuse += f.size
return v
}ข้อดีของ chain allocator คือมันสามารถนำหน่วยความจำที่ถูกปลดปล่อยแล้วกลับมาใช้ใหม่ได้ หน่วยพื้นฐานของการนำหน่วยความจำกลับมาใช้ใหม่คือ memory slice ขนาดคงที่ ขนาดของมันถูกกำหนดโดย fixalloc.size ในการปลดปล่อยหน่วยความจำ chain allocator จะเพิ่ม memory slice นี้เป็น head node เข้าสู่ linked list ของ memory slice ว่าง โค้ดแสดงดังด้านล่าง
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}内存组件
Memory allocator ใน go ประกอบด้วยคอมโพเนนต์หลัก ๆ คือ mspan, heaparena, mcache, mcentral, mheap คอมโพเนนต์เหล่านี้ทำงานร่วมกันเป็นชั้น ๆ จัดการ heap memory ทั้งหมดของ go
mspan

runtime.mspan เป็นหน่วยพื้นฐานในการจัดสรรหน่วยความจำของ go โครงสร้างมีดังนี้
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
freeindex uintptr
spanclass spanClass // size class and noscan (uint8)
needzero uint8 // needs to be zeroed before allocation
elemsize uintptr // computed from sizeclass or from npages
limit uintptr // end of data in span
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
nelems uintptr // number of object in the span.
allocCache uint64
allocCount uint16 // number of allocated objects
...
}mspan กับ mspan เชื่อมโยงกันในรูปแบบ doubly linked list ผ่าน next และ prev ที่อยู่หน่วยความจำไม่ต่อเนื่อง แต่ละ mspan จัดการ page memory ขนาด runtime.pageSize จำนวน mspan.npages page โดยทั่วไปแล้วขนาดของ page คือ 8KB และถูกบันทึกที่อยู่เริ่มต้นของ page เหล่านี้โดย mspan.startAddr และบันทึกที่อยู่สุดท้ายของหน่วยความจำที่ใช้งานแล้วโดย mspan.limit ขนาดขององค์ประกอบ elemsize ที่แต่ละ mspan เก็บไว้นั้นคงที่ ดังนั้นจำนวนองค์ประกอบที่สามารถรองรับได้ก็คงที่เช่นกัน เนื่องจากจำนวนคงที่ การเก็บออบเจกต์จึงกระจายอยู่ใน mspan เหมือนกับ array ในช่วง [0, nelems] และบันทึกดัชนีถัดไปที่สามารถใช้เก็บออบเจกต์โดย freeindex mspan มีสามสถานะทั้งหมด
- mSpanDead หน่วยความจำถูกปลดปล่อยแล้ว
- mSpanInUse ถูกจัดสรรไปยัง heap แล้ว
- mSpanManual ถูกจัดสรรไปยังส่วนที่ใช้จัดการหน่วยความจำด้วยตนเอง เช่น stack
สิ่งที่กำหนดขนาดองค์ประกอบของ mspan คือ spanClass spanClass เองเป็นจำนวนเต็มประเภท uint8 เจ็ดบิตสูงเก็บค่า class ที่แสดง 0-67 บิตสุดท้ายใช้แสดง noscan คือว่ามี pointer หรือไม่
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}มันมีค่าที่แตกต่างกันทั้งหมด 68 ค่า ค่าทั้งหมดถูกเก็บไว้ในไฟล์ runtime.sizeclasses.go ในรูปแบบของตาราง ในระหว่างรันไทม์ ใช้ spanClass ผ่าน runtime.class_to_size สามารถได้รับขนาดออบเจกต์ของ mspan ผ่าน class_to_allocnpages สามารถได้รับจำนวน page ของ mspan
| class | ขนาดออบเจกต์สูงสุด | ขนาด span | จำนวนออบเจกต์ | ส่วนท้ายที่เสีย | อัตราการสูญเสียหน่วยความจำสูงสุด | การจัดตำแหน่งต่ำสุด |
|---|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% | 8 |
| 2 | 16 | 8192 | 512 | 0 | 43.75% | 16 |
| 3 | 24 | 8192 | 341 | 8 | 29.24% | 8 |
| 4 | 32 | 8192 | 256 | 0 | 21.88% | 32 |
| 5 | 48 | 8192 | 170 | 32 | 31.52% | 16 |
| 6 | 64 | 8192 | 128 | 0 | 23.44% | 64 |
| 7 | 80 | 8192 | 102 | 32 | 19.07% | 16 |
| 8 | 96 | 8192 | 85 | 32 | 15.95% | 32 |
| 9 | 112 | 8192 | 73 | 16 | 13.56% | 16 |
| 10 | 128 | 8192 | 64 | 0 | 11.72% | 128 |
| 11 | 144 | 8192 | 56 | 128 | 11.82% | 16 |
| 12 | 160 | 8192 | 51 | 32 | 9.73% | 32 |
| 13 | 176 | 8192 | 46 | 96 | 9.59% | 16 |
| 14 | 192 | 8192 | 42 | 128 | 9.25% | 64 |
| 15 | 208 | 8192 | 39 | 80 | 8.12% | 16 |
| 16 | 224 | 8192 | 36 | 128 | 8.15% | 32 |
| 17 | 240 | 8192 | 34 | 32 | 6.62% | 16 |
| 18 | 256 | 8192 | 32 | 0 | 5.86% | 256 |
| 19 | 288 | 8192 | 28 | 128 | 12.16% | 32 |
| 20 | 320 | 8192 | 25 | 192 | 11.80% | 64 |
| 21 | 352 | 8192 | 23 | 96 | 9.88% | 32 |
| 22 | 384 | 8192 | 21 | 128 | 9.51% | 128 |
| 23 | 416 | 8192 | 19 | 288 | 10.71% | 32 |
| 24 | 448 | 8192 | 18 | 128 | 8.37% | 64 |
| 25 | 480 | 8192 | 17 | 32 | 6.82% | 32 |
| 26 | 512 | 8192 | 16 | 0 | 6.05% | 512 |
| 27 | 576 | 8192 | 14 | 128 | 12.33% | 64 |
| 28 | 640 | 8192 | 12 | 512 | 15.48% | 128 |
| 29 | 704 | 8192 | 11 | 448 | 13.93% | 64 |
| 30 | 768 | 8192 | 10 | 512 | 13.94% | 256 |
| 31 | 896 | 8192 | 9 | 128 | 15.52% | 128 |
| 32 | 1024 | 8192 | 8 | 0 | 12.40% | 1024 |
| 33 | 1152 | 8192 | 7 | 128 | 12.41% | 128 |
| 34 | 1280 | 8192 | 6 | 512 | 15.55% | 256 |
| 35 | 1408 | 16384 | 11 | 896 | 14.00% | 128 |
| 36 | 1536 | 8192 | 5 | 512 | 14.00% | 512 |
| 37 | 1792 | 16384 | 9 | 256 | 15.57% | 256 |
| 38 | 2048 | 8192 | 4 | 0 | 12.45% | 2048 |
| 39 | 2304 | 16384 | 7 | 256 | 12.46% | 256 |
| 40 | 2688 | 8192 | 3 | 128 | 15.59% | 128 |
| 41 | 3072 | 24576 | 8 | 0 | 12.47% | 1024 |
| 42 | 3200 | 16384 | 5 | 384 | 6.22% | 128 |
| 43 | 3456 | 24576 | 7 | 384 | 8.83% | 128 |
| 44 | 4096 | 8192 | 2 | 0 | 15.60% | 4096 |
| 45 | 4864 | 24576 | 5 | 256 | 16.65% | 256 |
| 46 | 5376 | 16384 | 3 | 256 | 10.92% | 256 |
| 47 | 6144 | 24576 | 4 | 0 | 12.48% | 2048 |
| 48 | 6528 | 32768 | 5 | 128 | 6.23% | 128 |
| 49 | 6784 | 40960 | 6 | 256 | 4.36% | 128 |
| 50 | 6912 | 49152 | 7 | 768 | 3.37% | 256 |
| 51 | 8192 | 8192 | 1 | 0 | 15.61% | 8192 |
| 52 | 9472 | 57344 | 6 | 512 | 14.28% | 256 |
| 53 | 9728 | 49152 | 5 | 512 | 3.64% | 512 |
| 54 | 10240 | 40960 | 4 | 0 | 4.99% | 2048 |
| 55 | 10880 | 32768 | 3 | 128 | 6.24% | 128 |
| 56 | 12288 | 24576 | 2 | 0 | 11.45% | 4096 |
| 57 | 13568 | 40960 | 3 | 256 | 9.99% | 256 |
| 58 | 14336 | 57344 | 4 | 0 | 5.35% | 2048 |
| 59 | 16384 | 16384 | 1 | 0 | 12.49% | 8192 |
| 60 | 18432 | 73728 | 4 | 0 | 11.11% | 2048 |
| 61 | 19072 | 57344 | 3 | 128 | 3.57% | 128 |
| 62 | 20480 | 40960 | 2 | 0 | 6.87% | 4096 |
| 63 | 21760 | 65536 | 3 | 256 | 6.25% | 256 |
| 64 | 24576 | 24576 | 1 | 0 | 11.45% | 8192 |
| 65 | 27264 | 81920 | 3 | 128 | 10.00% | 128 |
| 66 | 28672 | 57344 | 2 | 0 | 4.91% | 4096 |
| 67 | 32768 | 32768 | 1 | 0 | 12.50% | 8192 |
เกี่ยวกับโลจิกการคำนวณของค่าเหล่านี้สามารถพบได้ในฟังก์ชัน printComment ของ runtime.mksizeclasses.go สูตรการคำนวณอัตราการสูญเสียหน่วยความจำสูงสุดคือ
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)ตัวอย่างเช่น เมื่อ class เป็น 2 อัตราการสูญเสียหน่วยความจำสูงสุดคือ
((16-8-1)*512+0)/8192 = 0.4375เมื่อค่า class เป็น 0 ก็คือ spanClass ที่ใช้สำหรับการจัดสรรออบเจกต์ขนาดใหญ่กว่า 32KB ขึ้นไปโดยเฉพาะ โดยพื้นฐานแล้วออบเจกต์ขนาดใหญ่หนึ่งออบเจกต์จะกินหนึ่ง mspan ดังนั้น heap memory ของ go จึงประกอบด้วย mspan linked list ขนาดคงที่ที่แตกต่างกันจำนวนหนึ่ง
heaparena

ก่อนหน้านี้ได้กล่าวไปแล้วว่า mspan ประกอบด้วย page จำนวนหนึ่ง แต่ mspan เพียงแค่ถือการอ้างอิงที่อยู่ของ page ไม่รับผิดชอบในการจัดการ page เหล่านี้ สิ่งที่รับผิดชอบในการจัดการ page memory เหล่านี้จริงๆ คือ runtime.heaparena แต่ละ heaparena จัดการ page จำนวนหนึ่ง ขนาดของ heaparena ถูกกำหนดโดย runtime.heapArenaBytes โดยปกติคือ 64MB bitmap ใช้สำหรับระบุว่าที่อยู่ใดใน page มีออบเจกต์อยู่หรือไม่ zeroedBase คือที่อยู่เริ่มต้นของ page memory ที่ heaparena นี้จัดการ และบันทึกโดย spans ว่าแต่ละ page ถูกใช้โดย mspan ใด
type heapArena struct {
_ sys.NotInHeap
bitmap [heapArenaBitmapWords]uintptr
noMorePtrs [heapArenaBitmapWords / 8]uint8
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
zeroedBase uintptr
}เกี่ยวกับโลจิกการบันทึก page กับ mspan สามารถพบได้ในเมธอด mheap.setSpans ดังที่แสดงด้านล่าง
func (h *mheap) setSpans(base, npage uintptr, s *mspan) {
p := base / pageSize
ai := arenaIndex(base)
ha := h.arenas[ai.l1()][ai.l2()]
for n := uintptr(0); n < npage; n++ {
i := (p + n) % pagesPerArena
if i == 0 {
ai = arenaIndex(base + n*pageSize)
ha = h.arenas[ai.l1()][ai.l2()]
}
ha.spans[i] = s
}
}ใน heap ของ go ถูกจัดการ page memory ทั้งหมดโดย array สองมิติของ heaparena ดูที่ฟิลด์ mheap.arenas
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}บนแพลตฟอร์ม windows 64 บิต array มีหนึ่งมิติ 1 << 6 สองมิติ 1 << 16 บนแพลตฟอร์ม linux 64 บิต หนึ่งมิติคือ 1 สองมิติคือ 1 << 22 array สองมิติที่ประกอบด้วย heaparena ทั้งหมดนี้ประกอบเป็น virtual memory space ของ go runtime โดยรวมแล้วแสดงดังภาพด้านล่าง

แม้ว่า heaparena จะอยู่ติดกัน แต่ page memory ที่พวกมันจัดการนั้นไม่ต่อเนื่องกัน
mcache
mcache สอดคล้องกับโครงสร้าง runtime.mcache ได้ปรากฏแล้วในบทความเกี่ยวกับ concurrent scheduling แม้ว่ามันจะชื่อว่า mcache แต่จริงๆ แล้วมันถูกผูกไว้กับ processor P mcache คือ memory cache บน processor P แต่ละตัว ซึ่งประกอบด้วย mspan linked list array alloc ขนาดของ array คงที่ 136 พอดีเป็นสองเท่าของจำนวน spanClass และมี tiny object cache tiny โดย tiny ชี้ไปยังที่อยู่เริ่มต้นของ tiny object memory tinyoffset คือ offset ของหน่วยความจำว่างเทียบกับที่อยู่เริ่มต้น tinyAllocs แสดงจำนวน tiny object ที่ถูกจัดสรร เกี่ยวกับ stack cache stackcached สามารถไปที่ 栈内存分配 เพื่อทำความเข้าใจ
type mcache struct {
_ sys.NotInHeap
nextSample uintptr // trigger heap sample after allocating this many bytes
scanAlloc uintptr // bytes of scannable heap allocated
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan
stackcache [_NumStackOrders]stackfreelist
flushGen atomic.Uint32
}ในช่วงเริ่มต้น alloc ใน mcache มี linked list ที่มีเพียง head node ว่าง runtime.emptymspan เท่านั้น ซึ่งก็คือ mspan ที่ไม่มีหน่วยความจำที่ใช้ได้
func allocmcache() *mcache {
var c *mcache
systemstack(func() {
lock(&mheap_.lock)
c = (*mcache)(mheap_.cachealloc.alloc())
c.flushGen.Store(mheap_.sweepgen)
unlock(&mheap_.lock)
})
for i := range c.alloc {
c.alloc[i] = &emptymspan
}
c.nextSample = nextSample()
return c
}เฉพาะเมื่อต้องการจัดสรรหน่วยความจำเท่านั้นที่จะขอ mspan ใหม่จาก mcentral เพื่อแทนที่ span ว่างเดิม งานส่วนนี้ทำโดยเมธอด mcache.refill ทางเข้าการเรียกเพียงหนึ่งเดียวของมันคือฟังก์ชัน runtime.mallocgc ด้านล่างคือโค้ดที่ลดรูปแล้ว
func (c *mcache) refill(spc spanClass) {
// Return the current cached span to the central lists.
s := c.alloc[spc]
// Get a new cached span from the central lists.
s = mheap_.central[spc].mcentral.cacheSpan()
if s == nil {
throw("out of memory")
}
c.scanAlloc = 0
c.alloc[spc] = s
}ข้อดีของการใช้ mcache คือไม่ต้องใช้ global lock ในการจัดสรรหน่วยความจำ แต่เมื่อหน่วยความจำไม่เพียงพอต้องเข้าถึง mcentral ซึ่งยังคงต้องเพิ่ม lock
mcentral
runtime.mcentral จัดการ mspan ทั้งหมดที่เก็บออบเจกต์ขนาดเล็กใน heap เมื่อ mcache ขอหน่วยความจำก็ถูกจัดสรรโดย mcentral
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}ฟิลด์ของ mcentral มีน้อย spanClass แสดงประเภทของ mspan ที่เก็บ partial และ full เป็น spanSet สองตัว ตัวแรกเก็บ mspan ที่มีหน่วยความจำว่าง ตัวหลังเก็บ mspan ที่ไม่มีหน่วยความจำว่าง mcentral ถูกจัดการโดยตรงโดย mheap heap ในระหว่างรันไทม์มี mcentral ทั้งหมด 136 ตัว
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral รับผิดชอบงานหลักสองอย่าง เมื่อหน่วยความจำเพียงพอจะจัดสรร mspan ที่ใช้ได้ให้กับ mcache เมื่อหน่วยความจำไม่เพียงพอจะขอจัดสรร mspan ใหม่จาก mheap งานจัดสรร mspan ให้กับ mcache ทำโดยเมธอด mcentral.cacheSpan ก่อนอื่นจะค้นหา mspan ที่ใช้ได้ใน swept set ของรายการว่าง
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}หากไม่พบ ก็ค้นหา mspan ที่ใช้ได้ใน unswept set ของรายการว่าง
for ; spanBudget >= 0; spanBudget-- {
s = c.partialUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true)
sweep.active.end(sl)
goto havespan
}
}หากยังไม่พบ ก็ไปค้นหาใน unswept set ของรายการไม่ว่าง
for ; spanBudget >= 0; spanBudget-- {
s = c.fullUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true)
freeIndex := s.nextFreeIndex()
if freeIndex != s.nelems {
s.freeindex = freeIndex
sweep.active.end(sl)
goto havespan
}
c.fullSwept(sg).push(s.mspan)
}
}หากสุดท้ายยังไม่พบ ก็จะขอจัดสรร mspan ใหม่จาก mheap โดยเมธอด mcentral.grow
s = c.grow()
if s == nil {
return nil
}ในสถานการณ์ปกติ ไม่ว่าอย่างไรก็จะคืน mspan ที่ใช้ได้หนึ่งตัว
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sสำหรับกระบวนการขอ mspan จาก mheap จริงๆ แล้วเรียกใช้เมธอด mheap.alloc เมธอดนี้จะคืน mspan ใหม่
func (c *mcentral) grow() *mspan {
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
s := mheap_.alloc(npages, c.spanclass)
if s == nil {
return nil
}
n := s.divideByElemSize(npages << _PageShift)
s.limit = s.base() + size*n
s.initHeapBits(false)
return s
}หลังจากเริ่มต้นมันให้ดีแล้วก็สามารถจัดสรรให้ mcache ใช้ได้
mheap

runtime.mheap เป็นผู้จัด การ heap memory ของภาษา go ในระหว่างรันไทม์มันมีอยู่เป็นตัวแปร global runtime.mheap_
var mheap_ mheapมันจัดการ mspan ทั้งหมดที่ถูกสร้าง mcentral ทั้งหมด และ heaparena ทั้งหมด และ allocator อื่น ๆ อีกมากมาย โครงสร้างที่ลดรูปมีดังนี้
type mheap struct {
_ sys.NotInHeap
lock mutex
allspans []*mspan // all spans out there
pagesInUse atomic.Uintptr // pages of spans in stats mSpanInUse
pagesSwept atomic.Uint64 // pages swept this cycle
pagesSweptBasis atomic.Uint64 // pagesSwept to use as the origin of the sweep ratio
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
allArenas []arenaIdx
sweepArenas []arenaIdx
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
pages pageAlloc // page allocation data structure
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
specialfinalizeralloc fixalloc // allocator for specialfinalizer*
specialprofilealloc fixalloc // allocator for specialprofile*
specialReachableAlloc fixalloc // allocator for specialReachable
specialPinCounterAlloc fixalloc // allocator for specialPinCounter
arenaHintAlloc fixalloc // allocator for arenaHints
}สำหรับ mheap แล้ว ในระหว่างรันไทม์มีงานหลักสี่อย่างที่ต้องทำ
- เริ่มต้น heap
- จัดสรร
mspan - ปลดปล่อย
mspan - ขยาย heap
ด้านล่างจะพูดถึงสี่เรื่องนี้ตามลำดับ
初始化
ช่วงเริ่มต้นของ heap อยู่ที่ช่วง boot ของโปรแกรม และเป็นช่วงเริ่มต้นของ scheduler เช่นกัน ลำดับการเรียกคือ
schedinit() -> mallocinit() -> mheap_.init()ในช่วงเริ่มต้น มันรับผิดชอบหลักในการดำเนินการงานเริ่มต้นของ allocator แต่ละตัว
func (h *mheap) init() {
h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
h.specialReachableAlloc.init(unsafe.Sizeof(specialReachable{}), nil, nil, &memstats.other_sys)
h.specialPinCounterAlloc.init(unsafe.Sizeof(specialPinCounter{}), nil, nil, &memstats.other_sys)
h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)
h.spanalloc.zero = false
for i := range h.central {
h.central[i].mcentral.init(spanClass(i))
}
h.pages.init(&h.lock, &memstats.gcMiscSys, false)
}ซึ่งรวมถึง allocator mheap.spanalloc ที่รับผิดชอบจัดสรร mspan และ allocator mheap.pages ที่รับผิดชอบจัดสรร page และการเริ่มต้น mcentral ทั้งหมด
分配
ใน mheap การจัดสรร mspan ทั้งหมดทำโดยเมธอด mheap.allocSpan
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)หากหน่วยความจำที่ขอจัดสรรมีขนาดเล็กพอ คือเป็นไปตาม npages < pageCachePages/4 ก็จะพยายามไม่เพิ่ม lock เพื่อรับ mspan ที่ใช้ได้จาก cache mspan ใน P ท้องถิ่น หาก cache ของ P ว่าง ก็จะเริ่มต้นก่อน
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}จากนั้นรับจาก cache ของ P ทำโดยเมธอด mheap.tryAllocMSpan
pp := gp.m.p.ptr()
if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 {
c := &pp.pcache
base, scav = c.alloc(npages)
if base != 0 {
s = h.tryAllocMSpan()
if s != nil {
goto HaveSpan
}
}
}โค้ดสำหรับรับ mspan จาก cache ของ P มีดังนี้ มันจะพยายามรับ mspan สุดท้ายใน cache
func (h *mheap) tryAllocMSpan() *mspan {
pp := getg().m.p.ptr()
// If we don't have a p or the cache is empty, we can't do
// anything here.
if pp == nil || pp.mspancache.len == 0 {
return nil
}
// Pull off the last entry in the cache.
s := pp.mspancache.buf[pp.mspancache.len-1]
pp.mspancache.len--
return s
}หากหน่วยความจำที่ขอมีขนาดใหญ่พอสมควร ก็จะจัดสรรหน่วยความจำบน heap กระบวนการนี้ต้องถือ lock
lock(&h.lock)
if base == 0 {
// Try to acquire a base address.
base, scav = h.pages.alloc(npages)
if base == 0 {
var ok bool
growth, ok = h.grow(npages)
if !ok {
unlock(&h.lock)
return nil
}
base, scav = h.pages.alloc(npages)
if base == 0 {
throw("grew heap, but no adequate free space found")
}
}
}
if s == nil {
// We failed to get an mspan earlier, so grab
// one now that we have the heap lock.
s = h.allocMSpanLocked()
}
unlock(&h.lock)ก่อนอื่นจะใช้ pageAlloc.alloc เพื่อจัดสรร page memory ให้เพียงพอ หาก heap memory ไม่เพียงพอ mheap.grow จะทำการขยาย หลังจากจัดสรร page memory เสร็จแล้ว chain allocator mheap.spanalloc จะจัดสรร mspan 64 ตัวไปยัง cache ท้องถิ่นของ P 64 พอดีเป็นครึ่งหนึ่งของความยาว array cache จากนั้นคืน mspan ที่ใช้ได้หนึ่งตัวจาก cache ของ P
func (h *mheap) allocMSpanLocked() *mspan {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
if pp == nil {
// We don't have a p so just do the normal thing.
return (*mspan)(h.spanalloc.alloc())
}
// Refill the cache if necessary.
if pp.mspancache.len == 0 {
const refillCount = len(pp.mspancache.buf) / 2
for i := 0; i < refillCount; i++ {
pp.mspancache.buf[i] = (*mspan)(h.spanalloc.alloc())
}
pp.mspancache.len = refillCount
}
// Pull off the last entry in the cache.
s := pp.mspancache.buf[pp.mspancache.len-1]
pp.mspancache.len--
return s
}ตามสองสถานการณ์ข้างต้น ในที่สุดก็สามารถได้รับ mspan ที่ใช้ได้หนึ่งตัว หลังจากเริ่มต้น mspan เสร็จแล้วก็สามารถคืนค่าได้
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return s释放
เนื่องจาก mspan ถูกจัดสรรโดย chain allocator เมื่อปลดปล่อยหน่วยความ自然也由它来进行释放。
func (h *mheap) freeSpanLocked(s *mspan, typ spanAllocType) {
assertLockHeld(&h.lock)
// Mark the space as free.
h.pages.free(s.base(), s.npages)
s.state.set(mSpanDead)
h.freeMSpanLocked(s)
}ก่อนอื่นจะ标记หน้าที่ระบุโดย page allocator mheap.pages ว่าถูกปลดปล่อยแล้ว จากนั้นตั้งค่าสถานะของ mspan เป็น mSpanDead สุดท้าย mspan ถูกปลดปล่อยโดย mheap.spanalloc allocator
func (h *mheap) freeMSpanLocked(s *mspan) {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
// First try to free the mspan directly to the cache.
if pp != nil && pp.mspancache.len < len(pp.mspancache.buf) {
pp.mspancache.buf[pp.mspancache.len] = s
pp.mspancache.len++
return
}
// Failing that (or if we don't have a p), just free it to
// the heap.
h.spanalloc.free(unsafe.Pointer(s))
}หาก cache ของ P ยังไม่เต็ม มันจะถูกใส่ลงใน cache ท้องถิ่นของ P เพื่อใช้ต่อ มิฉะนั้นมันจะถูกปลดปล่อยกลับไปยัง heap memory
扩容
Page memory space ที่ heaparena จัดการนั้นไม่ได้ถูกขอไว้ทั้งหมดในช่วงเริ่มต้น มีเฉพาะเมื่อต้องการใช้หน่วยความจำเท่านั้นจึงจะจัดสรร สิ่งที่รับผิดชอบในการขยาย heap memory คือเมธอด mheap.grow ด้านล่างคือโค้ดที่ลดรูปแล้ว
func (h *mheap) grow(npage uintptr) (uintptr, bool) {
assertLockHeld(&h.lock)
ask := alignUp(npage, pallocChunkPages) * pageSize
totalGrowth := uintptr(0)
end := h.curArena.base + ask
nBase := alignUp(end, physPageSize)
if nBase > h.curArena.end || end < h.curArena.base {
av, asize := h.sysAlloc(ask, &h.arenaHints, true)
if uintptr(av) == h.curArena.end {
h.curArena.end = uintptr(av) + asize
} else {
// Switch to the new space.
h.curArena.base = uintptr(av)
h.curArena.end = uintptr(av) + asize
}
nBase = alignUp(h.curArena.base+ask, physPageSize)
}
...
}มันคำนวณหน่วยความจำที่ต้องการตาม npage และทำการจัดตำแหน่งก่อน จากนั้นตรวจสอบว่า heaparena ปัจจุบันมีหน่วยความจำเพียงพอหรือไม่ หากไม่เพียงพอ mheap.sysAlloc จะขอหน่วยความจำเพิ่มเติมให้กับ heaparena ปัจจุบันหรือจัดสรร heaparena ใหม่
func (h *mheap) sysAlloc(n uintptr, hintList **arenaHint, register bool) (v unsafe.Pointer, size uintptr) {
n = alignUp(n, heapArenaBytes)
if hintList == &h.arenaHints {
v = h.arena.alloc(n, heapArenaBytes, &gcController.heapReleased)
if v != nil {
size = n
goto mapped
}
}
...
}ก่อนอื่นจะพยายามใช้ linear allocator mheap.arena ขอหน่วยความจำหนึ่งบล็อกในพื้นที่หน่วยความจำที่จัดสรรไว้ล่วงหน้า หากไม่สำเร็จก็จะขยายตาม hintList ประเภทของ hintList คือ runtime.arenaHint มันบันทึกข้อมูลที่อยู่สำหรับการขยาย heaparena โดยเฉพาะ
for *hintList != nil {
hint := *hintList
p := hint.addr
v = sysReserve(unsafe.Pointer(p), n)
if p == uintptr(v) {
hint.addr = p
size = n
break
}
if v != nil {
sysFreeOS(v, n)
}
*hintList = hint.next
h.arenaHintAlloc.free(unsafe.Pointer(hint))
}หลังจากขอหน่วยความจำเสร็จแล้ว ก็อัปเดตเป็น array สองมิติ arenas
for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
l2 := h.arenas[ri.l1()]
var r *heapArena
r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), goarch.PtrSize, &memstats.gcMiscSys))
atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
}สุดท้าย page allocator จะ标记พื้นที่หน่วยความจำนี้เป็นสถานะพร้อม
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - v对象分配
เมื่อ go จัดสรรหน่วยความจำสำหรับออบเจกต์ จะแบ่งออกเป็นสามประเภทตามขนาด
- Tiny object - tiny น้อยกว่า 16B
- Small object - small น้อยกว่า 32KB
- Large object - large มากกว่า 32KB
ตามสามประเภทที่แตกต่างกัน ในการจัดสรรหน่วยความจำจะดำเนินการโลจิกที่แตกต่างกัน ฟังก์ชันที่รับผิดชอบในการจัดสรรหน่วยความจำสำหรับออบเจกต์คือ runtime.mallocgc signature ของฟังก์ชันมีดังนี้
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointerมันมีพารามิเตอร์เพียงสามตัว ขนาดหน่วยความจำ ประเภท และค่าบูลีนหนึ่งตัวใช้表示ว่าต้องล้างหน่วยความจำให้เป็นศูนย์หรือไม่ มันเป็นฟังก์ชันทางเข้าสำหรับการจัดสรรหน่วยความจำออบเจกต์ go ทั้งหมด โดยปกติเมื่อใช้ฟังก์ชัน new สร้าง pointer ก็จะเข้าสู่ฟังก์ชันนี้เช่นกัน เมื่อจัดสรรหน่วยความจำสำเร็จ pointer ที่มันคืนมาก็คือที่อยู่ของออบเจกต์นั้น ในส่วน mspan ได้กล่าวไปแล้วว่า แต่ละ mspan มี spanClass หนึ่งตัว spanClass กำหนดขนาดคงที่ของ mspan และ go แบ่งออบเจกต์ในช่วง [0, 32KB] ออกเป็น 68 ขนาดที่แตกต่างกัน ดังนั้น go memory จึงประกอบด้วย mspan linked list ขนาดคงที่ที่แตกต่างกันจำนวนหนึ่ง ในการจัดสรรหน่วยความจำออบเจกต์ เพียงคำนวณ spanClass ที่สอดคล้องตามขนาดออบเจกต์ จากนั้นหา mspan linked list ที่สอดคล้องตาม spanClass สุดท้ายค้นหา mspan ที่ใช้ได้จาก linked list这种做法สามารถแก้ปัญหา memory fragmentation ได้อย่างมีประสิทธิภาพมากขึ้น

微对象
Tiny object ทั้งหมดที่น้อยกว่า 16B และไม่มี pointer จะถูกจัดสรรไปยังหน่วยความจำต่อเนื่องเดียวกันโดย tiny allocator ใน P ใน runtime.mcache บันทึกที่อยู่ฐานของหน่วยความจำนี้โดยฟิลด์ tiny
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}ขนาดของ tiny object ถูกกำหนดโดยค่าคงที่ runtime.maxTinySize ทั้งหมดคือ 16B หน่วยความจำบล็อกที่ใช้เก็บ tiny object ก็มีขนาดเท่านี้ โดยทั่วไปแล้วออบเจกต์ที่เก็บไว้ที่นี่都是一些สตริงขนาดเล็ก ส่วนที่รับผิดชอบในการจัดสรร tiny object มีโค้ดดังนี้
if size <= maxSmallSize {
if noscan && size < maxTinySize {
off := c.tinyoffset
if off+size <= maxTinySize && c.tiny != 0 {
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// Allocate a new maxTinySize block.
span = c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
if (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySizeหาก tiny memory block ปัจจุบันยังมีพื้นที่เพียงพอที่จะรองรับ ก็直接使用 current memory block นั่นคือ off+size <= maxTinySize หากไม่เพียงพอ ก็จะพยายามค้นหาพื้นที่ที่ใช้ได้จาก span cache ของ mcache ก่อน หากไม่ได้ก็จะขอ mspan หนึ่งตัวจาก mcentral ไม่ว่าอย่างไรในที่สุดก็จะได้รับที่อยู่ที่ใช้ได้หนึ่งตัว สุดท้ายใช้ tiny object memory block ใหม่แทนที่ของเก่า
小对象
ออบเจกต์ส่วนใหญ่ใน go runtime ล้วนเป็น small object ในช่วง [16B, 32KB] กระบวนการจัดสรร small object ยุ่งยากที่สุด แต่โค้ดกลับน้อยที่สุด ส่วนที่รับผิดชอบในการจัดสรร small object มีโค้ดดังนี้
var sizeclass uint8
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
span = c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(x, size)
}ก่อนอื่นจะคำนวณว่าควรใช้ spanClass ประเภทใดตามขนาดของออบเจกต์ จากนั้น runtime.nextFreeFast พยายามรับพื้นที่หน่วยความจำที่ใช้ได้จาก mspan cache ที่สอดคล้องใน mcache ตาม spanClass
func nextFreeFast(s *mspan) gclinkptr {
theBit := sys.TrailingZeros64(s.allocCache) // Is there a free object in the allocCache?
if theBit < 64 {
result := s.freeindex + uintptr(theBit)
if result < s.nelems {
freeidx := result + 1
if freeidx%64 == 0 && freeidx != s.nelems {
return 0
}
s.allocCache >>= uint(theBit + 1)
s.freeindex = freeidx
s.allocCount++
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}หน้าที่ของ mspan.allocCache คือบันทึกว่าพื้นที่หน่วยความจำมีออบเจกต์ใช้หรือไม่ และมันแบ่งหน่วยความจำทีละหน่วยตามจำนวนออบเจกต์而非ตามขนาดพื้นที่ ซึ่งเท่ากับการมอง mspan เป็น object array ดังที่แสดงในภาพ

allocCache เป็นตัวเลข 64 บิต แต่ละบิตสอดคล้องกับพื้นที่หน่วยความจำหนึ่งหน่วย หากบิตใดเป็น 0 แสดงว่ามีออบเจกต์ใช้แล้ว หากเป็น 1 แสดงว่าพื้นที่หน่วยความจำนี้ว่าง sys.TrailingZeros64(s.allocCache) มีวัตถุประสงค์เพื่อคำนวณจำนวน trailing zero หากผลลัพธ์เป็น 64 แสดงว่าไม่มีหน่วยความจำว่างให้ใช้ หากมีก็จะคำนวณ offset ของหน่วยความจำว่างบวกกับที่อยู่ฐานของ mspan แล้วคืนค่า
เมื่อไม่มีพื้นที่เพียงพอใน mcache ก็จะ再去 mcentral เพื่อขอ งานส่วนนี้ทำโดยเมธอด mcache.nextFree
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
s = c.alloc[spc]
shouldhelpgc = false
freeIndex := s.nextFreeIndex()
if freeIndex == s.nelems {
c.refill(spc)
shouldhelpgc = true
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
v = gclinkptr(freeIndex*s.elemsize + s.base())
s.allocCount++
return
}mcache.refill ในนั้นจะรับผิดชอบขอ mspan ที่ใช้ได้หนึ่งตัวจาก mcentral
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}และเมธอด mcentral.cacheSpan จะทำการขยายโดย mcentral.grow เมื่อหน่วยความจำไม่เพียงพอ การขยายก็จะไปขอ mspan ใหม่จาก mheap
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}ดังนั้นเมื่อมองในที่สุดแล้ว การจัดสรรหน่วยความจำ small object จะเดินลงทีละระดับ เริ่มจาก mcache จากนั้น mcentral สุดท้าย mheap ค่าใช้จ่ายในการจัดสรรของ mcache ต่ำที่สุด เพราะมันเป็น cache ท้องถิ่นของ P ไม่ต้องถือ lock เมื่อจัดสรรหน่วยความจำ mcentral รองลงมา การขอหน่วยความจำจาก mheap โดยตรงมีค่าใช้จ่ายสูงสุด เพราะเมธอด mheap.alloc จะแข่งขัน global lock ของ heap ทั้งหมด
大对象
การจัดสรร large object ง่ายที่สุด หากขนาดของออบเจกต์เกิน 32KB ก็จะ直接向 mheap ขอจัดสรร mspan ใหม่หนึ่งตัวเพื่อรองรับ ส่วนที่รับผิดชอบในการจัดสรร large object มีโค้ดดังนี้
shouldhelpgc = true
span = c.allocLarge(size, noscan)
span.freeindex = 1
span.allocCount = 1
size = span.elemsize
x = unsafe.Pointer(span.base())
if needzero && span.needzero != 0 {
if noscan {
delayedZeroing = true
} else {
memclrNoHeapPointers(x, size)
}
}โดย mcache.allocLarge รับผิดชอบขอพื้นที่หน่วยความจำ large object จาก mheap
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}จากโค้ดจะเห็นว่า spanClass ที่ large object ใช้มีค่าเป็น 0 โดยพื้นฐานแล้ว large object หนึ่งออบเจกต์กินหนึ่ง mspan
其它
内存统计
go runtime เปิดเผยฟังก์ชัน ReadMemStats ให้ผู้ใช้ ซึ่งสามารถใช้สำหรับสถิติสถานการณ์หน่วยความจำของ runtime
func ReadMemStats(m *MemStats) {
_ = m.Alloc // nil check test before we switch stacks, see issue 61158
stopTheWorld(stwReadMemStats)
systemstack(func() {
readmemstats_m(m)
})
startTheWorld()
}แต่ค่าใช้จ่ายในการใช้มันใหญ่มาก จากโค้ดจะเห็นว่าก่อนวิเคราะห์สถานการณ์หน่วยความจำต้อง STW และระยะเวลาของ STW อาจเป็นไม่กี่มิลลิวินาทีถึงหลายร้อยมิลลิวินาที โดยทั่วไปใช้เฉพาะเมื่อ debug และ troubleshoot เท่านั้น โครงสร้าง runtime.MemStats บันทึกข้อมูลเกี่ยวกับ heap memory, stack memory และ GC
type MemStats struct {
// 总体统计
Alloc uint64
TotalAlloc uint64
Sys uint64
Lookups uint64
Mallocs uint64
Frees uint64
// 堆内存统计
HeapAlloc uint64
HeapSys uint64
HeapIdle uint64
HeapInuse uint64
HeapReleased uint64
HeapObjects uint64
// 栈内存统计
StackInuse uint64
StackSys uint64
// 内存组件统计
MSpanInuse uint64
MSpanSys uint64
MCacheInuse uint64
MCacheSys uint64
BuckHashSys uint64
// gc 相关的统计
GCSys uint64
OtherSys uint64
NextGC uint64
LastGC uint64
PauseTotalNs uint64
PauseNs [256]uint64
PauseEnd [256]uint64
NumGC uint32
NumForcedGC uint32
GCCPUFraction float64
EnableGC bool
DebugGC bool
BySize [61]struct {
Size uint32
Mallocs uint64
Frees uint64
}
}NotInHeap
Memory allocator แน่นอนใช้สำหรับจัดสรร heap memory แต่ heap又被แบ่งเป็นสองส่วน ส่วนหนึ่งคือ heap memory ที่ go runtime เองต้องการ อีกส่วนหนึ่งคือ heap memory ที่เปิดให้ผู้ใช้ใช้ ดังนั้นในบางโครงสร้างสามารถเห็นฟิลด์ embedded แบบนี้
_ sys.NotInHeap表示ว่าหน่วยความจำของประเภทนั้นจะไม่ถูกจัดสรรบน user heap ฟิลด์ embedded แบบนี้พบได้บ่อยเป็นพิเศษในคอมโพเนนต์จัดสรรหน่วยความจำ เช่น โครงสร้าง runtime.mheap ที่แสดง user heap
type mheap struct {
_ sys.NotInHeap
}หน้าที่แท้จริงของ sys.NotInHeap คือเพื่อหลีกเลี่ยง memory barrier เพื่อเพิ่มประสิทธิภาพรันไทม์ และ user heap ต้องรัน GC จึงต้องการ memory barrier
