Rust性能很高?不,它完全顶不住我的代码

序言

最近我给 Rspack 提了一个 RP(Pull Request),用于完善 SourceMapDevToolPlugin 的功能和配置选项。你可以在这个链接查看 PR 的具体内容:github.com/web-infra-d...

PR 的改动比较大。在完成了新功能并编写了必要的测试用例后,我兴奋地创建了 PR 并期待着评审。评审结果显示功能实现得不错,但考虑到代码的大幅度变更,我被要求进行性能基准测试。测试结果让人有些失望,因为性能显著下降了。

针对性能下降的问题,我投入了三天时间来解决。由于我还是 Rust 领域的新手,对如何进行 Rust 性能分析并不太了解,这个任务较为困难。

经过反复尝试多种方法后,我发现在 Mac 电脑上,用 Instruments 工具做 Rust 应用的 CPU 时间性能分析是最为有效的手段。本文将详细介绍如何利用 Instruments 对 Rust 应用进行 CPU/时间性能分析。

Instruments 的 Time Profiler 工具

Instruments 是内嵌于 macOS 的 Xcode 开发环境中的一个强大的分析工具集。它不仅可用于监测内存使用、文件 I/O 和网络活动等领域,还可以帮助开发者深入理解 CPU 性能问题,这对于优化 Rust 程序至关重要。

在 Instruments 的众多工具中,我们将聚焦于 Time Profiler------一个用于揭示程序中 CPU 时间分配的分析器。它唯一的任务是通过采样来揭示 CPU 如何在程序的不同部分之间分配时间,这对开发者意味着能够快速识别性能瓶颈并对症下药。

Time Profiler 基于采样动态记录程序在运行中的堆栈信息,而不是跟踪记录每次函数调用,这允许它在不显著影响程序性能的前提下,提供详尽的性能分析。通过设置适当的采样间隔,如每 1 毫秒采样一次,Instruments 能够搜集到精确的性能数据用于分析。

准备动作

为了充分了解 Instruments 的 Time Profiler 工具,我们准备一个运行时长足够以便于采样的示例程序。

下面的代码是一个基于莱布尼茨公式(Leibniz formula for π)的简版 π 值近似计算的 Rust 代码。莱布尼茨公式利用无穷级数来逼近 π 的值,其公式如下:

π = 4/1 - 4/3 + 4/5 - 4/7 + 4/9 - 4/11 + ...

rust 复制代码
#[inline(never)]
fn calculate_pi(iterations: u64) -> f64 {
    let mut pi: f64 = 0.0;
    let mut denominator: f64 = 1.0;
    for i in 0..iterations {
        if i % 2 == 0 {
            pi += 4.0 / denominator;
        } else {
            pi -= 4.0 / denominator;
        }
        denominator += 2.0;
    }
    pi
}

fn main() {
    let pi = calculate_pi(1_000_000_000);
    println!("Calculated Pi is: {}", pi);
}

我们在 calculate_pi 函数前使用 #[inline(never)] 属性来禁止内联优化,这样能确保在 Instruments 分析结果中清楚地看到这个函数。

紧接着,我们对 Cargo.toml 进行配置以生成足够的调试信息,确保在 Instruments 中能够获取人类可读的信息。以下配置可以理解为 Rust 版本的生成源码映射。

toml 复制代码
[profile.release]
debug = 1 # 启用调试信息
strip = false # 不要去除 symbols

最后,通过运行 cargo run --release 来构建你的 Rust 应用。这里添加 --release 标志以确保我们正在分析经过 Rust 优化的代码------也就是用户最终将使用的版本。

运行 Time Profiler 分析 Rust 程序

运行 Time Profiler 工具只需两个简单的步骤:

第一步,启动 Time Profiler: 打开你的终端,导航到你的 Rust 项目目录。使用以下命令,触发性能分析的记录:

bash 复制代码
xcrun xctrace record --template 'Time Profile' --output ./output.trace --launch -- /path/to/your/rust/project/target/release/your_binary

这条命令利用 xctrace 工具采用 Time Profiler 模板记录性能数据,最终生成一个 output.trace 文件。请确保替换 /path/to/your/rust/project/target/release/your_binary 为你的可执行文件的真实路径。

第二步,查看 .trace 文件 : 分析记录完毕后,用 open 命令来打开刚生成的 .trace 文件,这将自动触发 Instruments 并加载性能数据。

bash 复制代码
open ./output.trace

Instruments 启动后你就能直观地看到性能采样结果。例如,在我们的分析中,calculate_pi 函数单独耗费了大约 4.90 秒的 CPU 时间,几乎是程序运行时间的全部,占了 99.9%。这种一目了然的数据展示有助于我们迅速锁定可能的性能瓶颈。

解决 Rspack 性能劣化

回到我们的 Rspack 项目中,使用 Time Profiler 工具来对 main 分支和开发分支的性能进行对比分析。通过仔观察,我们发现性能下降主要出现在 process_assets_stage_dev_tooling 方法上。

具体数据如下:

  • main 分支耗时: 1.06 秒
  • 开发分支耗时: 2.22 秒

仔细检查调用栈显示,性能下降是由 filter_map 迭代中的 Source.map 函数调用引起的。该函数负责为代码生成 source map。

相关代码段如下所示:

rust 复制代码
let assets = compilation
    .assets()
    .iter()
    .filter_map(|(file, asset)| {
        let is_match = match &self.test {
            Some(test) => test(file.clone()),
            None => true,
        };

        if is_match {
            asset.get_source().map(|source| {
                let map_options = MapOptions::new(self.columns);
                let source_map = source.map(&map_options);
                (file, source, source_map)
            })
        } else {
            None
        }
    })
    .collect::<Vec<_>>();

依赖于代码 diff,问题的根源在于我之前将原本利用 Rust 的 rayon 库实现的并发迭代器 par_iter 更改为了单线程的 iter。顾名思义,par_iter 用于在多线程中并行处理迭代任务。在实际并未了解 rayon 的情况下,我错误地认为这一更改无关紧要。

实际上,在 main 分支的 Time Profiler 数据中,我们可以清楚地看到 source.map 方法确实在多个线程上并行执行。

我随后将代码改回使用 par_iter,并以此解决了性能问题。性能基准测试随之恢复正常。

最后

Rust性能很高么?不,它完全顶不住我的代码。一行代码查了三天,绷不住了。

哈哈,仅是开个玩笑。其实这次深入 Rust 的性能分析我更加理解了一点:要发挥 Rust 的最佳性能,还需要深刻理解它的特性。Rust 确实是一个功能强大的工具,但释放它的全部潜力,同样需要我们的持续学习和实践。

参考资料

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐4 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄5 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser7 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui7 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui