这一关回答:内核拿 -O2 到底买了什么?又为什么亲手关掉编译器最得意的几个优化?答案比直觉更分裂:-O2 不是"一个优化",而是上百个 pass 的默认组合——每个 pass 都是一次"大概率划算"的小赌注(内联启发式、向量化、死代码删除……);但内核和编译器签的是另一份契约:-fno-strict-aliasing("别拿类型别名来骗我")、-fno-delete-null-pointer-checks("别删我的 NULL 检查")。优化的本质是在 as-if 规则下重写你的代码——内核要的是"快,但不许自作聪明"。
这一关的机制支点:-O2 的 pass 组合与 -O2/-Os 开关(顶层 Makefile 已验证)、内联(__always_inline/noinline)、分支提示(likely/unlikely,以及它的运行期升级版 jump label)、LTO(内核只认 Clang ThinLTO)、PGO/AutoFDO(机制讲透,但 7.3 的 Makefile 里一个 PGO 的影子都没有——已验证),以及 barrier()。M18 讲"编译器会动你的代码"(as-if 与重排),本关讲"让它动得更快,同时划好红线"。
-O2 打开的是上百个 pass:内联启发式(-finline-small-functions:小函数默认内联)、向量化(-ftree-vectorize:循环自动 SIMD 化)、死代码删除(-fdce)、公共子表达式删除、循环优化、常量传播……每个 pass 独立下注,赌的是"大多数程序上划算"。这就是 deck 里"小赌注"的含义:没有哪个 pass 敢保证对你的代码一定有利,组合起来期望为正。
唯一的硬契约是 as-if 规则:可观察行为不变,编译器在中间随便重写——M18 的编译器重排正是这条规则的另一面。本关是同一条规则的"进攻形态"。
内核侧的开关在顶层 Makefile 里,白纸黑字(7.3.0-rc4 已验证):CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE → KBUILD_CFLAGS += -O2,CONFIG_CC_OPTIMIZE_FOR_SIZE → KBUILD_CFLAGS += -Os。-Os 不是"更慢的 -O2",是另一套赌注组合——为体积优化(嵌入式、小内存场景),赌的是"icache/内存省下来的比执行慢回去的多"。
__always_inline 的定义只有一行(include/linux/compiler_attributes.h,已验证):#define __always_inline inline __attribute__((__always_inline__))——强制内联,告诉编译器"别算账,直接内"。内核把它用在热路径的小函数上。
内联赚两笔钱:消掉 call/ret 开销;更重要的是跨函数优化——常量传播进函数体、形参变常量后的死代码删除,这些"第二轮"优化只有内联后才看得见。但代价同样显性:代码膨胀 → icache 压力。内核是 7×24 常驻的代码,icache miss 比一次 call 贵得多——这就是为什么不是所有小函数都配 __always_inline,普通 inline 只是"建议",最终由 -O2 的内联启发式按收益模型自动决策。
反方向的旋钮是 noinline(同一文件,已验证):ftrace、kprobe 需要真实的函数边界来下钩——函数被内联了,hook 就无处可挂。优化与可观测性在这里直接打架,内核选了"该留边界的地方留边界"。
定义(include/linux/compiler.h,已验证):#define likely(x) __builtin_expect(!!(x), 1),unlikely 同理取 0。这只是纸条,不是命令:不改变程序语义,编译器据此排代码布局——热路径直行、冷路径跳到远处,减少分支预测失败的惩罚。注意它不保证分支预测器听话,只影响"代码放哪"。
纸条的运行期升级版是 jump label(CONFIG_JUMP_LABEL,arch/Kconfig 原文:"Optimize very unlikely/likely branches",已验证):把"几乎恒为假"的分支编译成一条 nop,条件翻转的罕见时刻再动态 patch 成 jmp。tracepoint、调度器、网络栈、KVM 都在用——热路径上的判断指令本身消失了,只剩一条 nop。代价是 patch 时的同步复杂性,但"条件翻转"极少发生,代价只付在罕见时刻。
对比着记:likely 是编译期纸条(静态),jump label 是运行期改代码(动态)——后者是"把提示变成代码改写"。
没有 LTO,内联、常量传播、死代码删除全被 .c 文件边界挡住——跨文件的调用再小也内联不了。LTO 把优化的视野从"一个翻译单元"扩大到"整个程序"。
内核侧的事实(arch/Kconfig,已验证):CONFIG_LTO_CLANG、LTO_CLANG_THIN、LTO_CLANG_FULL——Kconfig 里只有 Clang 一家,没有 GCC LTO。务实的选择是 ThinLTO:用摘要做跨模块内联,内存可承受;Full LTO 要做全程序分析,对 vmlinux 这种体量基本不可用。代价:链接变慢、调试信息复杂化——LTO 从来不是免费午餐。
机制本身很漂亮:-fprofile-generate 插桩跑一遍,收集每条边的真实执行计数;-fprofile-use 重编译,分支概率、内联决策、代码布局全部用实测数据下注。AutoFDO 更进一步:用 perf 采样代替插桩,省掉第一遍的插桩开销——"用生产环境的真实采样指导优化"。
然后是内核侧的事实,同样白纸黑字:7.3 顶层 Makefile 全文 grep "PGO",零命中(已验证)——内核构建默认不走 profile 引导。为什么?因为内核的 workload 是"整个世界":桌面、服务器、嵌入式、路由器跑的是完全不同的代码路径,不存在"代表性 profile";而且"插桩→跑→重编"的两遍流程,对内核这种构建也不友好。本节讲透机制,但不虚构内核采用——诚实是本系列的底线。
-O2 是油门,下面三个是刹车——全在顶层 Makefile,已验证:
-fno-strict-aliasing:关掉基于类型的别名分析(TBAA)。TBAA 假设"不同类型的指针不指向同一内存",据此大胆重排 load/store。但内核到处 type punning——把一块内存换个类型解读是惯用法(网络包解析、驱动寄存器映射)。开了 TBAA,编译器会"合法地"生成错代码。代价:放弃一整类优化,换正确性。-fno-delete-null-pointer-checks:不许编译器以"解引用 NULL 是未定义行为"为由,删掉解引用之后的 NULL 检查。编译器的逻辑是"你都敢解引用了,说明它不可能是 NULL"——内核说"别替我做这个假设"。barrier()(include/linux/compiler.h,已验证):#define barrier() __asm__ __volatile__("": : :"memory")——编译器屏障,"内存访问不许跨这条线重排"。这是 M18 编译器重排的刹车片:M18 讲编译器为什么敢重排,这一行的存在就是"到此为止"。一句话总结本节:-O2 是"往快了改",这三个是"不许这么改"——优化是油门,契约是刹车,内核两个都要。
为什么默认 -O2 而不是 -O3?
-O3 的激进内联、循环展开让代码膨胀,icache miss 吃掉的那部分经常超过它省下的。内核是 7×24 常驻内存的代码,icache 友好比峰值 IPC 更重要——这是常驻代码和跑一次就退出的用户程序的根本区别。体积敏感场景另有 -Os 兜底,不需要 -O3 来凑。
为什么关掉 strict aliasing,而不是把内核代码改"干净"?
因为 type punning 是全树的惯用法,改"干净"等于重写半个内核;TBAA 带来的那点 load 提升,抵不上引入 subtle miscompile 的风险。务实压倒洁癖——这是内核工程哲学的缩影:正确性面前,优化让路。
likely/unlikely 只是纸条,为什么 jump label 值得动态 patch 代码的复杂性?
因为 tracepoint 这类"几乎恒为假"的分支就在热路径上,likely 只能排布局,省不掉判断指令本身;jump label 把热路径变成一条 nop——判断指令彻底消失。patch 的同步复杂性只付在"条件翻转"那个罕见时刻,一次投入、长期免单。
barrier() 是两关之间的铆钉,一边是重排的权力,一边是刹车的义务。fork 路径是 likely/unlikely 最密集的地方之一——热路径 + 分支提示,是本关机制最直接的用武之地。kmalloc 快路径上的小函数是内联启发式的典型受益者——"小、热、被频繁调用",正是内联赌注胜率最高的三要素。likely() 式断言的现实版——硬件有分支预测,软件有纸条,两边在赌同一件事。出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。
CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE → KBUILD_CFLAGS += -O2;CONFIG_CC_OPTIMIZE_FOR_SIZE → KBUILD_CFLAGS += -Os。关键是"受配置控制"——-O2 不是无条件启用的。
__always_inline(inline __attribute__((__always_inline__)))强制内联,用在热路径小函数,赚调用开销 + 跨函数优化;noinline 反向禁止内联,因为 ftrace/kprobe 需要真实的函数边界来下钩——内联了就无处可挂。
#define likely(x) __builtin_expect(!!(x), 1)——纯编译器提示,不改变语义,只影响代码布局(热路径直行、冷路径跳走)。它也不保证分支预测器听话:纸条 ≠ 命令。
代码膨胀 → icache 压力。函数被多处内联后二进制变大,热代码挤出 L1 icache,一次 icache miss 的代价远超一次 call。内联的赌注输了——这正是启发式内联要算账、而强制内联跳过算账的原因。
type punning 是全树惯用法(网络包解析、驱动寄存器映射),改"干净"等于重写半个内核;TBAA 省下的几个 load 抵不上引入 subtle miscompile 的风险。正确性面前优化让路——关掉整类优化,换"编译器不许拿类型骗我"的确定性。
likely 只排代码布局,判断指令还在;jump label 把"几乎恒为假"的分支编译成 nop,热路径上的判断指令彻底消失。代价是运行期 patch 代码的同步复杂性——但条件翻转极少发生,代价只付在罕见时刻。适用于 tracepoint 这类"几乎不变"的分支,不适用于频繁翻转的条件。
Full LTO 要做全程序分析,vmlinux 的体量下内存/时间不可承受;ThinLTO 用模块摘要做跨模块内联,用"近似全局视野"换"可承受的链接代价"。这是典型的工程折中:拿不到满分,就拿性价比最高的 80 分。
内核的 workload 是整个世界:桌面、服务器、嵌入式、路由器跑的代码路径完全不同,用某一种 profile 指导优化,等于拿"昨天的天气"赌"明天的所有天气"。再加上"插桩→跑→重编"两遍流程的工程代价,内核构建干脆不走 PGO——7.3 Makefile 里零命中就是铁证。
对。barrier()(__asm__ __volatile__("" ::: "memory"))是编译器屏障:内存访问不许跨这条线重排(管顺序,M18 的刹车片);likely() 是分支概率提示:只影响代码布局(管放哪),不改变语义、不约束重排。两者正交,一个管"能不能动",一个管"放哪更顺"。
参考思路:__always_inline——高频短小,调用开销占比大,内联稳赚(但要确认调用点少,避免多处内联导致膨胀);likely 标"成功收包"分支——编译期纸条,零运行期代价;jump label 留给"几乎恒定"的开关(如调试 tracepoint),不用于"几乎总是成功"这种仍有可观失败率的分支——patch 收益要大于翻转时的同步代价。核心判据:频率 × 稳定性——越热越稳,越值得上重手段。
KBUILD_CFLAGS:-O2/-Os 的条件开关、-fno-strict-aliasing、-fno-delete-null-pointer-checks__always_inline、noinline 的定义likely/unlikely、barrier()CONFIG_JUMP_LABEL、CONFIG_LTO_CLANG 的 Kconfig 描述-O2 的"赌注组合"(示意) 源代码 │ -finline-small-functions 小函数内联(启发式) ▼ -ftree-vectorize 循环自动向量化 │ -fdce 死代码删除 ▼ -fgcse / 循环优化 公共子表达式、循环变换 …上百个 pass,每个都是"大概率划算"的小赌注… ▼ -O2 二进制(as-if:可观察行为不变)
-O2 不是一个优化,是一整条 pass 流水线的默认配置。内核在这条流水线之外,另签了"别骗我"契约(-fno-strict-aliasing 等)——油门和刹车都要。
likely/unlikely vs jump label likely(x):编译期纸条 if (likely(ok)) … → 热路径直行,冷路径跳走 代价:判断指令还在,只是放得顺了 jump label:运行期改代码 热路径: nop ← 条件为假时,这里就是一条 nop 翻转时: patch → jmp cold ← 罕见时刻付代价 收益:判断指令彻底消失
纸条是静态的,patch 是动态的。tracepoint、调度器用 jump label 的理由:分支"几乎恒定",值得把判断本身也省掉。
在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。
点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。