为基准测试优化服务器
摘要
本文提供了逐步指南,介绍如何优化服务器以进行基准测试,旨在减少运行间噪音并确保测量可重复性,涉及硬件检查和核心固定等技术。
<p><a href="https://lobste.rs/s/fejuat/tuning_server_for_benchmarking">评论</a></p>
查看缓存全文
缓存时间: 2026/09/26 13:25
# 服务器调优以进行基准测试
来源:https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/
代码优化始于测量,而测量只有在可重复时才有用:在5%的噪声下,2%的改进是难以察觉的。然而在一台未调优的机器上,同一个二进制文件在不同运行中很容易出现几个百分点的速度差异。本文以一个小型基准测试为例,逐步调优机器,并在每次更改后重新测量,直至运行变得稳定。请注意,为*基准测试*调优与为*性能*调优不同:基准测试希望机器具有可重复性,甚至以牺牲一些峰值速度为代价。而生产环境则需要尽可能榨取每一分性能。
## 嘈杂的基线§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#a-noisy-baseline)
我们的示例是在短时间突发中对一个双精度数组求和。真实的服务很少会持续冲击CPU:它们通常处理一个请求,然后空闲,再被下一个请求唤醒。这里每个计时迭代在2毫秒的空闲间隔后执行256次求和的突发,空闲间隔不计入测量时间。`PauseTiming`/`ResumeTiming`使休眠时间不计入测量时间,而`DoNotOptimize`则确保结果在优化器优化后仍然有效;否则,编译器会删除整个循环。
```cpp
static void BM_Sum(benchmark::State& state) {
alignas(64) static std::array<double, 4096> data;
std::iota(data.begin(), data.end(), 0.0);
for (auto _ : state) {
state.PauseTiming();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
state.ResumeTiming();
for (size_t i = 0; i < 256; ++i) {
auto sum = std::accumulate(data.cbegin(), data.cend(), 0.0);
benchmark::DoNotOptimize(sum);
}
}
}
BENCHMARK(BM_Sum);
```
在启用所有优化选项(`-O3`,以及`-march=native -mtune=native -flto -ffast-math`)的 release 模式下编译,然后运行十次重复测试并汇总结果:
```
$ ./benchmark --benchmark_repetitions=10 --benchmark_min_time=100x
BM_Sum_mean 99575 ns
BM_Sum_stddev 2704 ns
BM_Sum_cv 2.72 %
```
值得关注的是`cv`(变异系数):标准差除以均值。接近**3%**的运行间噪声——任何小于此值的优化都将难以察觉。我们需要降低它。
## 了解你的硬件§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#know-your-hardware)
在调整任何旋钮之前,先看看你要调优的对象。`lstopo`能在一张图中展示整台机器的结构:缓存、核心、同步多线程(SMT)对以及连接的PCIe设备。先看我的笔记本电脑。
**图1:我的笔记本电脑(Intel Core Ultra 5 135U)。** 三种类型的核心:两个P核,每个有两个硬件线程(虚线表示);八个E核以四个为一组共享一个L2缓存;还有两个低功耗E核(左下角)完全位于L3缓存之外。
[笔记本电脑的lstopo图](https://david.alvarezrosa.com/images/lstopo-laptop.png)
在这里,选择不同的核心会改变你测量的内容:落在CPU 4上,你会得到一个时钟频率较低的E核;落在CPU 12上,则会失去L3缓存。
现在,对比我的家庭实验室服务器。
**图2:我的家庭实验室服务器(AMD Ryzen 7 PRO 8700GE)。** 八个相同的核心,具有相同的缓存;NVMe驱动器和网卡连接在右侧的PCIe总线上。
[服务器的lstopo图](https://david.alvarezrosa.com/images/lstopo-homelab.png)
在服务器上,每个核心都同样出色:同质机器更适合做基准测试。一旦基准测试涉及I/O,PCIe一侧就很重要:它显示了你正在使用哪个NVMe或网卡,以及在多路服务器上,它挂载在哪个NUMA节点下。
## 绑定到一个核心§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#pin-to-a-core)
调度器可以自由地在核心之间迁移基准测试进程,而每次迁移都会丢弃温暖的缓存。在混合CPU上情况更糟:性能核和效率核运行相同代码的速度差异巨大,因此结果会因进程落点不同而呈现双峰分布。将基准测试绑定到单个核心(在混合架构上,选择P核)。
```
$ taskset -c 2 ./benchmark ...
```
平均时间降至**55.3 μs**,变异系数(CV)减半多,达到**1.06%**。收益比单纯的迁移成本所暗示的要大:现在每个突发都唤醒同一个核心,因此该核心的时钟频率在突发之间没有时间下降。请注意,绑定只是将基准测试*放置*在核心上,并没有阻止*其他*任务运行在上面。在繁忙的机器上,需要更进一步,将核心专用于基准测试,可以通过内核命令行参数(`isolcpus=2 nohz_full=2 rcu_nocbs=2`)或在运行时使用`cpuset` cgroup来实现。
## 锁定CPU频率§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#lock-the-cpu-frequency)
默认情况下,Linux会根据负载调整CPU频率,因此基准测试可能从冷态、低频开始,到热态、高频结束。将频率调节器切换为`performance`模式以保持高频锁定。
```
$ sudo cpupower frequency-set --governor performance
```
并验证其是否生效:
```
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
performance
```
重新测量得到均值为**54.9 μs**,变异系数为**0.79%**。这个增量看起来不大,仅仅因为绑定已经让核心频率保持在高位:单独来看,性能调节器就能将未绑定的基线从99.6 μs直接降到54.5 μs。无论如何,再也不会有突发在冷频率下启动了。
## 禁用超线程§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#disable-hyperthreading)
CPU仍然与它的SMT兄弟核心共享执行单元和L1/L2缓存:调度器放置在那里的任何任务都会干扰我们的测量。完全禁用SMT。
```
$ echo off | sudo tee /sys/devices/system/cpu/smt/control
```
变异系数降至**0.26%**,提升了三倍:现在这个核心独享其执行单元和缓存。
## 禁用Turbo Boost§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#disable-turbo-boost)
即使使用性能调节器,睿频频率也会随温度和功耗预算而变化:在温暖的机器上运行与在凉爽的机器上运行,时钟频率会更低。禁用睿频以获得稳定的时钟。
```
$ echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost
```
在这台机器上没有任何变化,因为我们的短突发根本没给硅片留出睿频的时间。在确实会触发睿频的机器上,预计平均值会上升:你放弃了峰值性能。这个权衡是值得的,因为当我们优化时,我们关心的是*相对*数值,而这些现在在不同运行中是可比较的。低延迟生产环境调优则会做出*相反*的选择,并保持睿频开启:在那里,每一纳秒都很重要。最注重延迟的交易公司更进一步,运行超频服务器,将所有核心锁定在高于出厂设置的固定频率上——通过更好的散热来换取速度*和*稳定的时钟。
## 总结§
(https://david.alvarezrosa.com/posts/tuning-a-server-for-benchmarking/#summary)
以下是整个过程的总结表,每一行都在前一行的基础上增加一项更改。我们将噪声从接近**3%**降低到了**0.26%**,同时速度还提升了1.8倍;现在,半个百分点的差异已成为真实、可测量的信号。你可以使用我[CppPlayground](https://github.com/david-alvarez-rosa/CppPlayground)仓库中的[benchmark](https://github.com/david-alvarez-rosa/CppPlayground/blob/main/scratch/benchmark.cpp)在你的机器上复现。
| 步骤 | 平均时间 | 标准差 | 变异系数 |
| :--- | :--- | :--- | :--- |
| 未调优 | 99.6 μs | 2.70 μs | 2.72% |
| + 绑定到单核 | 55.3 μs | 0.59 μs | 1.06% |
| + 性能调节器 | **54.9 μs** | 0.43 μs | 0.79% |
| + 禁用超线程 | 55.3 μs | 0.15 μs | **0.26%** |
| + 禁用睿频 | 55.5 μs | 0.14 μs | **0.26%** |
在更繁忙的机器上,还有更多值得尝试的选项:禁用地址空间布局随机化(ASLR)、NMI看门狗或透明大页等。[bench-remote.sh](https://github.com/david-alvarez-rosa/CppPlayground/blob/main/scripts/bench-remote.sh)脚本会应用所有这些选项。没有任何设置能在重启后保留,这正是你想要的:调优、测量,然后重启回正常机器。可复现的基准测试万岁!
相似文章
AutoTuneBench: 用于LLM服务引擎代理自动调优的可信度量
AutoTuneBench 提出了一个基准和测量协议,用于LLM服务引擎的可信代理自动调优,解决了诸如稻草人基线和基础设施缺陷等故障模式,以确保可靠的结果。
以毫秒为单位进行基准测试
一篇风格类似 Matthias Endler(matklad)的博客文章,主张微基准测试应调整到运行约 300 毫秒——足够快以便快速迭代,又足够慢以避免启动开销并让人对速度有直观感受——其目标是让基准测试提供决策直觉,而非精确测量。
为本地LLM提供大量答案的新旧基准测试
本文介绍了一个用于评估本地LLM配置的基准测试工具,重点关注显存使用情况、性能指标和硬件优化,以协助开发者优化配置。
基准测试危机
一篇博客文章认为,AI 生成的查询优化通过利用 ClickBench 规则定义不严的漏洞来刷分,使古德哈特定律在数据库基准测试中卷土重来,并提出了临时基准、更新流以及在被测系统之后才创建的基准等修复方案。
性能对比:Wild 与 Mold 链接器
本文探讨了 Wild 与 Mold 链接器在基准测试结果中的差异,分析了文件系统、fork 设置等配置对性能的影响。