用 go build -gcflags=-m=2 读 Go 1.27.1 的内联诊断:预算 80,add 4、sum 16、big 62、recursive 71 可以内联,defer、recover、go 语句与代价超出的 caller(96)、main(310)不能;一连串 x += i 最多 26 条(代价 80);内联的收益在这个微基准里体现在逃逸分析上(返回指针的小函数内联后 0 次分配,关闭内联后 1 次分配);标准库的小函数可以被内联。
在 Go 1.27.1 里用汇编里的 runtime.panicBounds 调用点数验证边界检查消除:range、i < len(s)、i&7、uint 比较、倒序循环、clampN 都没有检查;i < n(n 来自参数)、i%8、s[i:j]、下标来自另一个切片仍有检查;_ = s[3] 把 4 处合成 1 处,带 if 的 _ = s[n-1] 反而无效;关掉所有检查(-B)在顺序求和里只快 0.3%–0.8%。
在 Go 1.27.1(arm64)上真实测 PGO:用五种工作负载的 CPU profile 合并成 default.pgo 重新编译,编译器把代价 146 的 score 在热调用点内联、把接口调用的热目标内联;但 5 种工作负载里只有 regexp 明显变快(471 → 433 ms,8.1%),接口调用、直接调用、compress/flate 几乎没变,接口加较长函数的组合反而慢 2.0%——PGO 不保证变快;-pgo=auto 的默认行为。
在 Go 1.27.1 里用 gctrace、源码摘录、汇编与基准验证三色标记与写屏障:一次 GC 由清扫终止 STW、并发标记、标记终止 STW 三段组成(STW 最大 0.045 与 0.120 ms);混合写屏障同时 shade 被覆盖的指针与新指针;指针赋值的汇编里检查 runtime.writeBarrier 标志并调用 gcWriteBarrier2;标记阶段里指针写从 2.37 ns 变成 17.59 ns,整数写不变;默认的 greenteagc 实现比 nogreenteagc 的 GC CPU 时间少约 23%。
在 Go 1.27.1 里验证 GC 的触发:刚做完一次 GC 时堆目标 = 存活堆 × (1 + GOGC/100)(6 种 GOGC 的预测误差 ≤ 1.5 MB);运行中分配越快目标越大;GOGC 从 25 到 800 时 GC 次数 313 → 32、吞吐 44.5 → 177.7 M 次/秒、峰值堆 69 → 912 MB;GOGC=off 一秒内堆涨到 5952 MB;debug.SetGCPercent 运行时生效;最小堆目标 4 MB 下每秒 1306 次 GC;分配 goroutine 增多时 mark assist 从 6 ms 涨到 2218 ms。
在 Go 1.27.1 里实测 GOMEMLIMIT:存活堆 200 MiB 时不设限制峰值 609 MB、吞吐 109.6 M/s,限制 300 MiB 时峰值 299 MB、吞吐 44.7 M/s、GC 占 52% CPU(GOGC 开不开都一样);限制只比存活堆多一点时吞吐降到 11.2 M/s;限制低于存活堆时进程没被杀、内存仍超限(273 MB)、GC 占 57.6%;GOMEMLIMIT 只认整数与 B/KiB/MiB/GiB/TiB。
在 Go 1.27.1(arm64,10 个 P)里量 GC 停顿:STW 停顿不随堆(10 MB 到 1 GB)或 goroutine 数(最多 50 万)变长,直方图上界都在几百微秒以内;但一个每 1 ms 睡一次的 goroutine 看到的 p99 多余延迟从 0.93 ms 涨到 26–27 ms(存活堆 500 MB 以上),GOMAXPROCS=1 时 p50 就是 30 ms;对照实验显示只有一个 goroutine 在做 GC、没有别的负载时,仍有约 30 ms 的 p99 延迟。
在 Go 1.27.1 里用 MemStats.BySize、堆增量与基准测试验证内存分配器:68 个 size class(8 到 32768 字节,页 8 KiB);请求大小向上取整到 class(17 字节 → 24,1030 → 1152);小于 16 字节的无指针对象被 tiny 分配器合并(1 字节对象只占 1 字节);含指针的对象大于 512 字节要多 8 字节 malloc header(576 字节含指针占 640);大于 32 KiB 按 8 KiB 页取整;分配成本从 8 字节的 7 ns 到 256 KiB 的 8.4 µs,并且在 128、192、256 字节处出现无法解释的跳变。
在 Go 1.27.1 里实测 goroutine 栈:初始约 2 KiB(10 万个阻塞的 goroutine 每个约 2050 字节),递归时栈大约每次翻倍并被整体拷贝(20 万层递归搬动 14 次、共 20.57 ms);函数返回后栈不立即变小,每次 GC 约减半(32.3 MiB → 0.53 MiB);第一次深递归 5.5–7.8 ms,栈已够大时 0.73 ms;无限递归在 1 GB 上限处 fatal error;函数序言的栈检查与 //go:nosplit 链的链接限制。
在 Go 1.27.1(arm64 macOS)里量 goroutine 的成本:一个阻塞的 goroutine 约 2.7–2.8 KB(栈 2050 字节 + 堆里的 g 结构体等约 650 字节),50 万个共 1320 MiB;创建并跑完一个约 346 ns(一次 channel 收发 131 ns);两个 goroutine 经 channel 来回一次 189–254 ns;对照 OS 线程创建 22–122 µs、每个约 57 KiB RSS,本机上限 4096;Go 的 SetMaxThreads 超限会 fatal error: thread exhaustion。
用 Go 1.27.1 复现七种 goroutine 泄漏(发送无人接收、接收无人发送、nil channel、重复 Lock、WaitGroup 永不 Done、空 select、等全局 channel)各 100 个,NumGoroutine 从 1 变成 701;goroutine profile(debug=1)把它们聚合到 main.go 的具体行,debug=2 给出每个的等待原因;Go 1.27.1 新增的 goroutineleak profile 通过 GC 报告其中 598 个(等全局 channel 的不报);两种修好的写法(缓冲 channel、context 取消)不留下 goroutine。
在 Go 1.27.1 里验证调度器的工作窃取:同一个 goroutine 创建的 200 个任务,GOMAXPROCS 从 1 到 10 几乎线性加速(1000 → 105 ms,9.52 倍);用 GODEBUG=schedtrace=1,scheddetail=1 看到创建者所在 P 的本地队列放不下的部分进入全局队列(774 个),空闲 P 一次窃取约 127 个(约一半);并摘录 runqgrab 的 n = n - n/2 与 stealTries = 4 等源码。