-oz是llvm最激进的代码体积优化级别,专为最小化二进制尺寸设计,比-os更彻底地禁用循环展开、降低内联阈值、默认启用dce与字符串合并,并隐式支持thinlto跨模块裁剪,配合--gc-sections链接选项可进一步压缩体积。

直接启用 -Oz 是让 LLVM 编译出更小体积程序最有效、最稳妥的方式。它比 -Os 更激进,专为代码尺寸优化设计,且默认开启死代码消除(DCE)、函数内联控制、字符串字面量合并等关键压缩行为。
为什么 -Oz 比 -Os 更适合减小体积
-Os 在优化体积的同时仍保留一定性能权衡,比如会保留部分调试信息友好结构、对循环展开更保守;而 -Oz 完全以 size 为第一目标:它禁用循环展开、强制函数内联阈值极低、默认启用 -dce 和 -dead-store-elimination,还会合并重复的常量池条目和字符串字面量。
常见误操作是只加 -Os 就以为够了——实际在嵌入式或 WASM 场景下,-Oz 通常能再省 8%~15% 的二进制体积。
-
-Oz自动包含-ffunction-sections和-fdata-sections,为后续链接时裁剪打基础 - 它隐式启用
-flto=thin(ThinLTO)时的跨模块 DCE,这是-Os不具备的能力 - 注意:
-Oz可能导致某些断点失效或行号映射不准,调试时建议切回-O0 -g
链接阶段必须配 --gc-sections
仅靠编译器优化不够。即使 -Oz 把无用函数编译成独立 section,不告诉链接器“丢弃未引用的 section”,它们仍会留在最终二进制里。
使用 ld.lld 或 ld.gold 时,务必加上:
clang -Oz ... -Wl,--gc-sections
如果用 GNU ld,需额外加 -Wl,--allow-multiple-definition 防止因弱符号引发的裁剪失败;而 lld 默认更严格,但更可靠。
- 没加
--gc-sections,-ffunction-sections几乎白开 - WASM 目标(
wasm32-unknown-unknown)必须用lld+--gc-sections,否则标准 libc 启动代码无法裁剪 - 某些静态库(如
libgcc.a)含大量未调用辅助函数,只有--gc-sections能清理掉
opt 工具对已生成 IR 手动做 DCE 和精简
当无法修改编译命令(例如 CI 中只能拿到 .bc 文件),可用 opt 直接处理 LLVM IR:
opt -dce -strip-dead-prototypes -strip-debug -o smaller.bc input.bc
这几个 pass 组合效果明确:
-
-dce:删除无用指令(包括未使用的全局变量初始化、冗余 store) -
-strip-dead-prototypes:移除未实现或未调用的函数声明 -
-strip-debug:删掉所有!dbg元数据(不删-g编译产生的调试段,只清 IR 级注解)
注意:opt 不会重排指令或压缩常量池,所以它补的是编译阶段遗漏的 DCE,不是替代 -Oz。
容易被忽略的膨胀点:C 标准库和启动代码
哪怕用了 -Oz 和 --gc-sections,默认链接的 libc 仍可能引入几百 KB。真实压体积必须干预链接时依赖:
- 用
-nostdlib+ 手写最小 _start,彻底绕过 libc(裸机/WASM 常用) - 换 musl 或 picolibc 替代 glibc,前者默认静态链接且无冗余 symbol
- Clang 下加
-lc之前插-u __cxa_atexit可避免拉入整个 C++ ABI 初始化逻辑 - printf 类函数是大户,改用
snprintf子集或自定义简易输出,可省数十 KB
真正卡体积上限的,往往不是你的业务代码,而是你没意识到正在链接的那部分标准库。











