如何使用Iai对Rust代码进行基准测试
Everett Pompeii
什么是基准测试?
基准测试是测试代码性能的实践, 以查看它的速度(延迟)或工作量(吞吐量)。 在软件开发中,这一步通常被忽视, 但对于创建和维护快速且高性能的代码至关重要。 基准测试为开发人员提供了必要的指标, 以了解他们的代码在各种工作负载和条件下的表现。 出于与编写单元测试和集成测试以防止功能回归相同的原因, 你应该编写基准测试以防止性能回归。 性能错误也是错误!
用 Rust 编写 FizzBuzz
为了编写基准测试,我们需要一些源代码来进行基准测试。 首先,我们将编写一个非常简单的程序, FizzBuzz。
FizzBuzz的规则如下:
写一个程序,打印从
1
到100
的整数(包含):
- 对于三的倍数,打印
Fizz
- 对于五的倍数,打印
Buzz
- 对于既是三的倍数又是五的倍数的数,打印
FizzBuzz
- 对于所有其他数,打印这个数字
有许多种编写FizzBuzz的方法。 所以我们会选择我最喜欢的一种:
- 创建一个
main
函数 - 从
1
迭代到100
(含)。 - 对于每个数字,分别计算
3
和5
的取余(除后余数)。 - 对两个余数进行模式匹配。
如果余数为
0
,那么这个数就是给定因素的倍数。 - 如果
3
和5
的余数都为0
,则打印FizzBuzz
。 - 如果只有
3
的余数为0
,则打印Fizz
。 - 如果只有
5
的余数为0
,则打印Buzz
。 - 否则,就打印这个数字。
按步骤操作
为了与本教程进行同步学习,您需要 安装 Rust。
🐰 这篇文章的源代码在 GitHub 上可以找到
安装好 Rust 后,您可以打开一个终端窗口,然后输入:cargo init game
然后导航至新创建的 game
目录。
你应该能看到一个名为 src
的目录,其中有一个名为 main.rs
的文件:
将其内容替换为上述的 FizzBuzz 实现。然后运行 cargo run
。
输出结果应该像这样:
🐰 砰!你正在破解编程面试!
应该生成了一个新的 Cargo.lock
文件:
在进一步探讨之前,有必要讨论微基准测试和宏基准测试之间的区别。
微基准测试 vs 宏基准测试
有两大类软件基准测试:微基准测试和宏基准测试。
微基准测试的操作层次和单元测试类似。
例如,为一个确定单个数字是 Fizz
、 Buzz
,还是 FizzBuzz
的函数设立的基准测试,就是一个微基准测试。
宏基准测试的操作层次和集成测试类似。
例如,为一函数设立的基准测试,该函数可以玩完整个 FizzBuzz 游戏,从 1
到 100
,这就是一个宏基准测试。
通常,尽可能在最低的抽象级别进行测试是最好的。 在基准测试的情况下,这使得它们更易于维护, 并有助于减少测量中的噪声。 然而,就像有一些端到端测试对于健全性检查整个系统根据预期组合在一起非常有用一样, 拥有宏基准测试对于确保您的软件中的关键路径保持高性能也非常有用。
在 Rust 中进行基准测试
在 Rust 中进行基准测试的三个热门选项是: libtest bench, Criterion, 和 Iai。
libtest 是 Rust 内置的单元测试和基准测试框架。虽然它是 Rust 标准库的一部分,但 libtest bench 仍被认为是不稳定的,因此仅在 nightly
编译器版本中可用。要在稳定版 Rust 编译器上工作,则需要使用一个单独的基准测试工具 。然而,这两者都没有得到积极的开发。
Rust 生态系统中最流行的基准测试工具是 Criterion。它可以在稳定版和 nightly
Rust 编译器版本上工作,并且已经成为 Rust 社区内事实上的标准。相比 libtest bench,Criterion 还具有更多的功能。
Criterion 的一个实验性替代品是 Iai,同样出自 Criterion 的创建者。然而,它使用指令计数而不是墙钟时间:CPU 指令、L1 访问、L2 访问和 RAM 访问。这允许一次性基准测试,因为这些指标在不同运行之间应该几乎保持相同。
三者都被 Bencher 支持。那么为什么选择Iai呢? Iai使用指令计数而非墙钟时间。 这使其理想的适合持续性基准测试,也就是在CI中进行基准测试。 我建议对于持续化基准测试使用Iai,特别是如果你正在使用共享的运行器。 了解一件重要的事情,Iai仅测量你真正关心之事的代理。 你的应用程序的延迟是否会因从1000个指令增加到2000个指令而加倍? 可能会也可能不会。 因此,与基于指令计数的基准测试并行运行基于墙钟时间的基准测试可能会比较有用。
🐰 Iai 已经超过3年没有更新了(查看 这里)。所以你可能需要考虑使用 Iai-Callgrind 替代。
安装 Valgrind
Iai使用一个叫做 Valgrind 的工具收集指令计数。 Valgrind支持Linux,Solaris,FreeBSD和MacOS。 然而,MacOS 是限售x86_64处理器,arm64 (M1, M2, etc) 处理器目前不支持。
在 Debian 上运行:sudo apt-get install valgrind
在 MacOS (仅限x86_64/Intel芯片)上:brew install valgrind
重构 FizzBuzz
为了测试我们的 FizzBuzz 应用,我们需要将逻辑从程序的 main
函数中解耦出来。
基准测试工具无法对 main
函数进行基准测试。为了做到这一点,我们需要做一些改动。
在 src
下,创建一个新的名为 lib.rs
的文件:
将以下代码添加到 lib.rs
:
play_game
:接受一个无符号整数n
,用该数字调用fizz_buzz
,如果print
为true
,则打印结果。fizz_buzz
:接受一个无符号整数n
,执行实际的Fizz
、Buzz
、FizzBuzz
或数字逻辑,然后将结果作为字符串返回。
然后更新 main.rs
,使其看起来像这样:
game::play_game
:从我们刚刚用lib.rs
创建的game
包中导入play_game
。main
:我们程序的主入口点,遍历从1
到100
的数字,对每个数字调用play_game
,并将print
设为true
。
对FizzBuzz进行基准测试
为了测试我们的代码,我们需要建立一个 benches
文件夹并添加文件来记录我们的基准测试, play_game.rs
:
在 play_game.rs
文件里添加下列代码:
- 导入我们
game
包中的play_game
函数。 - 创建名为
bench_play_game
的函数。 - 在”black box”里运行我们的宏基准,这样编译器就不会优化我们的代码。
- 从
1
到100
进行迭代。 - 对每个数字,调用
play_game
,将打印设置为false
。
现在我们需要配置 game
包以运行我们的基准测试。
在你的 Cargo.toml
文件底部添加以下内容:
iai
:将iai
添加为开发依赖项,因为我们仅在性能测试中使用它。bench
:注册play_game
作为基准测试并将harness
设置为false
,因为我们将使用 Iai 作为我们的基准测试框架。
现在我们已准备好对代码进行基准测试,运行 cargo bench
:
🐰 开始升温吧!我们得到了我们的第一次基准测试数据!
终于,我们可以休息下我们疲惫的开发者头脑了… 开个玩笑,我们的用户需要一个新的特性!
用 Rust 编写 FizzBuzzFibonacci
我们的关键绩效指标(KPI)下降了,所以我们的产品经理(PM)希望我们添加新功能。 经过多次头脑风暴和许多用户采访后,我们决定光有 FizzBuzz 已经不够了。 现在的孩子们希望有一个新的游戏,FizzBuzzFibonacci。
FizzBuzzFibonacci的规则如下:
编写一个程序,打印从
1
到100
的整数(包括):
- 对于三的倍数,打印
Fizz
- 对于五的倍数,打印
Buzz
- 对于既是三的倍数又是五的倍数的,打印
FizzBuzz
- 对于是斐波那契数列的数字,只打印
Fibonacci
- 对于所有其他的,打印该数
斐波那契数列是一个每个数字是前两个数字之和的序列。
例如,从 0
和 1
开始,斐波那契数列的下一个数字将是 1
。
后面是:2
, 3
, 5
, 8
等等。
斐波那契数列的数字被称为斐波那契数。所以我们将不得不编写一个检测斐波那契数的函数。
有许多方法可以编写斐波那契数列,同样也有许多方法可以检测一个斐波那契数。 所以我们将采用我的最爱:
- 创建一个名为
is_fibonacci_number
的函数,该函数接收一个无符号整数,并返回一个布尔值。 - 遍历从
0
到我们给定的数n
(包含n
)的所有数字。 - 用
0
和1
分别作为前一个
和当前
数字来初始化我们的斐波那契序列。 - 当
当前
数字小于当前迭代i
时持续迭代。 - 添加
前一个
和当前
数字来获得下一个
数字。 - 将
前一个
数字更新为当前
数字。 - 将
当前
数字更新为下一个
数字。 - 一旦
当前
大于或等于给定数字n
,我们将退出循环。 - 检查
当前
数字是否等于给定数字n
,如果是,则返回true
。 - 否则,返回
false
。
现在我们需要更新我们的 fizz_buzz
功能:
- 将
fizz_buzz
功能重命名为fizz_buzz_fibonacci
以使其更具描述性。 - 调用我们的
is_fibonacci_number
辅助函数。 - 如果
is_fibonacci_number
的结果为true
,则返回Fibonacci
。 - 如果
is_fibonacci_number
的结果为false
,则执行相同的Fizz
、Buzz
、FizzBuzz
或数字逻辑,并返回结果。
因为我们将 fizz_buzz
重命名为 fizz_buzz_fibonacci
,我们也需要更新我们的 play_game
功能:
我们的 main
和 bench_play_game
功能可以保持完全相同。
对FizzBuzzFibonacci进行基准测试
现在我们可以重新运行我们的基准测试:
哦,太棒了!Iai 告诉我们,FizzBuzz 和 FizzBuzzFibonacci 游戏之间预计周期的区别是 +522.6091%
。
你的结果会稍微有些不同于我的。
然而,两款游戏之间的区别可能在 5x
左右。
我觉得这已经很好了!尤其是对于增加一个听起来很高级的 斐波那契数列 功能到我们的游戏中。
孩子们一定会喜欢它的!
在 Rust 中展开 FizzBuzzFibonacci
我们的游戏很受欢迎!孩子们确实喜欢玩 FizzBuzzFibonacci。
为此,高层下达了他们想要续集的消息。
但这是现代世界,我们需要的是年度循环收入(ARR),而不是一次性购买!
我们游戏的新愿景是开放性的,不再固定在 1
和 100
之间(即使是包含在内的)。
不,我们正在开拓新的疆域!
Open World FizzBuzzFibonacci的规则如下:
编写一个程序,它接受_任何_正整数并打印:
- 对于三的倍数,打印
Fizz
- 对于五的倍数,打印
Buzz
- 对于同时是三和五的倍数的,则打印
FizzBuzz
- 对于是斐波那契数列的数字,只打印
Fibonacci
- 对于其他所有数字,打印该数字
为了让我们的游戏适应任何数字,我们需要接受一个命令行参数。
将 main
函数更新为如下形式:
- 收集所有从命令行传递给我们游戏的参数(
args
)。 - 获取传递给我们游戏的第一个参数,并将其解析为无符号整数
i
。 - 如果解析失败或没有传入参数,就默认以
15
作为输入运行我们的游戏。 - 最后,用新解析的无符号整数
i
来玩我们的游戏。
现在我们可以用任何数字来玩我们的游戏了!
使用 cargo run
后跟 --
将参数传递给我们的游戏:
如果我们省略或提供了无效的数字:
哇,这是一个仔细的测试过程!CI 通过了。我们的上司非常高兴。 让我们发布吧!🚀
结束
🐰 … 也许这是你的职业生涯的结束?
开玩笑的!其实一切都在燃烧!🔥
起初,一切看似进行得顺利。 但在周六早上02:07,我的寻呼机响了起来:
📟 你的游戏起火了!🔥
从床上匆忙爬起来后,我试图弄清楚发生了什么。 我试图搜索日志,但这非常困难,因为一切都在不停地崩溃。 最后,我发现了问题。孩子们!他们非常喜欢我们的游戏,以至于玩了高达一百万次! 在一股灵感的闪现中,我添加了两个新的基准测试:
bench_play_game_100
微基准测试用于播放游戏数字一百 (100
)bench_play_game_1_000_000
微基准测试用于播放游戏数字一百万 (1_000_000
)
当我运行它,我得到这个:
等待一下… 等待一下…
等等! 6,685 预计周期
x 1,000
应该是 6,685,000 预计周期
不是 155,109,206 预计周期
🤯
尽管我得到了我的斐波那契数列代码功能正确,我一定在某处有性能的bug。
修复 Rust 中的 FizzBuzzFibonacci
让我们再次看一下 is_fibonacci_number
函数:
现在我在考虑性能,我意识到我有一个不必要的,额外的循环。
我们可以完全摆脱 for i in 0..=n {}
循环,
只需直接比较 current
值和给定的数字 (n
) 🤦
- 更新您的
is_fibonacci_number
函数。 - 用
0
和1
初始化我们的斐波那契序列作为previous
和current
数字。 - 当
current
数字小于 给定数字n
时迭代。 - 将
previous
和current
数字相加以获得next
数字。 - 把
previous
数字更新为current
数字。 - 把
current
数字更新为next
数字。 - 一旦
current
大于或等于给定的数字n
,我们将退出循环。 - 检查
current
数字是否等于给定的数字n
并返回该结果。
现在让我们重新运行那些基准测试,看看我们做的如何:
哦,哇!我们的 bench_play_game
基准测试回归到了与最初的 FizzBuzz 相关的水平附近。
我真希望我能记住那个分数是多少。但已经过去三个星期了。
我的终端历史已经找不到了。
而且 Iai 只是与最近的结果进行比较。
但是我想应该差不多!
bench_play_game_100
基准测试几乎下降了10倍, -87.22513%
。
而 bench_play_game_1_000_000
基准测试下降了超过10,000倍!从 155,109,206 预计周期
到 950 预计周期
!
那是 -99.99939%
!
🐰 嗯,至少我们在这个性能bug进入生产之前发现了它… 噢,对了,已经忘了…
在 CI 中捕获性能回归
由于我那个小小的性能错误,我们的游戏收到了大量的负面评论,这让高管们非常不满。 他们告诉我不要让这种情况再次发生,而当我询问如何做到时,他们只是告诉我不要再犯。 我该如何管理这个问题呢‽
幸运的是,我找到了这款叫做 Bencher 的超棒开源工具。 它有一个非常慷慨的免费层,因此我可以在我的个人项目中使用 Bencher Cloud。 而在工作中需要在我们的私有云内,我已经开始使用 Bencher Self-Hosted。
Bencher有一个内建的适配器, 所以很容易集成到 CI 中。在遵循快速开始指南后, 我能够运行我的基准测试并用 Bencher 追踪它们。
使用这个由一个友善的兔子给我的巧妙的时间旅行设备, 我能够回到过去,重演如果我们一直都在使用Bencher的情况下会发生什么。 你可以看到我们首次推出存在问题的FizzBuzzFibonacci实现的位置。 我马上在我的拉取请求评论中得到了CI的失败信息。 就在那天,我修复了性能问题,摆脱了那不必要的额外循环。 没有火灾。顾客都非常开心。
Bencher: 持续性能基准测试
Bencher是一套持续型的性能基准测试工具。 你是否曾经因为性能回归影响到了你的用户? Bencher可以防止这种情况的发生。 Bencher让你有能力在性能回归进入生产环境 之前 就进行检测和预防。
- 运行: 使用你喜爱的基准测试工具在本地或CI中执行你的基准测试。
bencher
CLI简单地包装了你现有的基准测验设备并存储其结果。 - 追踪: 追踪你的基准测试结果的趋势。根据源分支、测试床和度量,使用Bencher web控制台来监视、查询和绘制结果图表。
- 捕获: 在CI中捕获性能回归。Bencher使用最先进的、可定制的分析技术在它们进入生产环境之前就检测到性能回归。
基于防止功能回归的原因,在CI中运行单元测试,我们也应该使用Bencher在CI中运行基准测试以防止性能回归。性能问题就是错误!
开始在CI中捕捉性能回归 — 免费试用Bencher Cloud。