memory
على عكس c/c++ التقليدية، go هي لغة ذات GC، في معظم الحالات تتم إدارة تخصيص وتحرير الذاكرة تلقائيًا بواسطة go، ما إذا كانت ذاكرة الكائن يجب تخصيصها على المكدس أم الكومة يقرره المترجم، ولا يحتاج المستخدم للمشاركة في إدارة الذاكرة تقريبًا، كل ما على المستخدم فعله هو استخدام الذاكرة. في go، تتكون إدارة ذاكرة الكومة بشكل رئيسي من مكونين كبيرين: مخصص الذاكرة المسؤول عن تخصيص ذاكرة الكومة، وجامع القمامة المسؤول عن استعادة وتحرير ذاكرة الكومة غير المستخدمة. يتحدث هذا المقال بشكل رئيسي عن طريقة عمل مخصص الذاكرة، تأثر مخصص ذاكرة go بشكل كبير بمخصص ذاكرة TCMalloc من جوجل.
المخصص
في go هناك نوعان من مخصصات الذاكرة، أحدهما المخصص الخطي، والآخر هو المخصص السلسلي.
التخصيص الخطي
المخصص الخطي يقابله البنية 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
}هذا المخصص يطلب مسبقًا من نظام التشغيل مساحة ذاكرة متصلة، next يشير لعنوان الذاكرة المتاحة، end يشير لعنوان نهاية مساحة الذاكرة، ويمكن فهمه تقريبًا كما في الشكل التالي.

طريقة تخصيص الذاكرة للمخصص الخطي سهلة الفهم جدًا، حسب حجم الذاكرة المطلوب يُفحص هل هناك مساحة كافية للاستيعاب، إذا كانت كافية يُحدث حقل 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 يشير فقط لعنوان الذاكرة المتبقية، ولا يستطيع الإحساس بمساحة الذاكرة التي استُخدمت سابقًا ثم حُررت، وهذا يسبب هدرًا كبيرًا في مساحة الذاكرة، كما في الشكل التالي.

لذا التخصيص الخطي ليس طريقة التخصيص الرئيسية في go، يُستخدم فقط في الأجهزة 32-bit كوظيفة للتخصيص المسبق للذاكرة.
التخصيص السلسلي
المخصص السلسلي يقابله البنية runtime.fixalloc، الذاكرة التي يخصصها المخصص السلسلي غير متصلة، وتوجد على شكل قائمة أحادية. يتكون المخصص السلسلي من عدة كتل ذاكرة بحجم ثابت، وكل كتلة ذاكرة تتكون من عدة شرائح ذاكرة بحجم ثابت، عند كل عملية تخصيص ذاكرة، تُستخدم شريحة ذاكرة بحجم ثابت.
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
}حقوله ليست بسيطة وسهلة الفهم مثل المخصص الخطي، إليك شرحًا بسيطًا للحقول المهمة:
size، يشير لكمية الذاكرة المستخدمة عند كل تخصيص ذاكرة.list، يشير لعقدة رأس شرائح الذاكرة القابلة لإعادة الاستخدام، حجم كل مساحة ذاكرة يحددهsize.chunk، يشير للعنوان الحر في كتلة الذاكرة الحالية قيد الاستخدامnchunk، عدد البايتات المتاحة المتبقية في كتلة الذاكرة الحاليةnalloc، حجم كتلة الذاكرة، ثابت عند 16KB.inuse، إجمالي البايتات المستخدمة من الذاكرةzero، عند إعادة استخدام كتلة الذاكرة، هل يتم تصفير الذاكرة
المخصص السلسلي يحتفظ بمرجع لكتلة الذاكرة الحالية وشرائح الذاكرة القابلة لإعادة الاستخدام، حجم كل كتلة ذاكرة ثابت عند 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
}توزيع كتل الذاكرة كما في الشكل التالي، كتل الذاكرة في الصورة مرتبة حسب وقت الإنشاء، في الواقع عناوينها غير متصلة.

حجم الذاكرة الذي يخصصه المخصص السلسلي في كل مرة ثابت أيضًا، يحدده fixalloc.size، عند التخصيص يُفحص أولاً هل هناك كتل ذاكرة قابلة لإعادة الاستخدام، إذا وُجدت تُستخدم أولًا، ثم تُستخدم كتلة الذاكرة الحالية، إذا كانت المساحة المتبقية في كتلة الذاكرة الحالية لا تكفي تُنشأ كتلة ذاكرة جديدة، هذا المنطق مقابل الكود التالي.
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
}ميزة المخصص السلسلي هي قدرته على إعادة استخدام الذاكرة المحررة، الوحدة الأساسية لإعادة استخدام الذاكرة هي شريحة ذاكرة بحجم ثابت، حجمها يحدده fixalloc.size، عند تحرير الذاكرة، يضيف المخصص السلسلي هذه الشريحة كعقدة رأس لقائمة شرائح الذاكرة الحرة، الكود كالتالي
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}مكونات الذاكرة
مخصص الذاكرة في go يتكون بشكل رئيسي من المكونات mspan، heaparena، mcache، mcentral، mheap، وهي تعمل معًا طبقة تلو الأخرى، وتدير كامل ذاكرة الكومة في 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 على شكل قائمة ثنائية الاتجاه عبر next و prev، وعناوين الذاكرة غير متصلة. كل mspan يدير mspan.npages صفحة ذاكرة بحجم runtime.pageSize، عادةً حجم الصفحة هو 8KB، ويُسجل بواسطة mspan.startAddr عنوان بداية هذه الصفحات وبواسطة mspan.limit عنوان نهاية الذاكرة المستخدمة. حجم العناصر التي يخزنها كل mspan elemsize ثابت، لذا عدد العناصر التي يمكن استيعابها ثابت أيضًا. وبسبب ثبات العدد، تخزين الكائنات يشبه توزيع المصفوفة في mspan، النطاق [0, nelems]، ويُسجل بواسطة freeindex الفهرس التالي المتاح لتخزين كائن. لـ mspan ثلاث حالات إجمالًا:
- mSpanDead، الذاكرة حُررت
- mSpanInUse، خُصصت على الكومة
- mSpanManual، خُصصت لجزء يُدار يدويًا، مثل المكدس.
ما يحدد حجم عناصر mspan هو spanClass، spanClass نفسه عدد صحيح من النوع uint8، البتات السبع العليا تخزن قيمة class من 0-67، والبت الأخير يُستخدم للدلالة على noscan أي هل يحتوي على مؤشرات.
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 يمكن الحصول على حجم كائن mspan عبر runtime.class_to_size، والحصول على عدد صفحات mspan عبر class_to_allocnpages.
| 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 واحدًا. إذن، ذاكرة كومة go تتكون فعليًا من عدة mspan بأحجام ثابتة مختلفة.
heaparena

ذكرنا سابقًا أن mspan يتكون من عدة صفحات، لكن mspan يحتفظ فقط بمرجع عنوان الصفحات، ولا يدير هذه الصفحات، المسؤول الحقيقي عن إدارة ذاكرة هذه الصفحات هو runtime.heaparena. كل heaparena يدير عدة صفحات، حجم heaparena يحدده runtime.heapArenaBytes، عادةً 64MB. bitmap يُستخدم لتحديد هل العنوان المقابل في الصفحة يخزن كائنًا، zeroedBase هو عنوان بداية ذاكرة الصفحات التي يديرها heaparena، ويُسجل بواسطة spans كل صفحة يستخدمها أي 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
}يمكن العثور على منطق تسجيل الصفحات و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
}
}في كومة go، مصفوفة heaparena ثنائية الأبعاد تدير كل ذاكرة الصفحات، انظر حقل mheap.arenas.
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}على منصة windows 64-bit، البعد الأول للمصفوفة هو 1 << 6، والبعد الثاني هو 1 << 16، على منصة linux 64-bit، البعد الأول هو 1، والبعد الثاني هو 1 << 22. هذه المصفوفة ثنائية الأبعاد المكونة من كل heaparena تشكل مساحة الذاكرة الافتراضية لبيئة تشغيل go، بشكل عام كما في الشكل التالي.

رغم أن heaparena متجاورة، إلا أن ذاكرة الصفحات التي تديرها غير متصلة.
mcache
mcache يقابله البنية runtime.mcache، ظهرت بالفعل في مقال الجدولة المتزامنة، رغم أن اسمها mcache لكنها في الواقع مرتبطة بالمعالج P. mcache هو مخبأ الذاكرة على كل معالج P، يحتوي على مصفوفة قوائم mspan المسماة alloc، حجم المصفوفة ثابت عند 136، وهو ضعف عدد spanClass بالضبط، ومخبأ الكائنات الدقيقة tiny، حيث tiny يشير لعنوان بداية ذاكرة الكائنات الدقيقة، وtinyoffset هو الإزاحة بالنسبة لعنوان البداية للذاكرة الحرة، وtinyAllocs ي表示 عدد الكائنات الدقيقة المخصصة. لمخبأ المكدس 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 تحتوي فقط على عقدة رأس فارغة 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 أنه لا يحتاج قفل عام عند تخصيص الذاكرة، لكن عندما تنفد ذاكرته يحتاج للوصول لـ mcentral، وهنا لا يزال يحتاج للقفل.
mcentral
runtime.mcentral يدير كل mspan التي تخزن كائنات صغيرة في الكومة، عند طلب 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، في وقت التشغيل يوجد 136 mcentral إجمالًا.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral مسؤول بشكل رئيسي عن عملين، عندما تكون الذاكرة كافية يخصص mspan متاح لـ mcache، عندما لا تكفي الذاكرة يطلب من mheap تخصيص mspan جديد. عمل تخصيص mspan لـ mcache تُنجزه طريقة mcentral.cacheSpan. أولًا يبحث عن mspan متاح في مجموعة الممسوحة في القائمة الحرة.
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}إذا لم يجد، يبحث عن mspan متاح في مجموعة غير الممسوحة في القائمة الحرة
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
}
}إذا لم يجد أيضًا، يذهب للبحث في مجموعة غير الممسوحة في القائمة غير الحرة
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)
}
}إذا لم يجد في النهاية، طريقة mcentral.grow تطلب من mheap تخصيص mspan جديد.
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

runtimme.mheap هو مدير ذاكرة الكومة في لغة go، في وقت التشغيل يوجد كمتغير عام runtime.mheap_.
var mheap_ mheapيدير كل mspan المُنشأ، وكل mcentral، وكل heaparena، والعديد من المخصصات المختلفة الأخرى، بنيته المبسطة كالتالي
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، في وقت التشغيل له أربعة أعمال رئيسية:
- تهيئة الكومة
- تخصيص
mspan - تحرير
mspan - توسيع الكومة
فيما يلي شرح لهذه الأربعة بالترتيب.
التهيئة
تهيئة الكومة تكون في مرحلة توجيه البرنامج، وهي أيضًا مرحلة تهيئة المجدول، ترتيب الاستدعاء كالتالي
schedinit() -> mallocinit() -> mheap_.init()في مرحلة التهيئة، هي مسؤولة بشكل رئيسي عن تنفيذ عمل تهيئة المخصصات المختلفة
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)
}من بينها مخصص mheap.spanalloc المسؤول عن تخصيص mspan ومخصص mheap.pages المسؤول عن تخصيص الصفحات، وتهيئة كل mcentral.
التخصيص
في mheap، تخصيص mspan كله يتم بواسطة طريقة mheap.allocSpan
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)إذا كانت الذاكرة المطلوبة صغيرة بما يكفي، أي تحقق npages < pageCachePages/4، فستحاول الحصول على mspan متاح من مخبأ mspan في P المحلي بدون قفل، إذا كان مخبأ P فارغًا، ستُهيأ أولًا
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}ثم تُحصل من مخبأ 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 من مخبأ P كالتالي، تحاول الحصول على آخر mspan في المخبأ.
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
}إذا كانت الذاكرة المطلوبة كبيرة، ستُخصص الذاكرة على الكومة، هذه العملية تحتاج قفل
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 لتخصيص صفحات ذاكرة كافية، إذا لم تكفِ ذاكرة الكومة يقوم mheap.grow بالتوسيع. بعد تخصيص ذاكرة الصفحات، المخصص السلسلي mheap.spanalloc يخصص 64 mspan للمخبأ المحلي في P، 64 هو نصف طول مصفوفة المخبأ بالضبط، ثم يُعاد mspan متاح من مخبأ 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 يُخصص بواسطة المخصص السلسلي، طبيعي أن يُحرر بواسطته عند تحرير الذاكرة.
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)
}أولًا عبر مخصص الصفحات mheap.pages يُعلن أن الصفحات المحددة حُررت، ثم تُضبط حالة mspan لـ mSpanDead، أخيرًا المخصص mheap.spanalloc يحرر mspan.
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))
}إذا كان مخبأ P غير ممتلئ، يُوضع في المخبأ المحلي في P للاستمرار في الاستخدام، وإلا يُحرر لذاكرة الكومة.
التوسيع
مساحة ذاكرة الصفحات التي يديرها heaparena ليست مطلوبة بالكامل في المرحلة المبكرة، فقط عند الحاجة للذاكرة يُخصص. المسؤول عن توسيع ذاكرة الكومة هو طريقة 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
}
}
...
}أولًا يحاول استخدام المخصص الخطي 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))
}بعد طلب الذاكرة، تُحدث في المصفوفة ثنائية الأبعاد 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))
}أخيرًا مخصص الصفحات يعلن هذه الذاكرة في حالة جاهزة.
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vتخصيص الكائنات
عند تخصيص الذاكرة للكائنات في go، حسب الحجم تُقسم لثلاثة أنواع مختلفة:
- الكائنات الدقيقة - tiny، أقل من 16B
- الكائنات الصغيرة - small، أقل من 32KB
- الكائنات الكبيرة - large، أكبر من 32KB
حسب الأنواع الثلاثة المختلفة، عند تخصيص الذاكرة يُنفذ منطق مختلف. الدالة المسؤولة عن تخصيص الذاكرة للكائنات هي runtime.mallocgc، توقيعها كالتالي
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointerلها ثلاث معاملات فقط، حجم الذاكرة، النوع، وقيمة منطقية لتحديد هل يجب تصفير الذاكرة. هي نقطة الدخول لكل تخصيص ذاكرة الكائنات في go، عند استخدام دالة new لإنشاء مؤشر تمر أيضًا عبر هذه الدالة، عند نجاح تخصيص الذاكرة، المؤشر الذي تعيده هو عنوان الكائن. ذكرنا في قسم mspan، كل mspan لديه spanClass، spanClass يحدد الحجم الثابت لـ mspan، وgo قسّم الكائنات من النطاق [0, 32KB] لـ 68 حجمًا مختلفًا، لذا ذاكرة go تتكون من عدة قوائم mspan بأحجام ثابتة مختلفة. عند تخصيص ذاكرة الكائن، يكفي حساب spanClass المقابل حسب حجم الكائن، ثم إيجاد قائمة mspan المقابلة حسب spanClass، أخيرًا البحث عن mspan متاح من القائمة، هذا النهج الطبقي يمكن أن يحل مشكلة تجزئة الذاكرة بشكل فعال نسبيًا.

الكائنات الدقيقة
كل الكائنات الدقيقة غير المؤشرية الأقل من 16B تُخصص بواسطة المخصص الدقيق في P في نفس مساحة الذاكرة المتصلة، في runtime.mcache، حقل tiny يسجل عنوان القاعدة لهذه الذاكرة.
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}حجم الكائنات الدقيقة يحدده الثابت runtime.maxTinySize، وهو 16B، حجم كتلة الذاكرة المستخدمة لتخزين الكائنات الدقيقة هو نفسه هذا الحجم، عمومًا الكائنات المخزنة هنا هي سلاسل نصية صغيرة، الكود المسؤول عن تخصيص الكائنات الدقيقة كالتالي.
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إذا كانت كتلة الذاكرة الدقيقة الحالية بها مساحة كافية للاستيعاب، تُستخدم كتلة الذاكرة الحالية مباشرة، أي off+size <= maxTinySize. إذا لم تكفِ، يحاول أولًا إيجاد مساحة متاحة من مخبأ span في mcache، إذا لم ينفع يطلب mspan من mcentral، مهما كان في النهاية يُحصل على عنوان متاح، أخيرًا تُستبدل كتلة الذاكرة الدقيقة القديمة بالجديدة.
الكائنات الصغيرة
معظم الكائنات في بيئة تشغيل go تقع في النطاق [16B, 32KB] من الكائنات الصغيرة، عملية تخصيص الكائنات الصغيرة هي الأكثر تعقيدًا، لكن الكود هو الأقل، الكود المسؤول عن تخصيص الكائنات الصغيرة كالتالي.
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 حسب spanClass يحاول الحصول على مساحة ذاكرة متاحة من mspan المخبأ المقابل في mcache.
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 مصفوفة كائنات، كما في الشكل التالي.

allocCache رقم 64-bit، كل bit يقابله مساحة ذاكرة، إذا كان bit معين 0 يعني أن هناك كائن يستخدمه، إذا كان 1 يعني أن هذه الذاكرة حرة. غرض sys.TrailingZeros64(s.allocCache) هو حساب عدد الأصفار اللاحقة، إذا كانت النتيجة 64 فهذا يعني لا توجد ذاكرة حرة للاستخدام، إذا وُجدت يُحسب إزاحة الذاكرة الحرة ويُضاف عنوان قاعدة 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
}إذن في النهاية، تخصيص ذاكرة الكائنات الصغيرة يتم طبقة تلو الأخرى، أولًا mcache، ثم mcentral، أخيرًا mheap. mcache بأقل تكلفة تخصيص، لأنه مخبأ محلي لـ P، لا يحتاج قفل عند تخصيص الذاكرة، mcentral يليه، وطلب الذاكرة مباشرة من mheap هو الأعلى تكلفة، لأن طريقة mheap.alloc تتنافس على القفل العام للكومة بأكملها.
الكائنات الكبيرة
تخصيص الكائنات الكبيرة هو الأبسط، إذا تجاوز حجم الكائن 32KB، يُطلب مباشرة من mheap تخصيص mspan جديد للاستيعاب، الكود المسؤول عن تخصيص الكائنات الكبيرة كالتالي.
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 مسؤول عن طلب مساحة ذاكرة للكائنات الكبيرة من mheap
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}من الكود نرى أن الكائنات الكبيرة تستخدم قيمة spanClass تساوي 0، الكائنات الكبيرة بشكل أساسي كل كائن يشغل mspan واحد.
أخرى
إحصائيات الذاكرة
بيئة تشغيل go تكشف للمستخدم دالة ReadMemStats، يمكن استخدامها لإحصاء حالة الذاكرة في وقت التشغيل.
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 قد تكون من مللي ثانية إلى مئات المللي ثانية، عمومًا تُستخدم فقط عند التصحيح واستكشاف المشاكل. البنية runtime.MemStats تسجل معلومات عن ذاكرة الكومة، وذاكرة المكدس، و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
مخصص الذاكرة بوضوح يُستخدم لتخصيص ذاكرة الكومة، لكن الكومة تُقسم لجزأين، جزء لذاكرة الكومة التي تحتاجها بيئة تشغيل go نفسها، وجزء مفتوح للمستخدم لاستخدامه. لذا في بعض الهياكل نرى مثل هذا الحقل المضمن
_ sys.NotInHeapيعني أن ذاكرة هذا النوع لن تُخصص على كومة المستخدم، هذا الحقل المضمن شائع جدًا في مكونات تخصيص الذاكرة، مثل البنية التي تمثل كومة المستخدم runtime.mheap
type mheap struct {
_ sys.NotInHeap
}الغرض الحقيقي من sys.NotInHeap هو تجنب حواجز الذاكرة لتحسين كفاءة وقت التشغيل، بينما كومة المستخدم تحتاج لتشغيل GC لذا تحتاج حواجز الذاكرة.
