本章导读

调优的第一步不是改代码而是会测量——本章是性能工作的方法论文(第 137 章是实战)。XRT 的度量基础设施给答案提供了样板:档案化基准(config/performance_profiles.json:每档基准源+参数+采样策略——"跑什么"是声明不是即兴);环境基线(十四要素:平台/CPU/亲和性/电源策略/编译器/优化级/调度策略——数字只在同基线内可比);统计纪律(repeats 采样+MAD(中位绝对偏差)与中央范围双离差门限——噪声测量被拒绝而非平均掉);回归阈值(higher 10%/lower 20% 的方向性阈值——慢了要严、快了也警惕);扫描实验(sweep——参数空间的系统遍历而非单点抽查)。加提问纪律:基准回答的是你问的问题——问题错了数字再准也没用。

136.1引入

性能工作的三种失败开局:无基线测量("这次感觉慢了"——多少?和什么比?在哪个机器上?);跨环境比较(开发机与 CI 机的数字混着看——CPU 代际/电源策略/后台负载全不同);噪声当信号(单次采样 3% 波动当回归处理——扑空)。三者共同根源:把测量当"跑一下看数字"而不是实验设计。XRT 的度量工具链(measure_performance/measure_size + 档案 + 报告 schema)把实验设计固化:档案声明跑什么怎么跑、基线记录在哪跑、门限判定算不算数——人只负责问对问题。

提问纪律展开:基准前先写一句话——"我怀疑 X 在 Y 场景下比 Z 慢/快"?X 是操作(如 SPSC 单条入队)、Y 是场景(容量 4096 双线程)、Z 是对照(上一版/另一数据结构)。写不出这句话就是还没到测量的时候。第 62 章的"先用默认值跑、接观测、按数据调"在这里的形态就是:默认档跑基线→看离差(噪声水平)→设计实验→数字回答问题。

136.2概念

档案化基准:声明式实验

1档案performance_profiles.json 每档
2默认策略repeats=5 / warmups=1 / policy=baseline-unpinned / -O2
3度量measure_performance.py --profiles 选档 → 编译 → 运行采样 → 报告

smoke 与全量的两速:smoke_args 是 CI 的快速形态(秒级验证"基准还能跑");全量 args 是正式采样(分钟级出统计)。基准源在 dev/bench/(挂模块清单的 benchmarks 字段——第 130 章的清单又一消费)——基准也是被清单管理的资产。

环境基线:十四要素

组要素
平台platform / machine / platform_release
处理器cpu / cpu_count / affinity(亲和性)/power_policy(电源策略)
构建compiler_family / compiler_version / arch / optimization / cflags / ldflags
策略policy(如 baseline-unpinned)

两个最常被忽略的要素:power_policy(笔记本省电模式与插电的性能差可达 2-3 倍——报告必须记录);affinity(绑核与否改变缓存与调度行为——queue 延迟基准专门有 pinned 变体对照)。对比规则:趋势结论只在同基线内下;跨基线差异先怀疑环境再怀疑代码(第 131 章尺寸七要素的性能版)。

统计纪律:离差门限

max_relative_mad: 0.2(中位绝对偏差相对中位数 ≤20%)与 max_relative_central_range: 0.3(中央范围 ≤30%)双门限:任一超限=这轮测量太噪——结果是"拒收"而不是"平均一下继续"。这是与业余测量的分水岭:业余把噪声平均掉(平均值很稳但毫无意义——每次都在不同位置);纪律把噪声暴露(拒收→查原因→干扰源/加 repeats/换策略)。median 优先:报告取中位数(对离群鲁棒);样本序列全记录(透明可复查)。

回归阈值:方向性双阈值

regression: {higher: 0.1, lower: 0.2}——变慢 10% 报警、变快 20% 也报警。变快警惕什么:测量环境变了(假提升)、或以牺牲别处为代价(此档快了别档慢了——需全档跑)。方向不对称是务实的:慢是直接伤害(严门槛)、快是间接信号(宽门槛)。

扫描实验:sweep

单点基准回答"这个配置多快";sweep 回答"配置空间长什么样"——queue 的容量/批量两维扫描(run_queue_bench_sweep):容量 64..65536 × 批量 1..128 的矩阵——拐点(批量 32 后收益饱和)与平台(小容量下 SPSC 退化)一眼可见。扫描是选型依据的产地:第 21 章"容量怎么选"、第 68 章"批量多大"的答案都来自 sweep 而非拍脑袋。报告(QUEUE_BATCH_SWEEP 等)归档在 dev/bench——实验结果是一等资产。

136.3示例

第一个完整程序:内存视角的微基准

下面的程序来自 examples/memory/stats——stats 读数作为最轻量的性能观测:

examples/memory/stats/main.c源码 ↗
























 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	xmemstats tStats;
	ptr pSmall;
	ptr pLarge;

	/* 开关可查询:先确认当前状态,再打开并清零。 */
	printf("before=%d\n", xrtMemStatsEnabled() ? 1 : 0);
	xrtMemStatsEnable(true);
	printf("after=%d\n", xrtMemStatsEnabled() ? 1 : 0);
	xrtMemStatsReset();

	/* 一次小块(走池)+ 一次大块(直通)。 */
	pSmall = xrtMalloc(48);
	pLarge = xrtMalloc(4096);
	if ( (pSmall == NULL) || (pLarge == NULL) ) {
		xrtFree(pSmall);
		xrtFree(pLarge);
		return 1;
	}
	xrtFree(pSmall);
	xrtFree(pLarge);

	/* 读取窗口内计数(字段语义见文件头读数解释)。 */
	xrtMemStatsGet(&tStats);
	printf("malloc_calls=%llu pooled=%llu direct=%llu backing=%llu\n",
		(unsigned long long)tStats.MallocCalls,
		(unsigned long long)tStats.PooledAllocCalls,
		(unsigned long long)tStats.DirectAllocCalls,
		(unsigned long long)tStats.BackingAllocCalls);

	/* 用完关闭,回到零开销模式。 */
	xrtMemStatsEnable(false);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/memory/stats/main.c -lws2_32 -liphlpapibefore=0 after=1malloc_calls=2 pooled=1 direct=1 backing=3

刚才发生了什么。 ① 性能分析的第一问常是"分配了多少"——stats 四读数回答它:本例 48B 命中池(零系统调用)而 4096B 直通——池边界的实测(比读文档快且永远与当前构建一致)。② 这是微观测(计数级)——时间维度的测量走 bench 工具链;但很多"性能问题"在计数层就有答案(backing 异常高=池配置不匹配业务尺寸)。③ 方法论位置:stats 是零成本常开的观测(第 135 章健康面板),也是性能调查的第一站——先看计数再测时间。

第二个完整程序:确定性输出的自检基准

第二个程序来自 examples/core/version_limits——基准的可信形态:

examples/core/version_limits/main.c源码 ↗
















 */

#include <stdio.h>
#include <xrt.h>

int main(void)
{
	xrtresourcelimits Limits;

	printf("version=%s\n", xrtVersion());
	xrtResourceLimitsInit(&Limits);
	printf("limits: depth=%u entries=%llu input=%llu\n",
		(unsigned)Limits.iMaxDepth,
		(unsigned long long)Limits.iMaxEntries,
		(unsigned long long)Limits.iMaxInputBytes);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/core/version_limits/main.c -lws2_32 -liphlpapiversion=2.0.0-devlimits: depth=128 entries=100000 input=268435456

刚才发生了什么。 ① 这个示例演示确定性输出——与时间基准的随机性对照:基准工程里"可复现"分两种(确定性输出=每次同值;统计性输出=分布稳定)。② 资源边界默认值(128/100000/256MiB)是性能档案的隐形参数——解析基准的输入规模受这些闸约束(超大输入测试要显式放宽——档案的 cflags 可注入)。③ dev/bench 的真基准(queue/coroutine/task 族)形态更大(多线程采样+统计输出),但方法论的原子在本示例里齐全:固定输入、明确输出、可对照。

一份基准报告的解剖(以 QUEUE_BENCH 为范本)

dev/bench/QUEUE_BENCH_20260728_XRT2.md 的结构就是模板:Sources(基准源+脚本+构建形态——可复现的入口);Environment(OS/CPU/编译器/构建标志/调度策略——基线自报);Matrix(矩阵参数:每生产者条目数/容量/生产者消费者数/批量/采样数——实验设计可见);Results(每档样本序列+median——透明原始数据);Interpretation(解读——结论与数字的连接:单条路径在预期区间、批量预留有效摊销、批量是高吞吐首选路径)。五节各司其职:缺 Sources 不可复现、缺 Environment 不可比、缺 Matrix 不可评、缺 Results 不可信、缺 Interpretation 不可用。写报告时对着这五节自查——这也是第 136 章练习与第 137 章挑战的验收格式。

136.4契约

  • 档案声明:基准源+args+smoke 两速+策略——跑什么不即兴。
  • 基线十四要素:平台/CPU/亲和/电源/构建/策略——趋势只比同基线。
  • 双离差门限:MAD≤20%+中央范围≤30%——超限拒收不平均。
  • 中位数报告:样本序列全记录;median 对离群鲁棒。
  • 方向性阈值:慢 10% 严警/快 20% 宽警——快也是信号。
  • 扫描实验:参数空间系统遍历——选型依据的产地;报告归档 dev/bench。
  • smoke/全量两速:CI 秒级验证+正式分钟采样。
  • 基准即资产:benchmarks 挂清单——与源同生命周期管理。
  • 提问先行:写不出"怀疑 X 在 Y 下比 Z"就没到测量时。

常见错误

坑 1:单次采样当结论

症状:"优化后快了 8%"——单次跑的 3% 噪声背景里 8% 毫无意义;重跑方向都可能反。

原因:单次采样没有分布——没有离差就没有分辨率。repeats 采样+门限是分辨信号与噪声的最小配置。

错误写法
run_bench_once();      /* 单次采样 */
if ( faster_by_8pct() ) { merge(); }  /* 噪声里下结论 */
正确写法
/* 档案采样:repeats=5+离差门限;median 对比基线 */
measure_profile("queue", /*repeats*/ 5, /*gates*/ true);
if ( regression_significant(&New, &Base) ) { conclude(); }
坑 2:跨环境比趋势

症状:CI 数字总比本地好 20%——不知道该信谁。

原因:基线不同(CPU/电源/亲和/负载)。先对齐基线(同机同策略同构建)再比;跨环境只比结构结论(如批量曲线形状)不比绝对数。

错误写法
/* 本地 9M、CI 11M items/s */
if ( ci_faster() ) { trust_ci(); }   /* 基线不同:无意义 */
正确写法
/* 同机同策略 A/B:环境恒等只变代码 */
measure_ab(same_machine(), build_a(), build_b());
/* 跨机只比 sweep 曲线形态(结构性结论) */
坑 3:基准只测想看的路径

症状:优化让基准快了 30% 生产没变化——基准的热路径与真实负载不同。

原因:基准问题错了(提问纪律)。真实负载里批量占比多少?容量分布如何?先 profile 真实(stats/计数)再设计基准——基准的问题来自真实。

错误写法
/* 只测单条路径 */
bench_single_enqueue();      /* 优化它 */
/* 生产负载 90% 是批量调用——优化无感 */
正确写法
/* stats 先行:生产行为计数 → 批量为主 */
/* 基准含批量档 + sweep 定批量——问题来自真实 */
bench_batch_sweep();

编程练习

基础:跑一个基准档案

measure_performance.py --profiles task(或对应档)——读报告的基线与统计节。验收标准:能指出十四要素里你机器的取值;离差门限是否通过。

进阶:A/B 两版对比

对任一基准源做一个小改动(如加一个分支)——档案化 A/B 对比(同机同策略)。验收标准:报告正确判定"无显著差异";能解释为什么小改动在该门限下不可见。

挑战:一维扫描报告

仿 queue sweep:对某参数(如池容量)在 5 个点上采样——手写 sweep 脚本+表格报告(环境/矩阵/结果/解读四节——QUEUE_BENCH 报告的结构)。验收标准:拐点可辨识;解读给出选型建议;报告归档 dev/bench 风格。

小结

知识点速查
方法核心实验设计——档案声明/基线锁定/统计门限/方向阈值
档案profiles.json:源+args+smoke 两速+策略
基线十四要素;电源/亲和最易漏;趋势只比同基线
离差门限MAD≤20%+中央范围≤30%——超限拒收不平均
报告median+样本全记录;schema 2
回归慢 10% 严/快 20% 宽——快也是信号
sweep参数空间遍历——选型依据产地;报告一等资产
两速smoke 秒级 CI / 全量分钟采样
提问纪律"怀疑 X 在 Y 下比 Z"——写不出就没到测量时
微观测stats 计数先行——先看分配再测时间

📖 本章涉及的 API 参考手册:memory_stats(5 个条目)core(25 个条目)

点击查看完整签名、参数约束、返回值与错误说明