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 确实是一个功能强大的工具,但释放它的全部潜力,同样需要我们的持续学习和实践。

参考资料

相关推荐
L耀早睡15 分钟前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer28 分钟前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿34 分钟前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年1 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net
爱分享的程序员2 小时前
全栈项目搭建指南:Nuxt.js + Node.js + MongoDB
前端
隐含3 小时前
webpack打包,把png,jpg等文件按照在src目录结构下的存储方式打包出来。解决同一命名的图片资源在打包之后,重复命名的图片就剩下一个图片了。
前端·webpack·node.js
lightYouUp3 小时前
windows系统中下载好node无法使用npm
前端·npm·node.js
Dontla3 小时前
npm cross-env工具包介绍(跨平台环境变量设置工具)
前端·npm·node.js