1 个不稳定版本
0.1.0 | 2020年11月30日 |
---|
#302 在 性能分析
197 每月下载量
在 cbdr 中使用
4KB
56 行
持续基准测试,做得对
将基准测试作为持续集成的一部分运行 ⇒ 自动标记回滚性能退化的合并请求。
听起来不错,是吧?那么为什么在 CI 中看到基准测试这么不常见呢?问题在于很难将其简化为简单的“通过/失败”。CI 运行程序通常很嘈杂,因此如果您想得到可靠的结果,需要非常小心。
虽然困难,但如果您关心性能,那么这值得去做。如果没有在 CI 中进行基准测试,意外的回归 将会 发生。当它们刚开始出现时更容易修复。
以下是情况说明
- 有一些你关心的数值:CPU 秒数、最大内存使用量,等等;
- 但事实上它不是一个数值 - 它是一个分布 -而你实际上关心的数值是这个分布的均值;
- 而你真正关心的是,当你合并你的分支时,它会有多大的变化。
换句话说,有两个未知的分布(“合并前”和“合并后”),我们的挑战是通过从它们中反复采样来估计它们的均值差异。(如果你假设分布是高斯分布,这被称为Behrens-Fisher 问题。)
此页包含了一些关于不要做什么的建议。你可能对我实际使用的方法感兴趣,该方法在此处描述这里。 如果你在寻找 cbdr
,这是一个自动化此处所提建议的一些工具的工具,请查看这里。
❌ 分别对提交 A 和提交 B 进行基准测试
我看到的大多数基准测试软件都是这样工作的
- 它对第一件事进行了一系列的测量。
- 对第二件事情进行了一系列测量。
- 它比较了两个样本。
这有什么不好? 假设你在安静的系统上完成了基准测试提交A,然后在你开始基准测试提交B时,cron作业刚刚开始;这种惩罚完全由提交B承担!
那么我应该怎么做呢? 我建议你随机穿插运行你正在比较的两个事物。考虑上面的例子:如果你随机化测量,cron作业的惩罚将平均分配到两个提交上。这会损害你的精度,但不会影响准确性(这更重要)。
还有一个好处:你可以持续采样,直到达到你想要的精度。每次采样后,你看看均值差异的置信区间有多宽。如果太宽,你继续采样;如果足够窄,你就停止。
(另一方面,如果你像开头描述的那样分组你的测量。你正在基准测试A,想知道是否该转向B。你可以计算A的均值置信区间,但这并不能真正告诉你均值差异的置信区间有多紧。仅仅因为你有提交A的均值的精确估计,并不意味着你会有足够的数据来精确估计均值差异。)
❌ 保存旧基准测试结果以供以后使用
这实际上是“基准测试提交A,然后基准测试提交B”的更极端版本,但现在你的结果与日或月度变化的噪声源相关联。你的散热是否足够好,以确保夏天和冬天获取的结果可以比较?(我们的不够。)你在这段时间内升级了硬件吗?你在/etc中做了任何更改吗?
除了提高数据质量外,重新基准测试基本意味着你的CI运行现在是(1)无状态的和(2)与机器无关的。如果你想重用旧结果,你需要维护一个数据库和一个专门的基准测试机器。如果你重新基准测试基本,你可以使用任何可用的CI机器,而你需要的只是你的代码。
重新基准测试旧提交感觉像是在浪费CPU时间;但你的CI机器在基准测试上最多会花费2倍的时间。这真的是你需要收回的资源吗?
⚠️ 检查基准测试阈值
一些项目将基准测试阈值检查到仓库中,并在阈值超过时在CI中失败。如果减速是预期的,你将在同一个PR中更新阈值。这个想法是,它允许你检测缓慢的长期性能缓慢。这是一个好主意。
(旁注:我已经对存储旧基准测试结果表示过反对,但我们可以解决这个问题:而不是检查结果,我们可以检查一个旧版本的代码的引用来对其进行基准测试。)
这个方案确实允许你检测性能缓慢。问题是,当你确实检测到时,通常你几乎无法采取任何措施。失败的提交并不真的是罪魁祸首——它只是压垮骆驼的最后一根稻草。所以实际上你只是更新阈值,然后继续前进。一旦这成为一种习惯,你的基准测试就毫无用处了。
GH的人长期使用这样一个系统,但最近放弃了它,因为它被视为一种麻烦。
❌ 计算两个均值并比较它们
即使包含置信区间,这也不是一个好主意。引用生物统计学手册
有一种说法,即当两个平均值具有重叠的置信区间时,平均值之间没有显著差异(在P<0.05水平上)。这种说法的另一种版本是,如果每个平均值都在另一个平均值的置信区间之外,则平均值之间存在显著差异。这两种说法都不正确(Schenker和Gentleman 2001,Payton等人2003);两组数字的置信区间可能重叠,但它们在两样本t检验中仍然可能显著不同;相反,每个平均值可能都在另一个平均值的置信区间之外,但它们仍然可能不显著不同。不要尝试通过比较它们的置信区间来比较两个平均值,只需使用正确的统计检验。
⚠绘图结果和直观比较差异
这实际上是一种很好的基准测试方法!当然,这并不是非常严谨,但好处是几乎不太可能出错。然而,这个页面是关于将基准测试作为你的CI的一部分来运行的,因此任何需要人工干预的事情都会自动被排除。
尽管如此,仍然尝试绘制你的原始基准测试数据,只作为一个合理性检查(特别是在首次设置基准测试CI时)。你可以绘制类似页面顶部的直方图、小提琴图或热图。这些都可以让你一眼看出你的基准测试是如何表现的。(顺便说一下,cbdr具有帮助进行此操作的功能。)
⚠️小心“±”
仅仅因为基准测试库以“± x”的形式打印输出,并不意味着它在计算置信区间。“±”通常表示标准差或某些百分位数;但它可能表示任何东西。有标准差是好事,但它并不能确切地告诉你你的结果是否显著。
❌使用分析工具进行基准测试
考虑你的测试套件。它的作用是回答一个问题:“它坏了没有?”如果答案是“没有”,那么你就没问题。如果答案是“是”,那么我们现在需要找出测试为什么会失败——是时候打开调试器四处探索了。
同样,这也适用于基准测试。如果你想知道代码为什么会慢,那么你可能需要使用一些重型设备:基于样本的分析、堆分析、因果分析、帧分析、微基准测试、CPU仿真等等。
但是对于CI的目的,我们只需要回答一个问题:“它变慢了吗?”。对于这个,你只需要一个好的宏基准测试和一个秒表。
❌检查过多的变量
如果你的基准测试测量多个东西(例如,墙时和最大RSS),那么你可能想要检查所有这些,以确保没有事情已经退化。但是要注意:涉及的值的数量越多,假阳性的可能性就会增加。你可以通过将你的置信区间的宽度乘以相同的数字来对抗这一点,但这意味着它们会花费更长的时间来缩小。
在CI中运行所有的微基准测试并将它们分别比较听起来是个好主意(“我会知道哪个组件变慢了!”),但在实践中,你会得到如此多的假阳性,以至于你开始忽略CI失败。相反,CI应该只检查整体性能。如果出现了退化,你就可以使用微基准测试来找出原因。
❌连接来自不同基准测试的结果
上述方法涉及运行一个“良好的宏基准测试”。假设您没有这样的宏基准测试,但您确实拥有大量的良好微基准测试。我们为什么不分别为每个基准测试单独进行一组测量,然后将它们组合成一个单独的集合呢?(即连接各种csv文件的行。)
这里有一个问题:结果的分布可能不会非常正常;事实上,它很可能是高度多模态的(也称为“崎岖不平”)。您可以比较这些崎岖不平的分布,但不能用t检验。您必须仔细找到一个合适的测试(可能是一个非参数测试),而且它可能要弱得多。
相反,为什么不创建一个运行所有微基准测试的宏基准测试呢?这将考虑您所有的微基准测试,并给您一个远比高斯分布更符合实际的分布。
关于测量指令计数呢?
有些人使用CPU计数器来测量退休的指令、CPU周期等,作为对墙时长的代理,希望得到更可重复的结果。有两个问题要考虑:
- 您的代理与墙时长的相关性有多好?
- 与墙时长相比,方差好多少?
据我所知,简单地计数指令与墙时长的相关性不够好,而计数CPU周期则具有惊人的高方差。如果您选择这条路线,我建议您探索更复杂一些的模型,比如cachegrind所使用的模型。
如果您找到了一个方差更小的良好代理,那么就去做吧!您的置信区间会更快地收敛。
测量程序的小变化
尼古拉斯·内瑟科特这样说到指令计数
您绝对不应该用它来比较两个不同程序的速度,但它对于比较同一程序的两个略有不同的版本非常有用,这些版本可能具有相似的指令混合。
指令计数不是难以使其确定性的
用100%确定性的东西来替换墙时长的想法非常诱人,因为这意味着您可以摆脱所有这些统计上的胡说八道,只比较两个数字。听起来不错,对吧?
此文档的先前版本声称不存在任何确定性的测量,它仍然是墙时长的良好代理。然而,Rust项目最近让我食言。
Rustc团队的一些最近的工作表明,可以将指令计数方差降低到几乎为零。这是一件非常令人印象深刻的事情,关于这个项目的报告也非常出色——我推荐您阅读。
如果您想亲自尝试,要点是您需要计数在ring 3中退休的指令,然后减去基于计时器的硬件中断数。您需要
- 一个禁用了ASLR的Linux设置;
- 一个具有必要计数器的CPU;
- 一个具有确定性控制流的基准测试。
最后一个是一个陷阱。通常当我们说一个程序是“确定性的”时,我们指的是它的可观察输出;但现在指令计数是可观察输出的一部分!
这在实践中意味着,您的程序被允许查看时钟、/dev/urandom等,但不允许在那些东西上分支。(或者,如果它确实分支,它最好确保两个分支有相同数量的指令。)
这是一个非常难以下咽的药丸,比“简单地”产生确定性的输出要困难得多。例如,许多hashmap实现会将一些随机性混合到它们的哈希中。使用此类hashmap的程序可能在每次运行时都有完全相同的行为,但如果您测量它的指令计数,它将在每次运行时都不同。
rustc 团队已经做了大量工作来确保(单线程)rustc 具有这个特性。例如,在某些时候,rustc 会打印出自己的进程ID (PID),并且格式化代码根据 PID 的位数进行分支。这是一个可测量的变量来源,必须通过在格式化的 PID 中填充空格来修复。哎呀!
结论是:这是可以实现的;但如果您不愿意像 Rust 项目那样全力以赴,那么在我看来,您仍然应该估计一个置信区间。
lib.rs
:
测量一个进程运行所需时间
示例
use std::{process::Command, time::Duration};
use time_cmd::*;
match time_cmd(Command::new("ls")) {
Err(_) => panic!("IO error - failed to run ls"),
Ok((timings, code)) => {
if !code.success() {
panic!("ls ran but exited non-zero");
}
if timings.wall_time > Duration::from_secs(1) {
panic!("That's a slow ls!");
}
}
}
依赖项
~43KB