优化:速度与大小的权衡

每个人都希望他们的程序超级快,超级小,但通常不可能同时拥有这两种特性。本节讨论了 `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 打印堆栈变量和函数参数。当代码被优化时,尝试打印变量会导致打印 `$0 = <value optimized out>`。

`dev` 配置文件最大的缺点是生成的二进制文件将非常大且缓慢。大小通常是一个更大的问题,因为未优化的二进制文件可能会占用数十 KiB 的 Flash,而您的目标设备可能没有 - 结果:您未优化的二进制文件不适合您的设备!

我们可以拥有更小、更友好的调试二进制文件吗?是的,有一个技巧。

优化依赖项

有一个名为 profile-overrides 的 Cargo 功能,它允许您覆盖依赖项的优化级别。您可以使用此功能将所有依赖项优化为大小,同时保持顶级 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

这减少了 6 KiB 的 Flash 使用量,而不会损失顶级 crate 的可调试性。如果您进入依赖项,那么您将开始再次看到那些 `<value optimized out>` 消息,但通常情况下,您希望调试顶级 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 错过优化机会(例如,消除死分支,内联对闭包的调用)。

在优化大小方面,您可能想尝试增加内联阈值,看看它是否对二进制大小有任何影响。更改内联阈值的推荐方法是在 `。cargo/config.toml` 中将 `-C inline-threshold` 标志附加到其他 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

在优化大小方面,您应该尝试 225 和 275。