优化:速度与大小的权衡
每个人都希望他们的程序既超级快又超级小,但通常不可能同时拥有这两个特性。本节讨论了 rustc
提供的不同优化级别,以及它们如何影响程序的执行时间和二进制文件大小。
不进行优化
这是默认设置。当您调用 cargo build
时,您使用的是开发(又名 dev
)配置文件。此配置文件针对调试进行了优化,因此它启用了调试信息,并且不启用任何优化,即它使用 -C opt-level = 0
。
至少对于裸机开发而言,调试信息是零成本的,因为它不会占用 Flash/ROM 中的空间,因此我们实际上建议您在发布配置文件中启用调试信息——默认情况下是禁用的。这将让您在调试发布版本时使用断点。
[profile.release]
# symbols are nice and they don't increase the size on Flash
debug = true
不进行优化对于调试非常有用,因为单步执行代码感觉就像您正在逐语句执行程序,此外您还可以在 GDB 中 print
堆栈变量和函数参数。当代码被优化时,尝试打印变量会导致打印 $0 = <值已优化掉>
。
dev
配置文件的最大缺点是生成的二进制文件会非常大且速度慢。大小通常更是一个问题,因为未优化的二进制文件可能会占用数十 KiB 的 Flash,而您的目标设备可能没有这么多空间——结果:您未优化的二进制文件无法放入您的设备!
我们可以拥有更小、对调试器友好的二进制文件吗?是的,有一个技巧。
优化依赖项
Cargo 有一个名为 profile-overrides
的功能,允许您覆盖依赖项的优化级别。您可以使用该功能来优化所有依赖项的大小,同时保持顶层 crate 未优化且对调试器友好。
请注意,泛型代码有时可以与实例化它的 crate 一起优化,而不是与定义它的 crate 一起优化。如果您在应用程序中创建了一个泛型结构的实例,并发现它引入了占用大量空间的代码,则可能是增加相关依赖项的优化级别没有任何效果。
这是一个例子
# Cargo.toml
[package]
name = "app"
# ..
[profile.dev.package."*"] # +
opt-level = "z" # +
没有覆盖
$ cargo size --bin app -- -A
app :
section size addr
.vector_table 1024 0x8000000
.text 9060 0x8000400
.rodata 1708 0x8002780
.data 0 0x20000000
.bss 4 0x20000000
有覆盖
$ cargo size --bin app -- -A
app :
section size addr
.vector_table 1024 0x8000000
.text 3490 0x8000400
.rodata 1100 0x80011c0
.data 0 0x20000000
.bss 4 0x20000000
这在 Flash 使用量上减少了 6 KiB,而顶层 crate 的可调试性没有任何损失。如果您步入一个依赖项,那么您将再次看到这些 <值已优化掉>
消息,但通常情况下,您只想调试顶层 crate 而不是依赖项。如果您确实需要调试一个依赖项,那么您可以使用 profile-overrides
功能将特定依赖项排除在优化之外。请参见下面的示例
# ..
# don't optimize the `cortex-m-rt` crate
[profile.dev.package.cortex-m-rt] # +
opt-level = 0 # +
# but do optimize all the other dependencies
[profile.dev.package."*"]
codegen-units = 1 # better optimizations
opt-level = "z"
现在顶层 crate 和 cortex-m-rt
对调试器友好了!
优化速度
截至 2018-09-18,rustc
支持三个“优化速度”级别:opt-level = 1
、2
和 3
。当您运行 cargo build --release
时,您正在使用发布配置文件,该配置文件默认为 opt-level = 3
。
opt-level = 2
和 3
都以牺牲二进制文件大小为代价来优化速度,但是级别 3
比级别 2
进行更多的向量化和内联。特别是,您会看到在 opt-level
等于或大于 2
时,LLVM 将展开循环。循环展开在 Flash/ROM 方面成本相当高(例如,将此数组清零循环从 26 字节变为 194 字节),但在条件合适的情况下(例如,迭代次数足够大),也可以将执行时间减半。
目前,无法在 opt-level = 2
和 3
中禁用循环展开,因此如果您无法承担其成本,则应优化程序的大小。
优化大小
截至 2018-09-18,rustc
支持两个“优化大小”级别:opt-level = "s"
和 "z"
。这些名称继承自 clang/LLVM,并且不太具有描述性,但 "z"
旨在表示它产生的二进制文件比 "s"
更小。
如果您希望发布二进制文件针对大小进行优化,请更改 Cargo.toml
中的 profile.release.opt-level
设置,如下所示。
[profile.release]
# or "z"
opt-level = "s"
这两个优化级别大大降低了 LLVM 的内联阈值,该阈值是一个用于决定是否内联函数的指标。Rust 的原则之一是零成本抽象;这些抽象倾向于使用大量 newtype 和小函数来保存不变量(例如,像 deref
、as_ref
这样借用内部值的方法),因此较低的内联阈值可能会使 LLVM 错过优化机会(例如,消除死分支,内联对闭包的调用)。
在优化大小的同时,您可能需要尝试增加内联阈值,以查看这对二进制文件大小是否有任何影响。更改内联阈值的推荐方法是将 -C inline-threshold
标志附加到 .cargo/config.toml
中其他的 rustflags。
# .cargo/config.toml
# this assumes that you are using the cortex-m-quickstart template
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
# ..
"-C", "inline-threshold=123", # +
]
使用什么值?截至 1.29.0,这些是不同优化级别使用的内联阈值
opt-level = 3
使用 275opt-level = 2
使用 225opt-level = "s"
使用 75opt-level = "z"
使用 25
当优化大小时,您应该尝试 225
和 275
。