如何标准Rust代码与Criterion
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支持的。那么为什么要选择Criterion呢? Criterion是Rust社区的事实标准基准测试工具。 我推荐使用Criterion来测试你的代码的延迟。 也就是说,Criterion非常适合测量时钟时间。
重构 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
中增加下列代码:
- 导入
Criterion
基准测试运行器。 - 从我们的
game
包中导入play_game
函数。 - 创建一个名为
bench_play_game
的函数,它接受一个对Criterion
的可变引用。 - 使用
Criterion
实例(c
)来创建一个名为bench_play_game
的基准测试。 - 然后使用基准测试运行器(
b
)来多次运行我们的宏基准测试。 - 在一个”黑箱”中运行我们的宏基准测试,这样编译器就不会优化我们的代码。
- 从
1
到100
包括,进行迭代。 - 对于每一个数字,调用
play_game
,设置print
为false
。
现在我们需要配置game
包来运行我们的基准测试。
在你的Cargo.toml
文件的底部添加以下内容:
criterion
:将criterion
添加为开发依赖,因为我们只在性能测试中使用它。bench
:注册play_game
作为一个基准测试,并设置harness
为false
,因为我们将使用Criterion作为我们的基准测试工具。
现在我们已经准备好进行基准测试了,运行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的基准测试
现在我们可以重新运行我们的基准测试了:
哦哦!Criterion向我们显示了FizzBuzz和FizzBuzzFibonacci游戏之间性能差距为+568.69%
。
你的数字会比我的稍微有些不同。
然而,两者之间的差距可能在5x
的范围内。
这对我来说看起来是比较好的结果!特别是考虑到我们将像_Fibonacci_这样的花哨功能添加到我们的游戏中。
孩子们会喜欢的!
在 Rust 中展开 FizzBuzzFibonacci
我们的游戏很受欢迎!孩子们确实喜欢玩 FizzBuzzFibonacci。
为此,高层下达了他们想要续集的消息。
但这是现代世界,我们需要的是年度循环收入(ARR),而不是一次性购买!
我们游戏的新愿景是开放性的,不再固定在 1
和 100
之间(即使是包含在内的)。
不,我们正在开拓新的疆域!
Open World FizzBuzzFibonacci的规则如下:
编写一个程序,它接受_任何_正整数并打印:
- 对于三的倍数,打印
Fizz
- 对于五的倍数,打印
Buzz
- 对于同时是三和五的倍数的,则打印
FizzBuzz
- 对于是斐波那契数列的数字,只打印
Fibonacci
- 对于其他所有数字,打印该数字
为了让我们的游戏适应任何数字,我们需要接受一个命令行参数。
将 main
函数更新为如下形式:
- 收集所有从命令行传递给我们游戏的参数(
args
)。 - 获取传递给我们游戏的第一个参数,并将其解析为无符号整数
i
。 - 如果解析失败或没有传入参数,就默认以
15
作为输入运行我们的游戏。 - 最后,用新解析的无符号整数
i
来玩我们的游戏。
现在我们可以用任何数字来玩我们的游戏了!
使用 cargo run
后跟 --
将参数传递给我们的游戏:
如果我们省略或提供了无效的数字:
哇,这是一个仔细的测试过程!CI 通过了。我们的上司非常高兴。 让我们发布吧!🚀
结束
🐰 … 也许这是你的职业生涯的结束?
开玩笑的!其实一切都在燃烧!🔥
起初,一切看似进行得顺利。 但在周六早上02:07,我的寻呼机响了起来:
📟 你的游戏起火了!🔥
从床上匆忙爬起来后,我试图弄清楚发生了什么。 我试图搜索日志,但这非常困难,因为一切都在不停地崩溃。 最后,我发现了问题。孩子们!他们非常喜欢我们的游戏,以至于玩了高达一百万次! 在一股灵感的闪现中,我添加了两个新的基准测试:
- 一个用于玩游戏并输入数字一百(
100
)的微基准测试bench_play_game_100
。 - 一个用于玩游戏并输入数字一百万(
1_000_000
)的微基准测试bench_play_game_1_000_000
。
当我运行它时,我得到了这个:
等待一下… 等待一下…
什么!403.57 ns
x 1,000
应该是 403,570 ns
而不是 9,596,800 ns
(9.5968 ms
x 1_000_000 ns/1 ms
) 🤯
尽管我的斐波那契数列代码功能上是正确的,我必须在某个地方有一个性能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测试的附近位置。
我希望我能记住那个得分是多少。但是已经过了三个星期了。
我的终端历史记录没有回溯这么远。
而Criterion只会和最近的结果进行比较。
但我认为这是很接近的!
bench_play_game_100
基准测试的结果下降了近10倍,-93.950%
。
和bench_play_game_1_000_000
基准测试的结果下降了超过10,000倍!从9,596,800 ns
降到30.403 ns
!
我们甚至让Criterion的改变计数器达到了最大值,它只会达到-100.000%
!
🐰 嘿,至少我们在性能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。