二进制小型化优化
1 二进制小型化优化方案
适用范围:C / C++ / Rust / Go / Swift / Android / iOS / 嵌入式固件等场景
1.1 概述与基础概念
1.1.1 为什么要做二进制小型化?
| 场景 | 主要收益 |
|---|---|
| 移动端 App | 降低用户下载门槛、减少卸载率、节省流量 |
| 嵌入式/IoT | Flash/ROM 容量受限,直接决定能否运行 |
| 服务端微服务 | 加速容器镜像拉取、缩短冷启动时间 |
| WebAssembly | 减少网络传输、提升首屏加载速度 |
| 边缘计算 | 硬件算力/存储双受限 |
1.1.2 二进制的组成结构(ELF 为例)
ELF Binary ├── .text ← 可执行代码段(通常最大) ├── .rodata ← 只读数据(字符串常量、查找表) ├── .data ← 已初始化全局/静态变量 ├── .bss ← 未初始化全局/静态变量(不占文件体积) ├── .debug_* ← 调试信息(DWARF) ├── .symtab ← 符号表 ├── .dynsym ← 动态符号表 └── ...1.1.3 衡量指标
- 文件大小(File Size):磁盘/下载占用,用户感知最直接
- 安装大小(Installed Size):解压/安装后的占用,iOS/Android 商店展示
- 内存映像大小(VM Size):运行时进程占用,影响 OOM 风险
- 下载大小(Download Size):经过 gzip/brotli 压缩后,实际网络传输量
1.2 编译器层面优化
1.2.1 优化等级选择
1.2.1.1 GCC / Clang
-O0# 不优化(调试用)-O1# 基础优化,略微缩小代码-O2# 标准优化(速度优先)-O3# 激进优化(可能膨胀二进制)-Os# 针对尺寸优化(= O2 去掉会膨胀体积的 pass)-Oz# 比 Os 更激进的尺寸优化(Clang 独有,速度换体积)-Og# 调试友好优化经验法则:大多数尺寸敏感项目首选
-Os;若能接受性能下降,Clang-Oz可额外节省 5%~15%。
1.2.1.2 MSVC(Windows)
/O1 # 最小代码优化 /O2 # 最快速度(体积可能更大) /Os # 速度与大小之间倾向大小 /GL # 全程序优化(配合 /LTCG 使用)1.2.1.3 Rust
# Cargo.toml [profile.release] opt-level = "z" # 最小体积("s" 次之) lto = true codegen-units = 1 panic = "abort" strip = true1.2.2 链接时优化(LTO / LTCG)
LTO 允许编译器跨编译单元进行分析,消除冗余代码。
# GCC Full LTOgcc-flto-O2-ooutput main.c utils.c# Clang ThinLTO(并行,构建更快)clang-flto=thin-O2-ooutput main.c utils.c# Rust[profile.release]lto="thin"# 或 lto = true(fat LTO,更慢更彻底)⚠️ LTO 会显著增加链接时间和内存消耗,CI 机器需评估资源。
1.2.3 函数/数据节(Section)粒度控制
# GCC/Clang:每个函数/变量单独放入一个节,便于链接器裁剪-ffunction-sections -fdata-sections配合链接器--gc-sections使用,可删除所有未引用的节。
1.2.4 Profile-Guided Optimization(PGO)
PGO 让编译器根据实际运行数据重排热路径,使 CPU 指令缓存命中更好,同时可将冷路径代码移至独立节,便于剔除。
# Step 1:插桩编译clang -fprofile-instr-generate-oapp_instrumented main.c# Step 2:运行典型场景,收集 profraw./app_instrumented llvm-profdata merge-output=app.profdata *.profraw# Step 3:使用 profile 重新编译clang -fprofile-instr-use=app.profdata-O2-oapp main.c1.2.5 其他编译选项
# 不生成异常处理表(若项目不使用 C++ 异常)-fno-exceptions# 不生成 RTTI(若不使用 dynamic_cast / typeid)-fno-rtti# 不展开循环(减少代码膨胀)-fno-unroll-loops# 内联函数阈值(减小可降低代码膨胀)-finline-limit=10# 禁用栈保护(嵌入式等安全不敏感场景)-fno-stack-protector# 使用短枚举(嵌入式)-fshort-enums# 合并重复常量字符串-fmerge-all-constants1.2.6 指令集选择
在嵌入式/ARM 场景中,指令集直接影响代码密度:
# ARM Thumb2 指令集(16/32 位混合,比 ARM 32 位更紧凑)-mthumb# RISC-V:使用压缩指令集扩展(RVC)-march=rv32imc1.3 链接器层面优化
1.3.1 Dead Code Elimination(DCE)
# GNU ld / gold-Wl,--gc-sections# macOS ld-Wl,-dead_strip# lld(LLVM)-Wl,--gc-sections必须配合编译器的
-ffunction-sections -fdata-sections使用,否则无效。
1.3.2 选择高效链接器
| 链接器 | 特点 |
|---|---|
ld(GNU BFD) | 最广泛,但慢,优化能力一般 |
gold | 比 BFD 快,支持 LTO |
lld(LLVM) | 最快,LTO/ThinLTO 支持最好,推荐首选 |
mold | 极速(并行链接),但尺寸优化功能与 lld 相当 |
# 切换到 lld-fuse-ld=lld1.3.3 版本脚本(Version Script)
控制导出的符号,隐藏不必要的符号可减小.dynsym表。
# version.map { global: my_public_api; # 仅导出这个 local: *; # 其余全部隐藏 };gcc -Wl,--version-script=version.map-olibfoo.so foo.c1.3.4 符号可见性
// 方式1:GCC 属性__attribute__((visibility("hidden")))voidinternal_func(){}__attribute__((visibility("default")))voidpublic_api(){}// 方式2:编译器全局默认隐藏// -fvisibility=hidden// 再对需要导出的符号单独标记 default1.3.5 ICF(Identical Code Folding)
将内容完全相同的函数合并为一个,可显著缩减模板/泛型代码膨胀。
# lld-Wl,--icf=all# gold-Wl,--icf=safe# MSVC/OPT:ICF⚠️
--icf=all可能破坏以函数地址作为唯一标识的代码,使用前需测试。
1.3.6 去除 PLT(Procedure Linkage Table)
# 对于已知位置的符号,直接调用而非通过 PLT-Wl,-z,now# 启动时立即解析所有符号(RELRO)-fno-plt# 直接调用,跳过 PLT1.4 代码层面优化
1.4.1 避免模板过度实例化(C++)
// ❌ 每个 T 都生成一套代码template<typenameT>voidprocess(T*ptr,size_t n){/* 大量逻辑 */}// ✅ 类型无关逻辑抽到非模板函数voidprocess_impl(void*ptr,size_t elem_size,size_t n);template<typenameT>inlinevoidprocess(T*ptr,size_t n){process_impl(ptr,sizeof(T),n);// 薄包装}1.4.2 避免虚函数滥用(C++)
虚函数会强制生成 vtable 和 RTTI,且难以被内联/DCE。
// 替代方案:CRTP(Curiously Recurring Template Pattern)template<typenameDerived>classBase{public:voidinterface(){static_cast<Derived*>(this)->impl();}};classConcrete:publicBase<Concrete>{public:voidimpl(){/* 实际逻辑 */}};1.4.3 字符串常量优化
// ❌ 重复字符串面量导致体积膨胀constchar*a="hello world";constchar*b="hello world";// 可能生成两份// ✅ 集中管理字符串// 使用 -fmerge-all-constants 让编译器合并// 或自定义字符串池// 嵌入式:压缩字符串后运行时解压// 或使用 printf 格式字符串代替多个字面量1.4.4 使用[[nodiscard]]与inline控制膨胀(C++17)
// 小型高频函数内联,避免函数调用开销同时不增加代码段inlineintclamp(intx,intlo,inthi){returnx<lo?lo:x>hi?hi:x;}// 大型函数明确禁止内联__attribute__((noinline))voidheavy_init();1.4.5 减少全局对象与静态初始化
全局 C++ 对象的构造函数会生成.init_array段,增加启动代码。
// ❌ 全局对象staticstd::vector<int>g_cache;// 有构造函数// ✅ 延迟初始化 / 使用 PODstaticintg_cache[MAX_SIZE];// POD,无构造函数staticintg_cache_size=0;// 或使用 constinit / constexpr(C++20)constinitstaticConfig g_config={};1.4.6 枚举与类型大小收紧
// 使用尽量小的整数类型uint8_tstatus;// 代替 intuint16_tcount;// 代替 size_t(嵌入式场景)// 位域structFlags{uint8_tenabled:1;uint8_tmode:3;uint8_tlevel:4;};1.4.7 避免异常与 RTTI(C++)
// 编译时关闭异常(-fno-exceptions)后,// 用错误码或 std::expected 替代std::expected<Result,Error>compute(){if(failed)returnstd::unexpected(Error::NotFound);returnresult;}// 或使用 outcome / tl::expected 等库1.4.8 Rust 特有技巧
// 1. 使用 panic = "abort" 避免 panic unwind 代码// 2. 避免动态分配(no_std 场景)#![no_std]#![no_main]// 3. 减少 monomorphization:用 dyn Trait 代替泛型(用运行时成本换体积)fnprocess(handler:&dynHandler){}// 生成一份代码fnprocess<H:Handler>(handler:&H){}// 每个 H 生成一份// 4. 使用 #[inline(never)] 阻止过度内联#[inline(never)]fnheavy_function(){}// 5. 裁剪 std 功能// 在 Cargo.toml 中禁用默认 features[dependencies]serde={version="1",default-features=false,features=["derive"]}1.5 资源与数据优化
1.5.1 资源压缩
| 资源类型 | 优化方案 |
|---|---|
| PNG 图片 | pngquant(有损压缩)、optipng/zopflipng(无损) |
| JPEG | mozjpeg、jpegoptim |
| WebP | 比 PNG/JPEG 更小,建议迁移 |
| SVG | svgo去除冗余属性 |
| 音频 | opus/aac 代替 mp3,适当降低比特率 |
| 视频 | AV1/HEVC,或按需流式加载 |
| 字体 | 子集化(subsetting)、woff2格式、pyftsubset |
1.5.2 查找表(LUT)替代运算
// 计算成本换体积(运行时计算 vs 静态表)// 三角函数表、CRC 表等可预计算后编译进二进制// 嵌入式中也可在运行时生成(RAM 换 Flash)// 或使用 Q 格式定点数代替浮点运算,减少浮点库依赖1.5.3 嵌入资源到二进制
# GNU ld objcopyobjcopy-Ibinary-Oelf32-littlearm logo.png logo.o# 生成 _binary_logo_png_start / _end / _size 符号# CMake + xxdxxd-iresource.bin>resource.h1.5.4 按需加载资源
避免将所有资源打包进二进制,改为运行时按需从网络/文件系统加载:
- Android:AAB(App Bundle)按设备配置分发资源
- iOS:On-Demand Resources(ODR)
- PC/服务端:DLC、资源热更新
1.6 依赖库管理
1.6.1 静态链接 vs 动态链接
| 维度 | 静态链接 | 动态链接 |
|---|---|---|
| 单文件体积 | 更大(含库代码) | 更小(依赖系统库) |
| 安装总体积 | 多应用共享时更大 | 共享库只有一份 |
| DCE 效果 | 好(链接器可裁剪) | 差(整个 .so 必须保留) |
| 部署灵活性 | 好(无依赖) | 需确保库版本兼容 |
结论:若目标是单二进制小型化,优先静态链接 +
--gc-sections;若目标是系统总体积,使用动态链接共享系统库。
1.6.2 替换重量级依赖
| 场景 | 重型库 | 轻量替代 |
|---|---|---|
| JSON 解析 | RapidJSON(功能全) | jsmn、yyjson |
| HTTP 客户端 | libcurl | picohttp、llhttp |
| 正则表达式 | PCRE2 | re2(精简版)、tiny-regex |
| 日志 | log4cpp | spdlog(header-only)、NanoLog |
| 压缩 | zlib | miniz(单文件)、lz4(更快更小) |
| TLS | OpenSSL | mbedTLS、BearSSL(嵌入式友好) |
| C++ 标准库 | libstdc++ | libc++(通常更小)、musl libc |
1.6.3 使用--as-needed链接标志
# 只链接实际用到的动态库,不引入无用的 .so 依赖-Wl,--as-needed1.6.4 Rust:cargo-bloat 分析依赖贡献
cargoinstallcargo-bloatcargobloat--release--crates# 按 crate 显示体积贡献cargobloat--release-n20# 显示最大的 20 个函数1.6.5 Go:按需 build tags 裁剪
//go:build !windows# 排除 CGOCGO_ENABLED=0go build-oapp.# 仅使用纯 Go 实现的网络库go build-tagsnetgo-oapp.1.7 调试信息处理
1.7.1 Strip 符号表
# 完全剥离(生产发布)strip --strip-all output# 仅剥离调试信息,保留符号表(可用于符号化崩溃)strip --strip-debug output# macOSstrip-xoutput# 删除局部符号strip-Soutput# 删除调试符号# objcopy 方式(可同时保留带调试的副本)objcopy --only-keep-debug output output.dbg# 提取调试信息objcopy --strip-debug output# 剥离原文件objcopy --add-gnu-debuglink=output.dbg output# 添加链接1.7.2 Split DWARF(DWP)
将 DWARF 调试信息拆分到独立.dwo文件,发布时不随二进制分发。
# GCC/Clang-gsplit-dwarf# 打包所有 .dwo 为一个文件llvm-dwp-eoutput-ooutput.dwp1.7.3 压缩调试信息(开发阶段)
# 压缩 DWARF 节,减少 debug 构建体积(不影响 release)-gz=zlib# GCC/Clang 支持1.7.4 最小化发布构建
RELEASE_FLAGS := -O2 -DNDEBUG -ffunction-sections -fdata-sections \ -fno-exceptions -fno-rtti -fvisibility=hidden RELEASE_LDFLAGS := -Wl,--gc-sections -Wl,--strip-all \ -Wl,--icf=all -flto1.8 平台专项优化
1.8.1 Android
1.8.1.1 R8 / ProGuard(Java/Kotlin)
// build.gradleandroid{buildTypes{release{minifyEnabledtrue// 开启代码压缩shrinkResourcestrue// 开启资源压缩proguardFilesgetDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro'}}}1.8.1.2 App Bundle(AAB)
# AAB 按设备 ABI / 语言 / 屏幕密度分发,避免打包所有资源./gradlew bundleRelease1.8.1.3 NDK 层
# CMakeLists.txt target_compile_options(mylib PRIVATE -Os -ffunction-sections -fdata-sections) target_link_options(mylib PRIVATE -Wl,--gc-sections -Wl,--icf=safe) set_target_properties(mylib PROPERTIES LINK_FLAGS "-s") # strip1.8.1.4 ABI 过滤
android{defaultConfig{ndk{// 仅支持主流 ABI,减少包体积abiFilters'arm64-v8a','x86_64'}}}1.8.2 iOS
1.8.2.1 编译器优化
// Xcode Build Settings SWIFT_OPTIMIZATION_LEVEL = -Osize // Swift GCC_OPTIMIZATION_LEVEL = s // ObjC/C DEAD_CODE_STRIPPING = YES ENABLE_BITCODE = NO // Bitcode 已废弃,不再需要 STRIP_INSTALLED_PRODUCT = YES STRIP_STYLE = all1.8.2.2 Swift 特定
// 使用 @inlinable 谨慎内联,避免不必要的模块间代码复制@inlinablepublicfunchot_path(){...}// 使用 final 阻止动态派发,便于优化finalclassMyService{...}// 开启 Whole Module Optimization// SWIFT_WHOLE_MODULE_OPTIMIZATION = YES1.8.2.3 On-Demand Resources
将大型资源(关卡数据、字体等)声明为 ODR,下载时才获取:
letrequest=NSBundleResourceRequest(tags:["level-5"])request.beginAccessingResources{errorin...}1.8.3 WebAssembly
# Emscriptenemcc-Os-flto--closure1\-sFILESYSTEM=0\-sASSERTIONS=0\-sMALLOC=emmalloc\# 更小的 malloc 实现-oapp.wasm main.c# wasm-opt(Binaryen 后处理,额外优化)wasm-opt-Oz--enable-mutable-globals app.wasm-oapp.opt.wasm# Rust + wasm-packwasm-pack build--releasewasm-opt-Ozpkg/app_bg.wasm-opkg/app_bg.opt.wasm1.8.4 嵌入式 / 裸机(Bare Metal)
// 1. 去除 C 标准库,使用 newlib-nano 或自定义 syscall// 链接 newlib-nano(ARM)// --specs=nano.specs --specs=nosys.specs// 2. 去除浮点支持(软浮点 + 不使用 libm)-mfloat-abi=soft-mfpu=none// 3. 自定义 startup 代码,去除未使用的初始化// 替换 crt0.o 中的 __libc_init_array// 4. 使用链接脚本精确控制段布局// 将热路径函数放入 ITCM(指令紧耦合内存)__attribute__((section(".itcm_text")))voidhot_isr_handler(){}1.8.5 Go
# 禁用 CGO(纯 Go 二进制更小,无需 glibc)CGO_ENABLED=0go build-oapp.# 去除调试信息和符号表go build-ldflags="-s -w"-oapp.# UPX 压缩(运行时自解压,冷启动有延迟)upx--bestapp# 使用 trimpath 去除源码路径(减小符号信息)go build-trimpath-oapp.# goblin:实验性的 Go binary 小型化工具# github.com/nicholasgasior/goblin1.9 分析与度量工具
1.9.1 符号大小分析
# nm:列出所有符号及大小nm --print-size --size-sort--radix=d output|tail-30# objdump:反汇编 + 节大小objdump-houtput# 各节大小objdump-toutput|sort-k5# 符号排序# size:快速查看各段大小size output size-Aoutput# 详细模式1.9.2 专用分析工具
1.9.2.1 Bloaty McBloatface(强烈推荐)
# 安装gitclone https://github.com/google/bloaty&&cdbloaty cmake-Bbuild&&cmake--buildbuild# 基础分析bloaty output# 按编译单元分解bloaty output-dcompileunits# 对比两个版本的差异(关键!用于 CI 中检测体积回归)bloaty new_binary -- old_binary# 输出 CSV 供进一步处理bloaty output-dsymbols--csv>symbols.csv1.9.2.2 cargo-bloat(Rust)
cargobloat--release--crates-n201.9.2.3 Android 专用
# apkanalyzer(Android SDK 自带)apkanalyzer apk summary app-release.apk apkanalyzer dex packages app-release.apk apkanalyzer manifest print app-release.apk1.9.2.4 iOS 专用
# Xcode 内置 App Size Report# Product → Archive → Distribute App → 选择 Development → 查看 App Thinning Size Report# otoolotool-lapp.o|grep-A4"sectname"1.9.3 可视化工具
| 工具 | 平台 | 功能 |
|---|---|---|
| Bloaty | 通用 | 层次化树形分析 |
| Binary Size Analyzer | ELF | 交互式 Web 可视化 |
| cargo-bloat | Rust | crate/函数级分析 |
| Twiggy | Wasm/ELF | 调用图分析,找出"保活"根因 |
| Weigh | Rust | 类型大小分析 |
| Android Profiler | Android | APK 内容树形图 |
| Xcode Memory Graph | iOS | 对象占用分析 |
1.9.4 段大小追踪脚本示例
#!/usr/bin/env python3# track_size.py —— 记录每次构建的二进制大小,便于趋势分析importsubprocess,json,datetime,pathlib,osdefget_section_sizes(binary):out=subprocess.check_output(["size","-A",binary]).decode()sizes={}forlineinout.splitlines()[1:]:parts=line.split()iflen(parts)>=2:sizes[parts[0]]=int(parts[1])returnsizes binary="./build/app"sizes=get_section_sizes(binary)record={"timestamp":datetime.datetime.utcnow().isoformat(),"commit":os.getenv("GIT_COMMIT","local"),"total_file":pathlib.Path(binary).stat().st_size,"sections":sizes,}log=pathlib.Path("size_history.jsonl")withlog.open("a")asf:f.write(json.dumps(record)+"\n")print(json.dumps(record,indent=2))1.10 CI/CD 集成
1.10.1 体积预算(Size Budget)
在 CI 中设置阈值,超标则 fail build:
# .github/workflows/size_check.yml-name:Check binary sizerun:|SIZE=$(stat -c%s build/app) MAX=5242880 # 5 MB if [ "$SIZE" -gt "$MAX" ]; then echo "❌ Binary too large: ${SIZE} bytes (limit: ${MAX})" exit 1 fi echo "✅ Binary size OK: ${SIZE} bytes"1.10.2 PR 体积对比(Bloaty + GitHub Actions)
-name:Compare binary size with mainrun:|git fetch origin main git checkout origin/main -- build/app mv build/app build/app_main git checkout HEAD -- build/app bloaty build/app -- build/app_main \ --domain=vm -d sections 2>&1 | tee size_diff.txt cat size_diff.txt >> $GITHUB_STEP_SUMMARY1.10.3 自动生成 Size Report
-name:Generate size reportrun:|{ echo "## 📦 Binary Size Report" echo "\`\`\`" bloaty build/app -d compileunits -n 20 echo "\`\`\`" } >> $GITHUB_STEP_SUMMARY1.11 各语言速查表
1.11.1 C / C++
# 编译CFLAGS=-Os-ffunction-sections -fdata-sections\-fno-exceptions -fno-rtti-fvisibility=hidden\-flto# 链接LDFLAGS=-Wl,--gc-sections -Wl,--icf=all\-Wl,--strip-all -fuse-ld=lld-flto\-Wl,--as-needed1.11.2 Rust
# Cargo.toml [profile.release] opt-level = "z" lto = "thin" codegen-units = 1 panic = "abort" strip = "symbols" overflow-checks = false1.11.3 Go
CGO_ENABLED=0go build\-ldflags="-s -w"\-trimpath\-oapp.1.11.4 Swift
SWIFT_OPTIMIZATION_LEVEL = -Osize DEAD_CODE_STRIPPING = YES STRIP_INSTALLED_PRODUCT = YES SWIFT_COMPILATION_MODE = wholemodule1.11.5 WebAssembly(Rust)
[profile.release] opt-level = "z" lto = true # .cargo/config.toml [target.wasm32-unknown-unknown] rustflags = ["-C", "link-arg=--export-dynamic"]wasm-opt-Oz--strip-debug input.wasm-ooutput.wasm1.12 优化优先级建议
按照投入产出比排序,建议按此顺序逐步尝试:
| 优先级 | 措施 | 预期收益 | 风险 |
|---|---|---|---|
| ⭐⭐⭐⭐⭐ | -Os/-Oz优化等级 | 10%~30% | 低 |
| ⭐⭐⭐⭐⭐ | --gc-sections+-ffunction-sections | 5%~40% | 低 |
| ⭐⭐⭐⭐⭐ | strip调试信息 | 30%~60% | 无(保留 .dbg) |
| ⭐⭐⭐⭐ | LTO(ThinLTO 优先) | 5%~20% | 中(构建变慢) |
| ⭐⭐⭐⭐ | ICF(Identical Code Folding) | 3%~15% | 中 |
| ⭐⭐⭐⭐ | -fno-exceptions -fno-rtti | 5%~15% | 高(需代码改造) |
| ⭐⭐⭐ | 符号可见性控制 | 1%~5% | 低 |
| ⭐⭐⭐ | 替换重型依赖库 | 项目相关 | 高(需重测) |
| ⭐⭐⭐ | 资源压缩(图片/字体) | 项目相关 | 低 |
| ⭐⭐ | PGO 优化 | 5%~10% | 高(流程复杂) |
| ⭐⭐ | 模板实例化优化 | 项目相关 | 中 |
| ⭐ | 自定义 malloc(嵌入式) | 5%~20% | 高 |
| ⭐ | UPX 压缩 | 30%~50% | 冷启动变慢 |
1.13 参考资料
- Google Bloaty 文档
- Minimizing Rust Binary Size
- Android App Size 优化官方指南
- iOS App Thinning 文档
- LLVM LTO 文档
- Clang 属性参考
- Embedded Rust Book
- Binaryen wasm-opt
