Rust
二进制文件大小优化,在优化二进制文件过程中,编译时间会增加,但是一般编译时候的优化会加快程序执行速度
初始化项目
在Ubuntu22
上面进行实验
创建项目
$ cargo new demo
修改Cargo.toml
如下,需要加入一点常用的库
[dependencies]
tokio = {version = "1", features = ["full"]}
tracing = "0.1"
tracing-subscriber = "0.3"
修改main.rs
如下
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
tracing::info!("Hello, world!");
}
}
debug
和release
的大小对比
编译
$ cargo build
$ cargo build -r
查看大小
$ ll target/*
target/debug:
总计 28M
-rwxrwxr-x 2 gong gong 28M 四月 25 15:47 demo
...
target/release:
总计 5.0M
-rwxrwxr-x 2 gong gong 5.0M 四月 25 15:46 demo
....
可见release
版本二进制大约是debug
版本的17%
,后续所有二进制优化采用release 5M
大小为基准,每次优化都只改动一个配置项作为对比
优化符号信息symbols
在Linux
和macOS
上,默认情况下,符号信息包含在编译的.elf
文件中,正确执行二进制文件不需要此信息
配置Cross.toml
[profile.release]
strip = true
编译之后demo
二进制变为887k
,约为之前的17%
,效果好
这个配置项在项目比较大的时候,比如最终二进制是100M
优化效果就不怎么明显了,一般符号信息是会减小几M大小,存在一个上限
配置优化level
opt-level
可选项
0
: 无优化(debug
默认优化)1
: 基本优化2
: 一些优化3
: 全部优化,(release
默认优化)"s"
: 优化输出的二进制文件的大小"z"
: 优化二进制文件大小,但也会关闭循环向量化
配置如下
[profile.release]
opt-level = "z"
优化后demo
变为5.1M
了,二进制反而变大,很多文档都提到要配置这个优化项,但是从来没发现有什么效果
详情参考官方文档
https://doc.rust-lang.org/cargo/reference/profiles.html
启用链接时优化
默认情况下,Cargo
指示编译单元单独编译和优化,LTO
指示链接器在链接阶段进行优化,例如,这可以删除无用代码,有些代码段不会被调用到,并减少二进制大小
默认release
是没有开启这个配置,开启这个配置后如果项目比较大,则会大幅度增加编译时间,项目依赖越多,代码总量越多,优化效果越明显
支持的选项包括:
false
: 只会对代码生成单元中的本地包进行thin LTO
优化,若代码生成单元数为 1 或者opt-level
为 0,则不会进行任何LTO
优化true
或fat
:对依赖图中的所有包进行fat LTO
优化thin
:对依赖图的所有包进行thin LTO
,相比fat
来说,它仅牺牲了一点性能,但是换来了链接时间的可观减少off
: 禁用LTO
[profile.release]
lto = true
编译后二进制变为2.6M
,约为之前的52%
,效果好
减少并行代码生成单元
Cargo
为发布版本指定了16个并行代码单元,这改善了编译时间,但妨碍了一些优化,如果配置为1,就可以最大程度避免并行代码单元中的妨碍优化
[profile.release]
codegen-units = 1
编译后文件大小为4.8M
,约为之前的96%
,效果不怎么明显
配置程序panic
时候的行为
当Rust
代码遇到必须调用panic!()
的情况时,它会展开堆栈并生成有用的回溯,展开代码需要额外的二进制,可以调整Rust
立即中止程序而不是展开栈信息,这样就不需要额外的展开代码
[profile.release]
panic = "abort"
编译后程序大小为4.7M
,约为之前的97.5%
,这样配置的话程序如果报错就看不到什么地方panic
了,会比较影响线上环境体验,不推荐使用
大杀器UPX
upx
是一个二进制压缩工具,支持各类二进制可执行文件的压缩,rust/golang/c.....
,在大部分情况下可以把二进制执行文件压缩到原来的30%
左右,压缩后的文件可以直接由系统执行,支持多系统和平台
项目地址
安装的时候尽量去官方项目的release
获取到最新的release
,里面包含一个upx
可执行文件用于压缩二进制程序
https://github.com/upx/upx
upx
支持不同的压缩级别1-9
,最高压缩级别是9
一般执行upx -9 文件
即可
$ upx -9 target/release/demo
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2023
UPX 4.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 30th 2023
File size Ratio Format Name
-------------------- ------ ----------- -----------
5178056 -> 1173024 22.65% linux/amd64 demo
Packed 1 file.
之后查看二进制可执行文件变为了1.2M
,约为原来的22.6%
linux
上面使用strings
查看二进制文件
查看压缩前的可执行文件内容
$ strings target/release/demo |head -n 10
/lib64/ld-linux-x86-64.so.2
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
_Unwind_Resume
_Unwind_Backtrace
_Unwind_GetLanguageSpecificData
_Unwind_GetIPInfo
_Unwind_GetDataRelBase
_Unwind_GetRegionStart
查看压缩后的内容,可以看到UPX
把开头的信息改写了
$ strings target/release/demo |head -n 10
1tUPX!
m@/H
(O0OS
tdPo
/lib64
nux-x86-
.so.
_ITM_deregist
CloneTable
__gm_.art
原理
采用二进制程序加壳技术,UPX
将程序压缩,并在头部加入解压的程序,加壳过的程序可以直接运行,但是不能查看源代码,要经过脱壳才可以查看源代码
加壳与解压
- 在文件头里加了一段指令,告诉CPU,怎么才能解压自己(在解压时候执行)
- 利用特殊的算法,对
EXE
、DLL
,其他二进制文件里的资源进行压缩,类似zip
的效果 - 压缩之后的文件,可以独立运行,给可执行的文件加上个外衣
- 加壳工具解压过程完全隐蔽,都在内存中完成,用户执行的只是这个外壳程序
- 执行时候,壳就会把原来的程序在内存中解开,解开后,以后的就交给真正的程序
优点
UPX
可以压缩各种类型的可执行文件,压缩效率非常可观- 压缩后的文件可以直接由操作系统执行
- 压缩过程不会修改源文件,也就意味着解压后直接可以得到原始文件
- 不会产生额外的动态库调用
- 没有运行时性能损失
- 反跟踪、被人跟踪调试、防止程序被别人静态分析,保护你程序数据的完整性,防止被程序修改和被窥视内幕
缺点
- 运行的程序不会共享数据段(汇编),所以多实例运行的程序不适合压缩
- 使用
ldd
和size
命令无法获取到程序的有效信息 - 打开时消耗更多的
CPU
资源(执行的时候会使用解压算法对压缩后的可执行文件进行解压运算) - 在运行时占用更多的内存(主要是多了几兆
UPX
解压程序)
总结
很多优化方式一般都是压缩效果不怎么明显,但是会增加编译耗时或者影响程序使用体验
所以采用优化方式如下
- 启用链接时优化
- 除去不必要的
Symbols
- 采用
upx
进一步压缩
配置Cargo.toml
为如下
[profile.release]
strip = true
lto = true
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。