• 作者:老汪软件技巧
  • 发表时间:2024-09-29 00:02
  • 浏览量:

临时使用不同工具链

如果我们只想在某个项目使用不同的工具链,不改变全局的默认设置,可以使用:

rustup override set 

例如:

rustup override set nightly-x86_64-apple-darwin

这个命令只会在当前目录下使用指定的工具链,不会影响其他项目或全局的默认设置。

切换为特定版本的 Rust

我们也可以切换到已安装的特定版本,比如 1.75.0:

rustup default 1.75.0-x86_64-apple-darwin

2.4 SIMD

在v8官网中有这么一篇文章 - Fast, parallel applications with WebAssembly SIMD里面就介绍了很多关于SIMD的内容,我们来将与我们相关的内容做一下总结。

SIMD(单指令多数据)指令是一类特殊指令,能够通过同时对多个数据元素执行相同操作来利用数据并行性。这类指令广泛应用于计算密集型应用中,比如音频/视频编解码器、图像处理器等,能够加速性能。大多数现代体系结构都支持某种形式的 SIMD 指令。

我们可以从caniuse中看到它的兼容性情况。

将Rust编译为WebAssembly SIMD

在Fast, parallel applications with WebAssembly SIMD文章中,它介绍了如何将c/c++的代码编译为SIMD以供前端环境使用。当然,也有将Rust编译为SIMD的方式。其实我们比较关心这部分。

此举有助于将 Rust 代码高效地编译为 WebAssembly并利用底层硬件的并行性

当我们将 Rust 代码编译为目标 WebAssembly SIMD 时,需要启用simd128 LLVM 特性。

我们可以直接控制 rustc 的标志或通过环境变量 RUSTFLAGS,可以传递 -C target-feature=+simd128:

rustc … -C target-feature=+simd128 -o out.wasm

或者使用 Cargo:

RUSTFLAGS="-C target-feature=+simd128" cargo build

当启用了 simd128 特性时,LLVM 的自动矢量化器会默认在优化代码时启用。

2.5 binaryen

Binaryen 是一个为 WebAssembly 设计的编译器和工具链基础库,由 C++ 编写。它旨在让编译为 WebAssembly 变得简单、快速且高效

Binaryen为我们提供了很多优化工具,而今天我们选择其中的一个也就是-wasm-opt。

安装wasm-opt

wasm-opt 是 Binaryen 工具的一部分,可以通过多种方式安装,下面列出了几种常用的安装方法:

使用 npm 安装:安装完成后,wasm-opt 会成为全局命令,直接在终端中使用

npm install -g binaryen

通过 Homebrew 安装(适用于 macOS 和 Linux)

brew install binaryen

通过预编译二进制文件安装:前往 Binaryen的GitHub releases 页面,下载与你的操作系统相匹配的压缩包。验证安装

安装完成后,我们可以通过以下命令检查 wasm-opt 是否安装成功:

wasm-opt --version
// wasm-opt version 119 (version_119)

如果返回版本号,说明安装成功。

3. 常规编译

我们之前在Rust 编译为 WebAssembly 在前端项目中使用就介绍过,如何将一个Rust项目编译为WebAssembly。

当时我们使用常规的编译方式。

cargo build --release --target wasm32-unknown-unknown --package xxx
wasm-bindgen target/wasm32-unknown-unknown/release/xxx.wasm --out-dir yyy --target web

上面我们是通过cargo和wasm-bindgen编译Rust文件为WebAssembly,然后在yyy的文件下生成相关的文件资源。

xxx.wasmxxx.jsxxx.d.ts

然后,我们就可以将yyy的相关文件引入到前端项目中,通过配置Webpack/Vite的Wasm相关内容,就可以通过import引入对应的实例或者方法了。

release的默认profiles配置

在Cargo Book中对release的默认profiles配置有相关介绍。

当我们在使用cargo build --release对项目进行打包处理时候,它内部默认是根据下面的配置优化相关项目的。

[profile.release]
opt-level = 3
debug = false
split-debuginfo = '...'  # Platform-specific.
strip = "none"
debug-assertions = false
overflow-checks = false
lto = false
panic = 'unwind'
incremental = false
codegen-units = 16
rpath = false

针对上面各个属性的解释,大家可以翻看release相关解释去了解更多,这里就不在赘述了。

效果展示

下面的所有的效果展示,和自己的本机环境息息相关,也就是如果你在编译/执行项目时,电脑资源被占用的很多或者电脑过热。这个时间也是有波动的。 最终的时间对比,按自己的情况而定。

资源大小

首先,我们先看编译后的文件大小

编译时间

运行时间

我们将上面编译好的文件引入到之前我们的OCR的前端项目。然后,运行相关代码。

在执行相关的操作后,整体的运行时间为4秒

4. 优化编译详解

写在最前面,下面的一些配置,有最大力度的优化方案,但是可能根据项目性质的不同,你使用了,却没达到想要的效果。这就是一个取舍问题,也是一个实践出真知的问题。要想将自己的项目配置成最好,下面的配置方案可能适用你,也可能不使用。如果不适用,你可以根据下面的配置方向,找出符合你的最佳方案。

我们来针对上面的打包做一次优化处理。我们先把相关的优化方案列举出来,然后最后给一个最终的解决方案。

4.1 删除符号或调试信息

这部分,我们可以通过设置release-strip的信息来优化编译结果。

[profile.release]
strip = true  

在 Rust 项目中,strip它决定了 rustc是否从生成的二进制文件中删除符号或调试信息。这个选项主要用于减小生成文件的大小,特别是在发布(release)模式下打包时。

strip 的选项

除了字符串值外,还可以使用布尔值进行设置:

场景说明

在 Linux 和 macOS 系统上,编译生成的 .elf 文件中默认会包含符号信息。这些符号信息通常不需要在执行二进制文件时使用,因此可以选择剥离,以减小文件大小。尤其是在发布模式下,剥离符号信息是常见的做法,用来生成更小、更优化的可执行文件。

4.2 设置opt-level

Rust 的 opt-level 设置控制 rustc 的 -C opt-level 标志,它用于决定编译时的优化级别。

优化级别越高,生成的代码在运行时可能越快,但同时也会增加编译时间,并且更高的优化级别可能会对代码进行重排和改动,这可能会使调试更加困难。

可用的优化级别

这里我们选择大力出奇迹直接使用最高级别的优化。

[profile.release]
opt-level = "s" 

其实,每个项目的优化力度是不同的,这个需要根据自己项目去决定

4.3 Link Time Optimization (LTO)

Link Time Optimization (LTO) 是一种优化技术,它将编译单元在链接阶段进行优化。通常情况下,Cargo 会将每个编译单元独立编译和优化,而 LTO 允许在整个程序的链接阶段对其进行优化。这可以去除不需要的代码(例如死代码),并且在许多情况下会减小二进制文件的大小。这个和我们前端的TreeSharke是一个道理。

lto 设置的选项

这里我们也是下猛药。直接使用最大力度的优化方案。

_前端编译命令_前端的编译器

LTO 的配置方法

[profile.release]
lto = true

4.4 设置并行代码生成单元

在 Rust 中,代码生成单元(codegen-units) 是编译器将 crate 拆分为多个部分并行处理的机制。通过增加代码生成单元,编译器可以并行处理多个部分,从而加快编译速度。然而,更多的代码生成单元会限制某些全局优化的能力,从而可能导致较大的二进制文件或运行速度稍慢的代码。

减少代码生成单元数,尤其是在发布模式下,将有助于 Rust 编译器执行更深入的全局优化,生成更高效和更小的二进制文件。在性能需求高或者文件大小敏感的场景下,将 codegen-units 设置为 1 是一种常见的优化手段。

并行代码生成单元的设置

Rust 默认在发布构建中将 crate 分成 16 个并行代码生成单元。这种设置有助于加快编译速度,特别是在多核 CPU 上,因为多个单元可以同时生成代码。然而,这会限制编译器进行某些全局优化,例如跨模块优化,影响代码运行时的性能或二进制文件的大小。

权衡我们的选择

我们选择将codegen-units设置为1,牺牲编译速度,减少文件大小。

[profile.release]
codegen-units = 1

4.5 修改panic!()行为

当 Rust 代码执行 panic!() 时,默认的行为是 展开栈(unwinding the stack),从而生成有用的回溯信息(backtrace),以帮助我们定位问题。然而,栈展开过程需要额外的代码,这增加了二进制文件的大小。

为了减少二进制文件的大小,Rust 提供了另一种策略,即在程序出现 panic!() 时,立即 终止进程(abort),而不是展开栈。通过启用这种行为,可以完全去掉栈展开代码,显著减少程序的二进制大小。

启用 Abort on Panic

在 Cargo.toml 中通过在发布配置下设置 panic = "abort" 来启用此功能:

[profile.release]
panic = "abort"

使用 "abort" 策略可以有效减少二进制文件的大小,特别适合生产环境和资源受限的场景,但会牺牲部分调试能力和安全性,所以这就要求我们在前端环境做一些容错机制。

4.6 移除位置信息

在 Rust 中,默认情况下,panic!() 和 #[track_caller] 特性会生成文件、行号和列号的位置信息,用于在代码运行出错时提供更有用的回溯信息(traceback)。这些信息对调试非常有帮助,但也会增加二进制文件的大小。

为了进一步减小生成的二进制文件的大小,Rust 提供了一个实验性的功能,可以移除这些位置信息。这通过使用 rustc 的不稳定选项 -Zlocation-detail 来实现。

有效的 location-detail 选项:移除位置信息

通过设置 RUSTFLAGS 环境变量并将其值设为 -Zlocation-detail=none,我们可以在构建二进制时移除这些位置信息,从而减少文件大小。这种优化特别适用于生产环境,或者对二进制文件大小有较高要求的项目。

示例命令如下:

$ RUSTFLAGS="-Zlocation-detail=none" cargo +nightly build --release

从上面可以看到,有一段cargo +nightly。这说明啥,这需要我们切换到nightly版本。

4.7 移除 fmt::Debug

在 Rust 中,#[derive(Debug)] 和 {:?} 格式化符号用于调试输出,帮助我们打印结构体和枚举的内部信息。然而,调试功能会在生成的二进制文件中包含大量类型信息和格式化函数,这可能会增加文件大小。

fmt-debug 选项说明:移除 fmt::Debug:

Rust 提供了一个实验性选项 -Zfmt-debug,允许将 #[derive(Debug)] 和 {:?} 格式化操作变成空操作(no-op),即不输出任何调试信息。通过这种方式,派生的 Debug 实现和相关的字符串将被移除,从而减小二进制文件的大小。

可以使用如下命令启用该功能:

$ RUSTFLAGS="-Zfmt-debug=none" cargo +nightly build --release

和之前的location-detail一样,开启该项目功能,我们也需要使用Rust的nightly版本。

4.8 进一步优化 panic

panic_immediate_abort,旨在彻底移除 panic!() 相关的字符串格式化逻辑。这是 panic = "abort" 选项的进一步优化,即便已指定了 panic = "abort",Rust 仍然会默认将一些与 panic!() 相关的字符串和格式化代码包含在最终的二进制文件中。这会导致二进制文件中存在不必要的占用空间,尤其是在极致优化二进制大小的场景下。

如何使用

配置方式如下:

   $ cargo +nightly build \
   -Z build-std=std,panic_abort \
   -Z build-std-features=panic_immediate_abort 

使用 build-std 重新构建 libstd:按照 build-std 的流程,重新编译标准库,同时启用 panic_abort 行为。

进一步缩小二进制大小:启用 panic_immediate_abort 特性后,所有与 panic!() 相关的字符串信息和格式化逻辑都将被移除。

4.9 开启simd128

之前我们就说过,我们可以对Rust开启simd128。

RUSTFLAGS="-C target-feature=+simd128" cargo build

这里就不在过多解释了。

4.10 优化wasm-bindgen

之前的优化都是针对Rust部分,下面我们来看看,针对wasm-bindgen的优化角度。

之前我们不是,使用wasm-bindgen为 Rust 编写的 WebAssembly 模块生成 JavaScript 绑定。它可以帮助 Rust 和 JavaScript 之间进行高效的数据交互。

wasm-bindgen target/wasm32-unknown-unknown/release/audioAndVideo.wasm --out-dir js/dist/ --target web

我们还可以在后面添加一下配置,来优化生成的代码。

--reference-types

--reference-types 通过允许 WebAssembly 直接引用 JavaScript 对象,提高了效率,减少了不必要的转换。

--weak-refs

--weak-refs 通过启用弱引用,改善了内存管理,防止内存泄漏,确保不必要的对象能被及时回收。

4.11 使用wasm-opt

由于,我们在前面已经下载了wasm-opt了。所以,这里我们就直接上代码了。

我们在Rust项目中构建一个tools/optimize-wasm.sh文件。

内容如下:

#!/bin/sh
set -eu
BIN_PATH="${1:-}"
WASMOPT_BIN=$(which wasm-opt || true)
if [ -z "$BIN_PATH" ]; then
  echo "Usage: $(basename "$0") "
  exit 1
fi
if [ -z "$WASMOPT_BIN" ]; then
  echo '由于未找到 `wasm-opt` 二进制文件,因此跳过编译后优化。'
  exit
fi
if [ -n "${SKIP_WASM_OPT:-}" ]; then
  echo  "由于设置了 SKIP_WASM_OPT,所以跳过了编译后优化"
  exit
fi
wasm-opt --enable-simd --enable-reference-types -O2 "$BIN_PATH" -o "$BIN_PATH".optimized
mv "$BIN_PATH.optimized" "$BIN_PATH"

上面代码,最关键的就是

wasm-opt --enable-simd --enable-reference-types -O2 "$BIN_PATH" -o "$BIN_PATH".optimized
mv "$BIN_PATH.optimized" "$BIN_PATH"

最后,通过移动 (mv) 优化后的文件替换原始文件。

5. 最终方案配置Cargo.toml

[profile.release]
strip = true  
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"

构建shell 文件

注意下文中的xxx需要替换成你项目的名称

build.sh

我们在项目根目录构建一个build.sh文件,内容如下

#!/bin/bash
# 执行 optimize-rust.sh
echo "执行 optimize-rust.sh..."
./tools/optimize-rust.sh
# 检查是否成功执行
if [ $? -ne 0 ]; then
    echo "optimize-rust.sh 构建失败."
    exit 1
fi
# 执行 optimize-wasm.sh
echo "执行 optimize-wasm.sh..."
./tools/optimize-wasm.sh js/dist/xxx_bg.wasm
# 检查是否成功执行
if [ $? -ne 0 ]; then
    echo "optimize-wasm.sh 构建失败."
    exit 1
fi
echo "Rust 已构建成功,到指定目录查看相关信息."

我们构建一个tools文件,然后新建两个文件

optimize-rust.shoptimize-wasm.shoptimize-rust.sh

#!/bin/bash
# 获取项目名称
PACKAGE_NAME="xxx"
# 编译 Rust 代码
RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none -C target-feature=+simd128" cargo +nightly build \
    -Z build-std=std,panic_abort \
    -Z build-std-features=panic_immediate_abort \
    --release --target wasm32-unknown-unknown --package "$PACKAGE_NAME"
# 生成 wasm 绑定
wasm-bindgen target/wasm32-unknown-unknown/release/"$PACKAGE_NAME".wasm --out-dir js/dist/ --target web

optimize-wasm.sh

#!/bin/sh
set -eu
BIN_PATH="${1:-}"
WASMOPT_BIN=$(which wasm-opt || true)
if [ -z "$BIN_PATH" ]; then
  echo "Usage: $(basename "$0") "
  exit 1
fi
if [ -z "$WASMOPT_BIN" ]; then
  echo '由于未找到 `wasm-opt` 二进制文件,因此跳过编译后优化。'
  exit
fi
if [ -n "${SKIP_WASM_OPT:-}" ]; then
  echo  "由于设置了 SKIP_WASM_OPT,所以跳过了编译后优化"
  exit
fi
wasm-opt --enable-simd --enable-reference-types -O2 "$BIN_PATH" -o "$BIN_PATH".optimized
mv "$BIN_PATH.optimized" "$BIN_PATH"

然后,我们就可以在Rust项目根目录执行./build.sh来执行编译任务了。

运行结果文件大小

可以看到,我们将之前1.4MB的资源缩小到了900KB。

如果我们还想减少二进制文件的大小,我们还可以继续更改上面的配置信息。如果单纯的追求资源大小的话,我们可以将其缩小到300kb,但是,其运行时间会比没瘦身之前还长

鱼和熊掌不能兼得,我们只能根据实际情况而定。也就是实践出真知

运行时间

可以看到,虽然文件大小变小了,但是我们运行性能却没有打折扣。那就充分说明,我们此次的瘦身是成功的。

后记