优化:速度与大小的权衡

每个人都希望他们的程序既超级快又超级小,但通常不可能同时拥有这两个特性。本节讨论了 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 = 123。当您运行 cargo build --release 时,您正在使用发布配置文件,该配置文件默认为 opt-level = 3

opt-level = 23 都以牺牲二进制文件大小为代价来优化速度,但是级别 3 比级别 2 进行更多的向量化和内联。特别是,您会看到在 opt-level 等于或大于 2 时,LLVM 将展开循环。循环展开在 Flash/ROM 方面成本相当高(例如,将此数组清零循环从 26 字节变为 194 字节),但在条件合适的情况下(例如,迭代次数足够大),也可以将执行时间减半。

目前,无法在 opt-level = 23 中禁用循环展开,因此如果您无法承担其成本,则应优化程序的大小。

优化大小

截至 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 和小函数来保存不变量(例如,像 derefas_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 使用 275
  • opt-level = 2 使用 225
  • opt-level = "s" 使用 75
  • opt-level = "z" 使用 25

当优化大小时,您应该尝试 225275