构建配置

你可以通过改变 Rust 程序的构建配置,而不改变其代码,极大地改变其性能。每个 Rust 程序都有许多可能的构建配置。所选择的配置将影响编译代码的多个特性,如编译时间、运行时速度、内存使用、二进制大小、调试性、可分析性,以及编译后的程序将在哪些架构上运行。

大多数配置选择会在改善一个或多个特性的同时,恶化一个或多个其他特性。例如,一个常见的权衡是为了获得更高的运行时速度而接受更差的编译时间。对于你的程序来说,正确的选择取决于你的需求和你的程序的具体情况,并且与性能相关的选择(这是大多数选择)应该通过基准测试来验证。

请注意,Cargo 只查看工作空间根目录中 Cargo.toml 文件中的配置。在依赖项中定义的配置将被忽略。因此,这些选项大多数情况下只与二进制crate相关,而不是库crate。

发布构建

最重要的构建配置选择是简单但易于忽略的:确保在需要高性能时使用发布构建,而不是开发构建。通常可以通过向 Cargo 指定 --release 标志来完成这一点。

开发构建是默认选项。它们适用于调试,但没有经过优化。如果你运行 cargo buildcargo run,则会生成它们。(另外,运行 rustc 时不带额外选项也会生成一个未经优化的构建。)

考虑一下从 cargo build 运行的输出的最后一行。

Finished dev [unoptimized + debuginfo] target(s) in 29.80s

这个输出表明已经生成了一个开发构建。编译后的代码将被放置在 target/debug/ 目录中。cargo run 将运行开发构建。

相比之下,发布构建要更加优化,省略了调试断言和整数溢出检查,并且省略了调试信息。相对于开发构建,10-100 倍的速度提升是常见的!如果你运行 cargo build --releasecargo run --release,就会生成它们。(另外,rustc 有多个选项用于优化构建,比如 -O-C opt-level。)这通常会比开发构建需要更长的时间,因为需要额外的优化。

考虑一下从 cargo build --release 运行的输出的最后一行。

Finished release [optimized] target(s) in 1m 01s

这个输出表明已经生成了一个发布构建。编译后的代码将被放置在 target/release/ 目录中。cargo run --release 将运行发布构建。

请查看 Cargo 配置文件 中的文档以了解更多关于开发构建(使用 dev 配置)和发布构建(使用 release 配置)之间的区别。

在发布构建中使用的默认构建配置选择提供了编译时间、运行时速度和二进制大小等特性之间的良好平衡。但是有许多可能的调整,如下面的部分所述。

最大化运行时速度

以下构建配置选项主要设计用于最大化运行时速度。其中一些可能也会减小二进制大小。

代码生成单元

Rust 编译器将 crates 分成多个 代码生成单元 来并行化(从而加快)编译。然而,这可能导致它错过一些潜在的优化。通过将单元数设置为一个,你可能能够提高运行时速度并减小二进制大小,但代价是增加编译时间。将以下行添加到 Cargo.toml 文件中:

[profile.release]
codegen-units = 1

示例 1, 示例 2

链接时优化

链接时优化(LTO)是一种整个程序的优化技术,可以通过 10-20% 或更多来提高运行时速度,并减小二进制大小,但代价是更差的编译时间。它有几种形式。

LTO 的第一种形式是 thin local LTO,是一种轻量级的 LTO。默认情况下,编译器会对任何涉及非零级别优化的构建使用它。这包括发布构建。要显式请求此级别的 LTO,请将以下行放入 Cargo.toml 文件中:

[profile.release]
lto = false

LTO 的第二种形式是 thin LTO,这是一种稍微激进一些的形式,很可能会提高运行时速度和减小二进制大小,同时还会增加编译时间。在 Cargo.toml 中使用 lto = "thin" 来启用它。

LTO 的第三种形式是 fat LTO,这是一种更加激进的形式,可能会进一步提高性能并减小二进制大小,但再次增加构建时间。在 Cargo.toml 中使用 lto = "fat" 来启用它。

最后,完全禁用 LTO 也是可能的,这可能会使运行时速度变慢,并增加二进制大小,但会减少编译时间。在 Cargo.toml 中使用 lto = "off" 来实现。请注意,这与 lto = false 选项不同,如上所述,后者保留了 thin local LTO。

替换分配器

可以用另一个分配器替换 Rust 程序使用的默认(系统)堆分配器。确切的影响将取决于个别程序和选择的替换分配器,但在实践中已经看到了运行时速度的大幅提升和内存使用的大幅减少。该效果在不同平台上也会有所不同,因为每个平台的系统分配器都有其优点和缺点。使用替换分配器还可能增加二进制大小和编译时间。

jemalloc

Linux 和 Mac 上一个流行的替换分配器是 jemalloc,可以通过 tikv-jemallocatorcrate 使用。要使用它,请将依赖项添加到你的 Cargo.toml 文件中:

[dependencies]
tikv-jemallocator = "0.5"

然后将以下内容添加到你的 Rust 代码中,例如在 src/main.rs 的顶部:

#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

此外,在 Linux 上,jemalloc 可以配置为使用 transparent huge pages (THP)。这可以进一步加速程序,可能以更高的内存使用为代价。

在构建程序之前,通过在 MALLOC_CONF 环境变量中适当设置来完成这个目标,例如:

MALLOC_CONF="thp:always,metadata_thp:always" cargo build --release

运行编译后的程序的系统也必须配置为支持 THP。更多详情请参阅 此博客文章

mimalloc

另一个适用于许多平台的替换分配器是 mimalloc,可以通过 mimalloc crate使用。要使用它,请将依赖项添加到你的 Cargo.toml 文件中:

[dependencies]
mimalloc = "0.1"

然后将以下内容添加到你的 Rust 代码中,例如在 src/main.rs 的顶部:

#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

CPU 特定指令

如果你不在乎二进制在旧的(或其他类型的)处理器上的兼容性,你可以告诉编译器生成特定于某个 特定的 CPU 架构(如 x86-64 CPU 的 AVX SIMD 指令)的最新(可能也是最快的)指令。

要从命令行请求这些指令,请使用 -C target-cpu=native 标志。例如:

RUSTFLAGS="-C target-cpu=native" cargo build --release

或者,要从 config.toml 文件(针对一个或多个项目)中请求这些指令,添加以下行:

[build]
rustflags = ["-C", "target-cpu=native"]

这可以提高运行时速度,特别是如果编译器在你的代码中发现了向量化机会。

如果你不确定 -C target-cpu=native 是否正常工作,请比较 rustc --print cfgrustc --print cfg -C target-cpu=native 的输出,看看 CPU 特性是否在后一种情况下被正确检测到。如果没有,你可以使用 -C target-feature 来定位特定的特性。

基于配置文件的优化

配置文件是 Rust 的 nightly 版本中的一个实验性功能,可以为你的项目启用或禁用一些实验性功能,从而影响编译时间和生成的代码。要了解更多,请查看 RFC 2994

自定义配置文件

除了 devrelease 配置文件外,Cargo 还支持 自定义配置文件。例如,如果你发现开发构建的运行时速度不够,并且发布构建的编译时间对于日常开发来说太慢,那么创建一个介于 devrelease 之间的自定义配置文件可能会有用。

总结

在构建配置方面有很多选择。以下几点总结了以上信息并给出了一些建议。

  • 如果要最大化运行时速度,请考虑以下所有内容:codegen-units = 1lto = "fat"、替换分配器和 panic = "abort"
  • 如果要最小化二进制大小,请考虑 opt-level = "z"codegen-units = 1lto = "fat"panic = "abort"strip = "symbols"
  • 无论哪种情况,如果不需要广泛的架构支持,请考虑 -C target-cpu=native,并且如果它符合你的发布机制,还有 cargo-pgo
  • 如果你的平台支持的话,一定要使用更快的链接器,因为这样做没有任何坏处。
  • 对所有更改进行基准测试,一个一个地进行,以确保它们有预期的效果。

最后,这个问题跟踪了 Rust 编译器自身的构建配置的演变。Rust 编译器的构建系统比大多数 Rust 程序的构建系统更奇怪、更复杂。尽管如此,这个问题可能有助于说明构建配置选择如何应用于一个大型程序。