Skip to content

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 ดังที่แสดงด้านล่าง

go
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 และคืนที่อยู่เริ่มต้นของพื้นที่ที่เหลือ โค้ดมีดังนี้

go
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 ขนาดคงที่หนึ่งหน่วย

go
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 ที่สามารถนำกลับมาใช้ใหม่ได้ ขนาดของพื้นที่หน่วยความจำแต่ละหน่วยถูกกำหนดโดย size
  • chunk ชี้ไปยังที่อยู่空闲ของ memory block ที่กำลังใช้งานอยู่ปัจจุบัน
  • nchunk จำนวนไบต์ที่ใช้ได้เหลืออยู่ของ memory block ปัจจุบัน
  • nalloc ขนาดของ memory block ซึ่งคงที่ที่ 16KB
  • inuse จำนวนไบต์ที่ถูกใช้งานทั้งหมด
  • zero ว่าต้องล้างหน่วยความจำให้เป็นศูนย์หรือไม่เมื่อนำ memory block กลับมาใช้ใหม่

Chain allocator ถือการอ้างอิงไปยัง memory block ปัจจุบันและ memory slice ที่สามารถนำกลับมาใช้ใหม่ได้ ขนาดของแต่ละ memory block คงที่ที่ 16KB ค่านี้ถูกตั้งค่าไว้ในช่วงเริ่มต้น

go
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 ใหม่ โลจิกส่วนนี้สอดคล้องกับโค้ดด้านล่าง

go
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 ว่าง โค้ดแสดงดังด้านล่าง

go
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 โครงสร้างมีดังนี้

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 หรือไม่

go
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จำนวนออบเจกต์ส่วนท้ายที่เสียอัตราการสูญเสียหน่วยความจำสูงสุดการจัดตำแหน่งต่ำสุด
1881921024087.50%8
2168192512043.75%16
3248192341829.24%8
4328192256021.88%32
54881921703231.52%16
6648192128023.44%64
78081921023219.07%16
8968192853215.95%32
91128192731613.56%16
10128819264011.72%128
1114481925612811.82%16
12160819251329.73%32
13176819246969.59%16
141928192421289.25%64
15208819239808.12%16
162248192361288.15%32
17240819234326.62%16
1825681923205.86%256
1928881922812812.16%32
2032081922519211.80%64
21352819223969.88%32
223848192211289.51%128
2341681921928810.71%32
244488192181288.37%64
25480819217326.82%32
2651281921606.05%512
2757681921412812.33%64
2864081921251215.48%128
2970481921144813.93%64
3076881921051213.94%256
318968192912815.52%128
32102481928012.40%1024
3311528192712812.41%128
3412808192651215.55%256
351408163841189614.00%128
3615368192551214.00%512
37179216384925615.57%256
38204881924012.45%2048
39230416384725612.46%256
4026888192312815.59%128
413072245768012.47%1024
4232001638453846.22%128
4334562457673848.83%128
44409681922015.60%4096
45486424576525616.65%256
46537616384325610.92%256
476144245764012.48%2048
4865283276851286.23%128
4967844096062564.36%128
5069124915277683.37%256
51819281921015.61%8192
52947257344651214.28%256
5397284915255123.64%512
541024040960404.99%2048
55108803276831286.24%128
5612288245762011.45%4096
57135684096032569.99%256
581433657344405.35%2048
5916384163841012.49%8192
6018432737284011.11%2048
61190725734431283.57%128
622048040960206.87%4096
63217606553632566.25%256
6424576245761011.45%8192
652726481920312810.00%128
662867257344204.91%4096
6732768327681012.50%8192

เกี่ยวกับโลจิกการคำนวณของค่าเหล่านี้สามารถพบได้ในฟังก์ชัน printComment ของ runtime.mksizeclasses.go สูตรการคำนวณอัตราการสูญเสียหน่วยความจำสูงสุดคือ

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 ใด

go
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 ดังที่แสดงด้านล่าง

go
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

go
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 สามารถไปที่ 栈内存分配 เพื่อทำความเข้าใจ

go
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 ที่ไม่มีหน่วยความจำที่ใช้ได้

go
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 ด้านล่างคือโค้ดที่ลดรูปแล้ว

go
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

go
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 ตัว

go
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 ของรายการว่าง

go
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
    goto havespan
}

หากไม่พบ ก็ค้นหา mspan ที่ใช้ได้ใน unswept set ของรายการว่าง

go
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 ของรายการไม่ว่าง

go
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

go
s = c.grow()
if s == nil {
    return nil
}

ในสถานการณ์ปกติ ไม่ว่าอย่างไรก็จะคืน mspan ที่ใช้ได้หนึ่งตัว

go
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 ใหม่

go
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_

go
var mheap_ mheap

มันจัดการ mspan ทั้งหมดที่ถูกสร้าง mcentral ทั้งหมด และ heaparena ทั้งหมด และ allocator อื่น ๆ อีกมากมาย โครงสร้างที่ลดรูปมีดังนี้

go
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 เช่นกัน ลำดับการเรียกคือ

go
schedinit() -> mallocinit() -> mheap_.init()

ในช่วงเริ่มต้น มันรับผิดชอบหลักในการดำเนินการงานเริ่มต้นของ allocator แต่ละตัว

go
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

go
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)

หากหน่วยความจำที่ขอจัดสรรมีขนาดเล็กพอ คือเป็นไปตาม npages < pageCachePages/4 ก็จะพยายามไม่เพิ่ม lock เพื่อรับ mspan ที่ใช้ได้จาก cache mspan ใน P ท้องถิ่น หาก cache ของ P ว่าง ก็จะเริ่มต้นก่อน

go
// If the cache is empty, refill it.
if c.empty() {
    lock(&h.lock)
    *c = h.pages.allocToCache()
    unlock(&h.lock)
}

จากนั้นรับจาก cache ของ P ทำโดยเมธอด mheap.tryAllocMSpan

go
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

go
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

go
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

go
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 เสร็จแล้วก็สามารถคืนค่าได้

go
HaveSpan:
  h.initSpan(s, typ, spanclass, base, npages)
  return s

释放

เนื่องจาก mspan ถูกจัดสรรโดย chain allocator เมื่อปลดปล่อยหน่วยความ自然也由它来进行释放。

go
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

go
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 ด้านล่างคือโค้ดที่ลดรูปแล้ว

go
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 ใหม่

go
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 โดยเฉพาะ

go
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

go
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 จะ标记พื้นที่หน่วยความจำนี้เป็นสถานะพร้อม

go
// 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 ของฟังก์ชันมีดังนี้

go
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

go
type mcache struct {
  tiny       uintptr
  tinyoffset uintptr
  tinyAllocs uintptr
}

ขนาดของ tiny object ถูกกำหนดโดยค่าคงที่ runtime.maxTinySize ทั้งหมดคือ 16B หน่วยความจำบล็อกที่ใช้เก็บ tiny object ก็มีขนาดเท่านี้ โดยทั่วไปแล้วออบเจกต์ที่เก็บไว้ที่นี่都是一些สตริงขนาดเล็ก ส่วนที่รับผิดชอบในการจัดสรร tiny object มีโค้ดดังนี้

go
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 มีโค้ดดังนี้

go
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

go
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

go
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

go
func (c *mcache) refill(spc spanClass) {
  ...
  s = mheap_.central[spc].mcentral.cacheSpan()
  ...
}

และเมธอด mcentral.cacheSpan จะทำการขยายโดย mcentral.grow เมื่อหน่วยความจำไม่เพียงพอ การขยายก็จะไปขอ mspan ใหม่จาก mheap

go
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 มีโค้ดดังนี้

go
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

go
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

go
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

go
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 แบบนี้

go
_ sys.NotInHeap

表示ว่าหน่วยความจำของประเภทนั้นจะไม่ถูกจัดสรรบน user heap ฟิลด์ embedded แบบนี้พบได้บ่อยเป็นพิเศษในคอมโพเนนต์จัดสรรหน่วยความจำ เช่น โครงสร้าง runtime.mheap ที่แสดง user heap

go
type mheap struct {
  _ sys.NotInHeap
}

หน้าที่แท้จริงของ sys.NotInHeap คือเพื่อหลีกเลี่ยง memory barrier เพื่อเพิ่มประสิทธิภาพรันไทม์ และ user heap ต้องรัน GC จึงต้องการ memory barrier

Golang by www.golangdev.cn edit