03. Size Class 分级机制

8 字节到 32KB 的对象,只需要 67 个"规格"就能几乎零浪费地分配------本文拆解 size class 的约束、查表算法与生成器


版本说明:基于 Go ~1.24+ 源码,含 GreenTeaGC / size-specialized malloc 等实验特性


1. 为什么需要分级?

想象一个仓库要存放大小不一、几乎无限多种尺寸的货物。最朴素的方案是每块地都按最大尺寸划分 ------但这会浪费大量空间(小货物占大格子);另一个极端是精确切割------但每次都去精确搜索合适的内存块,分配会变得很慢。

Go 的折中方案:只提供 67 种"标准尺寸"(size class),把任意请求向上取整到最近的规格

go 复制代码
// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8

--- sizeclasses.go:6-9

例如申请 20 字节,会被向上取整到 24 字节(class 3)的槽位。虽然多花了 4 字节,但换来的是:

收益 说明
分配 O(1) 每种大小有独立的空闲链表,直接取头,不用精确搜索
碎片可控 浪费率被生成器限制在 ~12.5% 以内(见下一节)
对象遍历 span 内对象等大小,地址 = base + index * size,可用魔法数除法直接算出下标
GC 友好 GC 扫描时能精确计算对象边界,指针位图按固定粒度布局

一句话理解:size class = 用「少量规格 + 向上取整」换取「O(1) 分配 + 可预测的浪费」,是 TCMalloc 家族分配器的共同基石。


2. 67 个 size class 的设计约束

size class 不是随便拍的,mksizeclasses.go 的开头注释写明了三条设计约束:

go 复制代码
// The size classes are chosen so that rounding an allocation
// request up to the next size class wastes at most 12.5% (1.125x).
//
// Each size class has its own page count that gets allocated
// and chopped up when new objects of the size class are needed.
// That page count is chosen so that chopping up the run of
// pages into objects of the given size wastes at most 12.5% (1.125x)
// of the memory. It is not necessary that the cutoff here be
// the same as above.

--- mksizeclasses.go:9-17

三条约束翻译成人话:

  1. 取整浪费 ≤ 12.5%:从一个 size class 向上取整到下一个,多花的空间不超过 1.125 倍。例如 class 1(8B)到 class 2(16B)跨度很大,是因为对齐约束压不下来(见下)。
  2. 切页浪费 ≤ 12.5% :每个 size class 有自己的页数(1~10 页/span)。把若干页切成等大小对象时,尾部剩余(tail waste) 也不超过 span 的 1/8。这是决定 span 页数的直接因素。
  3. 两类浪费相乘 :理论上最坏情况是 1.125 × 1.125 ≈ 26.6% 开销 。但生成器注释指出,实际中两个浪费很少同时出现------< 512B 主要浪费在取整,> 512B 主要浪费在切页
go 复制代码
// The two sources of waste multiply, so the worst possible case
// for the above constraints would be that allocations of some
// size might have a 26.6% (1.266x) overhead.
// In practice, only one of the wastes comes into play for a
// given size (sizes < 512 waste mainly on the round-up,
// sizes > 512 waste mainly on the page chopping).
// For really small sizes, alignment constraints force the
// overhead higher.

--- mksizeclasses.go:19-26
注意 87.5% 的"max waste" :class 1(8B)的 max waste 高达 87.5%,远超 12.5% 约束。这是因为 8B 对象必须满足 MinHeapAlign = 8 字节对齐sizeclasses.go:85),跨度无法压到 12.5% 以内。对齐约束在极小尺寸上"凌驾"于浪费约束。

对齐(alignment)策略

对象对齐是生成器隐含的硬约束,最终汇总在表尾:

arduino 复制代码
// alignment  bits  min obj size
//         8     3             8
//        16     4            32
//        32     5           256
//        64     6           512
//       128     7           768
//      4096    12         28672
//      8192    13         32768

--- sizeclasses.go:75-82

读法:size class 的对象大小越高,要求的对齐越严格。例如:

  • 8B~16B:8 字节对齐(MinHeapAlign
  • 达到 32B 起:至少 16 字节对齐(heap bitmap 假设 ≥32B 的分配都 16 字节对齐,mksizeclasses.go:79
  • 达到 32KB(class 67):8192 字节对齐,即整页对齐

为什么对齐重要? ① 8 字节对齐保证指针字(uintptr)操作天然安全;② GC 的指针位图(heapBits)按 8 字节/位布局,只有对齐的地址才能直接映射到位图下标。对象错位会让位图索引计算变得极其昂贵。


3. 完整 size class 表解读

sizeclasses.go工具生成 的文件(首行 Code generated by mksizeclasses.go; DO NOT EDIT.),顶部注释就是完整表的唯一权威来源。下表把 1~67 全部列出,按 bytes/span(页数)分组观察规律:

class bytes/obj bytes/span 页数 objects tail waste max waste min align
1 8 8192 1 1024 0 87.50% 8
2 16 8192 1 512 0 43.75% 16
3 24 8192 1 341 8 29.24% 8
4 32 8192 1 256 0 21.88% 32
5 48 8192 1 170 32 31.52% 16
6 64 8192 1 128 0 23.44% 64
7 80 8192 1 102 32 19.07% 16
8 96 8192 1 85 32 15.95% 32
9 112 8192 1 73 16 13.56% 16
10 128 8192 1 64 0 11.72% 128
11 144 8192 1 56 128 11.82% 16
12 160 8192 1 51 32 9.73% 32
13 176 8192 1 46 96 9.59% 16
14 192 8192 1 42 128 9.25% 64
15 208 8192 1 39 80 8.12% 16
16 224 8192 1 36 128 8.15% 32
17 240 8192 1 34 32 6.62% 16
18 256 8192 1 32 0 5.86% 256
19 288 8192 1 28 128 12.16% 32
20 320 8192 1 25 192 11.80% 64
21 352 8192 1 23 96 9.88% 32
22 384 8192 1 21 128 9.51% 128
23 416 8192 1 19 288 10.71% 32
24 448 8192 1 18 128 8.37% 64
25 480 8192 1 17 32 6.82% 32
26 512 8192 1 16 0 6.05% 512
27 576 8192 1 14 128 12.33% 64
28 640 8192 1 12 512 15.48% 128
29 704 8192 1 11 448 13.93% 64
30 768 8192 1 10 512 13.94% 256
31 896 8192 1 9 128 15.52% 128
32 1024 8192 1 8 0 12.40% 1024
33 1152 8192 1 7 128 12.41% 128
34 1280 8192 1 6 512 15.55% 256
35 1408 16384 2 11 896 14.00% 128
36 1536 8192 1 5 512 14.00% 512
37 1792 16384 2 9 256 15.57% 256
38 2048 8192 1 4 0 12.45% 2048
39 2304 16384 2 7 256 12.46% 256
40 2688 8192 1 3 128 15.59% 128
41 3072 24576 3 8 0 12.47% 1024
42 3200 16384 2 5 384 6.22% 128
43 3456 24576 3 7 384 8.83% 128
44 4096 8192 1 2 0 15.60% 4096
45 4864 24576 3 5 256 16.65% 256
46 5376 16384 2 3 256 10.92% 256
47 6144 24576 3 4 0 12.48% 2048
48 6528 32768 4 5 128 6.23% 128
49 6784 40960 5 6 256 4.36% 128
50 6912 49152 6 7 768 3.37% 256
51 8192 8192 1 1 0 15.61% 8192
52 9472 57344 7 6 512 14.28% 256
53 9728 49152 6 5 512 3.64% 512
54 10240 40960 5 4 0 4.99% 2048
55 10880 32768 4 3 128 6.24% 128
56 12288 24576 3 2 0 11.45% 4096
57 13568 40960 5 3 256 9.99% 256
58 14336 57344 7 4 0 5.35% 2048
59 16384 16384 2 1 0 12.49% 8192
60 18432 73728 9 4 0 11.11% 2048
61 19072 57344 7 3 128 3.57% 128
62 20480 40960 5 2 0 6.87% 4096
63 21760 65536 8 3 256 6.25% 256
64 24576 24576 3 1 0 11.45% 8192
65 27264 81920 10 3 128 10.00% 128
66 28672 57344 7 2 0 4.91% 4096
67 32768 32768 4 1 0 12.50% 8192

--- sizeclasses.go:6-73(注释表)

图 3-1:前 20 个 size class 示意图(条宽 ∝ 对象大小)

读表要点

  • bytes/span 列的变化 :绝大多数 class 用一个 span(8192B = 1 页)。当 1 页装不下、且 2 页能让尾部浪费降到 12.5% 以内时,就换 2 页/3 页......最多 10 页(class 65,MaxSizeClassNPages = 10sizeclasses.go:93)。页数绝不跳跃式增长到不需要的大小,这是切页浪费约束的直接体现。
  • objectsbytes/span ÷ bytes/obj 向下取整。class 1 一个 span 塞 1024 个对象(MaxObjsPerSpan = 1024sizeclasses.go:92)------这也定义了 allocCache 是 64 位、需要按 64 个对象分块的原因(见 06 篇)。
  • tail waste:span 尾部无法再塞下一个完整对象的剩余字节。class 3(24B × 341 = 8184B,剩 8B)就是典型的"1 页切不干净"。
  • min align:即对象大小的最低 2 的幂因子。越大的对象对齐要求越高,呼应第 2 节的对齐策略。

NumSizeClasses = 68 而不是 67? 表里只有 67 个真实 class,但数组长度是 68------下标 0 留空不用 (相当于一个"空位",mksizeclasses.go:69),让 SizeClassToSize[0] = 0。这样一来,任何合法的 size class 从 1 开始编号,如果代码因为 bug 拿到 0 号,查表得到的大小是 0,一眼就能发现问题;同时查表时不用再写 if (class == 0) 的边界判断。所以文档常说"67 个 size class",代码里数组却是 68 格------第 0 格是安全垫片。


4. 查表算法:SizeToSizeClass8 / SizeToSizeClass128

拿到一个 size,runtime 必须 O(1) 地找到对应 class。直接线性扫描 67 项显然太慢,于是生成了两张前缀表,用两次索引代替遍历:

go 复制代码
var SizeToSizeClass8 = [SmallSizeMax/SmallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, ...}
var SizeToSizeClass128 = [(MaxSmallSize-SmallSizeMax)/LargeSizeDiv + 1]uint8{32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, ...}

--- sizeclasses.go:101-102

为什么要两张表?

因为 size 跨度太大,单张表按 8 字节粒度建到 32KB 会需要 4096 项。Go 的分段策略是:

覆盖范围 粒度 下标公式 元素数
SizeToSizeClass8 0 ~ 1024B(SmallSizeMax 8B(SmallSizeDiv size / 8 129
SizeToSizeClass128 1024 ~ 32768B(MaxSmallSize 128B(LargeSizeDiv (size - 1024) / 128 249

下标公式中的常量定义在 sizeclasses.go:86-89

查表套路(runtime 侧)

分配时的查找分三步(见 malloc.go:1379-1383mallocgcSmallNoscan,同款逻辑在 msize.goroundupsize):

go 复制代码
// 1. 先判区间
if size <= gc.SmallSizeMax-8 {
    // 2. 小尺寸:向上取整到 8 的倍数后除 8 得下标
    sizeclass = gc.SizeToSizeClass8[divRoundUp(size, gc.SmallSizeDiv)]
} else {
    // 3. 大尺寸:偏移后按 128 除
    sizeclass = gc.SizeToSizeClass128[divRoundUp(size-gc.SmallSizeMax, gc.LargeSizeDiv)]
}
size = uintptr(gc.SizeClassToSize[sizeclass])

--- malloc.go:1379-1384(简化)

妙处:divRoundUp 免费完成"向上取整"

divRoundUp(a, b) = (a + b - 1) / b。例如请求 100B:

  1. 100 ≤ 1024-8 走 8B 表
  2. divRoundUp(100, 8) = (100+7)/8 = 13SizeToSizeClass8[13] = 9(看看表里第 13 项确实是 9)
  3. SizeClassToSize[9] = 112 返回 112B 槽位

为什么 SmallSizeMax-8 这个边界? 若请求恰好 1024B,divRoundUp(1024,8)=128,而 8B 表只有 129 项(下标 0~128),SizeToSizeClass8[128]=32,仍合法。用 -8 是为了把 1024 整 1 的请求安全推给 128B 表,避免下标越界的同时保持语义一致。

另一张索引表:SizeClassToSize

拿到 class 号后,用 SizeClassToSize[class] 直接读出对齐后的对象大小:

go 复制代码
var SizeClassToSize = [NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, ...}

--- sizeclasses.go:98

同理还有 SizeClassToNPages(每个 class 的页数)和 SizeClassToDivMagic(32 位魔法数除法,用于把"span 内偏移"换算成"对象下标",见 mksizeclasses.go:136-205computeDivMagic):

go 复制代码
var SizeClassToNPages = [NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

--- sizeclasses.go:99

SizeClassToNPages 能直观看到页数序列:前 34 个全 1,从 class 35(1408B)开始出现 2,1,2,1,2,1,3,...------正是第 3 节表格中"页数"列的压缩形式。


5. roundupsize():大小对齐的快速路径

roundupsize 把用户请求的 size 变成真正会分配的槽位大小 ,是 roundupsize 查表 对齐的快速路径:

go 复制代码
// Returns size of the memory block that mallocgc will allocate if you ask for the size,
// minus any inline space for metadata.
func roundupsize(size uintptr, noscan bool) (reqSize uintptr) {
	reqSize = size
	if reqSize <= maxSmallSize-gc.MallocHeaderSize {
		// Small object.
		if !noscan && reqSize > gc.MinSizeForMallocHeader { // !noscan && !heapBitsInSpan(reqSize)
			reqSize += gc.MallocHeaderSize
		}
		// (reqSize - size) is either mallocHeaderSize or 0. We need to subtract mallocHeaderSize
		// from the result if we have one, since mallocgc will add it back in.
		if reqSize <= gc.SmallSizeMax-8 {
			return uintptr(gc.SizeClassToSize[gc.SizeToSizeClass8[divRoundUp(reqSize, gc.SmallSizeDiv)]]) - (reqSize - size)
		}
		return uintptr(gc.SizeClassToSize[gc.SizeToSizeClass128[divRoundUp(reqSize-gc.SmallSizeMax, gc.LargeSizeDiv)]]) - (reqSize - size)
	}
	// Large object. Align reqSize up to the next page. Check for overflow.
	reqSize += pageSize - 1
	if reqSize < size {
		return size
	}
	return reqSize &^ (pageSize - 1)
}

--- msize.go:14-36

三步解读

  1. scan 对象 > 512B 时额外加 8BreqSize += gc.MallocHeaderSize。这类对象前面要放一个 MallocHeader(8 字节) 存类型指针(见 04 的 heapBitsInSpan 一节)。但返回值会再减掉这 8 字节 ,因为调用方(mallocgc)实际拿到的指针会指向 header 之后------大小算对,地址不用重复加。
  2. 小对象查表SizeToSizeClass8SizeToSizeClass128 二选一(见第 4 节),从 SizeClassToSize 读出对齐后大小。
  3. 大对象整页对齐(size + pageSize - 1) &^ (pageSize - 1) 就是"向上取整到 8KB 倍数",溢出则回退原 size(msize.go:30-35)。

MallocHeaderSize = 8 定义在 internal/runtime/gc/malloc.go:17。取 8 是为了保证 32 位平台上也满足 8 字节对齐(8 字节原子操作需要)。MinSizeForMallocHeader = PtrSize * PtrBits (64 位下 = 512)定义在同文件 internal/runtime/gc/malloc.go:47------小于等于 512B 的 scan 对象,指针位图放在 span 尾部(heapBits),不需要 header。
为什么大对象直接整页对齐,而不是查表? 大对象(>32KB)通常一次分配就独占整页,没有"槽位复用"的概念,所以不需要 size class 表。整页对齐既能满足 mmap/页映射的硬件要求,也让页管理(pageAlloc)用最简单的方式记账。


6. spanClass:scan / noscan 双维度

光有 size class 还不够------同样 64 字节的对象,含指针 (GC 要扫)和不含指针 (GC 直接跳过)在 GC 开销上天差地别。于是 runtime 在 size class 之上叠了一个维度,得到 spanClass

go 复制代码
// A spanClass represents the size class and noscan-ness of a span.
//
// Each size class has a noscan spanClass and a scan spanClass. The
// noscan spanClass contains only noscan objects, which do not contain
// pointers and thus do not need to be scanned by the garbage
// collector.
type spanClass uint8

const (
	numSpanClasses = gc.NumSizeClasses << 1
	tinySpanClass  = spanClass(tinySizeClass<<1 | 1)
)

func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
	return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

//go:nosplit
func (sc spanClass) sizeclass() int8 {
	return int8(sc >> 1)
}

//go:nosplit
func (sc spanClass) noscan() bool {
	return sc&1 != 0
}

--- mheap.go:567-592

编码规则

  • 一个 uint8 存两个信息:高 7 位 = size class 号,最低位 = noscan 标志。
  • makeSpanClass(sizeclass, noscan) = sizeclass << 1 | noscanmheap.go:580-582)。
  • 取出时 sizeclass() = sc >> 1noscan() = sc & 1mheap.go:585-592)。
  • 总数 numSpanClasses = 68 << 1 = 136mheap.go:576)。mcache.alloc [numSpanClasses]*mspan 就是按这 136 个下标索引的(见 06)。
  • tinySpanClass = spanClass(2<<1 | 1) = 5:Tiny 分配器专用的 span,对应 size class 2(16B)+ noscan(mheap.go:577)。

为什么 span 要区分 scan/noscan? 一个 span 内的所有对象共享同一套元数据(指针位图/分配位图)。如果 scan 和 noscan 对象混在一个 span 里,GC 就必须按对象逐个判断,位图布局也无法统一。拆成两套 spanClass,GC 扫到 noscan span 直接跳过整个 span,这是分配侧最便宜的优化之一。

spanClass 值 含义
0 ~ 133 常规 spanClass:sizeclass << 1(scan)或 +1(noscan)
5 tinySpanClass(SC2 noscan),Tiny 分配器专用

spanClass 会随 size class 一起被 mcache 索引 ,所以 04 的分配路径里,拿到 class 号后第一件事就是 spc := makeSpanClass(sizeclass, true)(noscan 分配,见 malloc.go:1385)。


7. 为什么是 67 个 class?mksizeclasses.go 生成算法

sizeclasses.gomksizeclasses.go 生成(//go:generate go -C ../../../runtime/_mkmalloc run mksizeclasses.go,见 sizeclasses.go:2)。makeClasses() 是核心(mksizeclasses.go:66-134),它做三件事:

第一步:遍历所有候选尺寸,选 class(mksizeclasses.go:71-105

go 复制代码
align := minHeapAlign
for size := align; size <= maxSmallSize; size += align {
	if powerOfTwo(size) { // bump alignment once in a while
		if size >= 2048 {
			align = 256
		} else if size >= 128 {
			align = size / 8
		} else if size >= 32 {
			align = 16 // heap bitmaps assume 16 byte alignment for allocations >= 32 bytes.
		}
	}
	// Make the allocnpages big enough that
	// the leftover is less than 1/8 of the total,
	// so wasted space is at most 12.5%.
	allocsize := pageSize
	for allocsize%size > allocsize/8 {
		allocsize += pageSize
	}
	npages := allocsize / pageSize
	...
}

--- mksizeclasses.go:71-93

逻辑链:

  1. align=8 开始,每 8 字节试一个候选尺寸
  2. 碰到 2 的幂就提高对齐步长>=2048 后步长升到 256;>=128 后升到 size/8>=32 后升到 16(mksizeclasses.go:73-80)。这就是为什么 class 越小越密、越大越疏。
  3. 为候选尺寸选页数 :从 1 页开始,只要 allocsize % size > allocsize/8(尾部浪费 > 12.5%),就加一页再试mksizeclasses.go:89-92)。这是"切页浪费 ≤ 12.5%"约束的直接落地。
  4. 合并等价 class :如果新候选和上一个 class 页数相同、且每页装的整数对象数也相同,就把上一个 class 的尺寸改成更大的那个,去掉冗余(mksizeclasses.go:100-103)。

第二步:把能塞进同页数的对象"加大"(mksizeclasses.go:107-123

go 复制代码
// Increase object sizes if we can fit the same number of larger objects
// into the same number of pages. For example, we choose size 8448 above
// with 6 objects in 7 pages. But we can well use object size 9472,
// which is also 6 objects in 7 pages but +1024 bytes (+12.12%).
for i := range classes {
	...
	c := &classes[i]
	psize := c.npages * pageSize
	new_size := (psize / (psize / c.size)) &^ (largeSizeDiv - 1)
	if new_size > c.size {
		c.size = new_size
	}
}

--- mksizeclasses.go:107-123

核心洞察 :既然某些尺寸用 7 页只能装 6 个对象,那为什么不把对象做大一点、同样装 6 个 ,把尾部的页浪费变成"对象更大带来的取整收益"?示例注释里的 8448 9472 就是这个思路。这也是为什么 class 52 有 9472B 这种"奇怪"的尺寸(对应 SizeClassToSize[52])。

这步对 SizeToSizeClass128 尤其重要:largeSizeDiv = 128 保证所有 class 尺寸是 128 的倍数,查表时按 128 分段的 SizeToSizeClass128 才能精确命中。

第三步:校验 class 总数并计算魔法数(mksizeclasses.go:125-131

go 复制代码
if len(classes) != 68 {
	panic("number of size classes has changed")
}
for i := range classes {
	computeDivMagic(&classes[i])
}

--- mksizeclasses.go:125-131

  • len(classes) != 68 直接 panic :67 个真实 class + 1 个 dummy 是写死的契约sizeclasses.go:90NumSizeClasses = 68mheap.go:576numSpanClasses = 68 << 1 都依赖它,改任何一个都会连锁破坏 mcache/位图布局。
  • computeDivMagic 为每个 class 算出一个 32 位魔法乘法倒数 ,把"span 内偏移 对象下标"的除法换成乘法+移位(mksizeclasses.go:136-205)。这是 06 篇 nextFreeIndex/位图下标计算性能的关键。

生成器常量与源码常量一一对应

printClassesmksizeclasses.go:266-335)把生成器内部的 minHeapAlign/maxSmallSize/smallSizeDiv/... 原样写成 sizeclasses.go:84-96 的常量块。两边的值完全一致:

源码常量 含义
MinHeapAlign 8 最小对象对齐
MaxSmallSize 32768 小对象上限(> 此值走 Large 路径)
SmallSizeDiv 8 小尺寸查表粒度(0~1024B)
SmallSizeMax 1024 8B 粒度查表的上界
LargeSizeDiv 128 大尺寸查表粒度(1024~32KB)
NumSizeClasses 68 class 总数(含 dummy 0)
PageShift 13 页大小 = 2^13 = 8192B
MaxObjsPerSpan 1024 单个 span 最多对象数(class 1)
MaxSizeClassNPages 10 单 span 最多页数(class 65)
TinySize 16 Tiny 分配器块大小
TinySizeClass 2 Tiny 块对应的 size class

--- sizeclasses.go:84-96


8. 小结

  1. size class = 67 个标准规格 + 向上取整,用可预测的碎片换取 O(1) 分配,源自 TCMalloc 设计。
  2. 两条 12.5% 约束(取整浪费 + 切页浪费)决定 class 取值与 span 页数;对齐约束在极小尺寸上凌驾其上(class 1 达 87.5%)。
  3. 两张查表SizeToSizeClass8(01024B,8B 粒度)和 SizeToSizeClass128(1024B 32KB,128B 粒度),divRoundUp 让"向上取整 + 查表"一步完成。
  4. roundupsize 是小对象查表、大对象整页对齐的统一入口;scan 对象 >512B 时额外预算 8B 的 MallocHeader。
  5. spanClass = sizeclass << 1 | noscan 叠加 scan/noscan 维度,136 个 spanClass 驱动 mcache 索引与 GC 跳过。
  6. 67 = 生成器产物 :遍历候选尺寸 + 页数最小化 + 等价合并 + 对象加大,最后由 len(classes) != 68 强约束。

下一步,从 mallocgc 入口看这些 class 如何被分派到 Tiny / Small / Large 三条路径:04. mallocgc:分配总入口。


相关推荐
用户330144867632 小时前
04. mallocgc:分配总入口
go
程序员爱钓鱼6 小时前
Go 编程实战:函数 Function——参数、返回值与代码复用
后端·面试·go
名字还没想好☜1 天前
Go 1.23 range-over-func 迭代器实战:自定义可迭代类型、提前退出与惰性求值
开发语言·后端·golang·go·迭代器
赫媒派1 天前
Go 1.27 来了:泛型方法补齐,JSON 提速不踩坑
后端·go·敏捷开发
小满zs1 天前
Go语言第九章(错误处理)
后端·go
程序员爱钓鱼1 天前
Go 编程实战:指针 Pointer——理解地址、取址与解引用
后端·面试·go
Flynt2 天前
Go 1.27 升了一波,泛型方法和 JSON v2 真香但有个坑
后端·go
用户125758524362 天前
对象存储 URL 为什么别到处拼:后台附件预览要验这一层
后端·go·ai编程
名字还没想好☜2 天前
Go 的 database/sql 连接池实战:SetMaxOpenConns 怎么配、连接泄漏怎么查
数据库·sql·golang·go·数据库连接池