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
三条约束翻译成人话:
- 取整浪费 ≤ 12.5%:从一个 size class 向上取整到下一个,多花的空间不超过 1.125 倍。例如 class 1(8B)到 class 2(16B)跨度很大,是因为对齐约束压不下来(见下)。
- 切页浪费 ≤ 12.5% :每个 size class 有自己的页数(1~10 页/span)。把若干页切成等大小对象时,尾部剩余(tail waste) 也不超过 span 的 1/8。这是决定 span 页数的直接因素。
- 两类浪费相乘 :理论上最坏情况是 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 = 10,sizeclasses.go:93)。页数绝不跳跃式增长到不需要的大小,这是切页浪费约束的直接体现。objects列 :bytes/span ÷ bytes/obj向下取整。class 1 一个 span 塞 1024 个对象(MaxObjsPerSpan = 1024,sizeclasses.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-1383 的 mallocgcSmallNoscan,同款逻辑在 msize.go 的 roundupsize):
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:
100 ≤ 1024-8走 8B 表divRoundUp(100, 8) = (100+7)/8 = 13查SizeToSizeClass8[13] = 9(看看表里第 13 项确实是 9)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-205 的 computeDivMagic):
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
三步解读
- scan 对象 > 512B 时额外加 8B :
reqSize += gc.MallocHeaderSize。这类对象前面要放一个 MallocHeader(8 字节) 存类型指针(见 04 的 heapBitsInSpan 一节)。但返回值会再减掉这 8 字节 ,因为调用方(mallocgc)实际拿到的指针会指向 header 之后------大小算对,地址不用重复加。 - 小对象查表 :
SizeToSizeClass8或SizeToSizeClass128二选一(见第 4 节),从SizeClassToSize读出对齐后大小。 - 大对象整页对齐 :
(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 | noscan(mheap.go:580-582)。- 取出时
sizeclass() = sc >> 1、noscan() = sc & 1(mheap.go:585-592)。 - 总数
numSpanClasses = 68 << 1 = 136(mheap.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.go 由 mksizeclasses.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
逻辑链:
- 从
align=8开始,每 8 字节试一个候选尺寸。 - 碰到 2 的幂就提高对齐步长 :
>=2048后步长升到 256;>=128后升到size/8;>=32后升到 16(mksizeclasses.go:73-80)。这就是为什么 class 越小越密、越大越疏。 - 为候选尺寸选页数 :从 1 页开始,只要
allocsize % size > allocsize/8(尾部浪费 > 12.5%),就加一页再试 (mksizeclasses.go:89-92)。这是"切页浪费 ≤ 12.5%"约束的直接落地。 - 合并等价 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:90的NumSizeClasses = 68与mheap.go:576的numSpanClasses = 68 << 1都依赖它,改任何一个都会连锁破坏 mcache/位图布局。computeDivMagic为每个 class 算出一个 32 位魔法乘法倒数 ,把"span 内偏移 对象下标"的除法换成乘法+移位(mksizeclasses.go:136-205)。这是 06 篇nextFreeIndex/位图下标计算性能的关键。
生成器常量与源码常量一一对应
printClasses(mksizeclasses.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. 小结
- size class = 67 个标准规格 + 向上取整,用可预测的碎片换取 O(1) 分配,源自 TCMalloc 设计。
- 两条 12.5% 约束(取整浪费 + 切页浪费)决定 class 取值与 span 页数;对齐约束在极小尺寸上凌驾其上(class 1 达 87.5%)。
- 两张查表 :
SizeToSizeClass8(01024B,8B 粒度)和32KB,128B 粒度),SizeToSizeClass128(1024BdivRoundUp让"向上取整 + 查表"一步完成。 roundupsize是小对象查表、大对象整页对齐的统一入口;scan 对象 >512B 时额外预算 8B 的 MallocHeader。- spanClass =
sizeclass << 1 | noscan叠加 scan/noscan 维度,136 个 spanClass 驱动 mcache 索引与 GC 跳过。 - 67 = 生成器产物 :遍历候选尺寸 + 页数最小化 + 等价合并 + 对象加大,最后由
len(classes) != 68强约束。
下一步,从 mallocgc 入口看这些 class 如何被分派到 Tiny / Small / Large 三条路径:04. mallocgc:分配总入口。