本章导读
调优的第一步不是改代码而是会测量——本章是性能工作的方法论文(第 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概念
档案化基准:声明式实验
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 读数作为最轻量的性能观测:
*/ #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; }
刚才发生了什么。 ① 性能分析的第一问常是"分配了多少"——stats 四读数回答它:本例 48B 命中池(零系统调用)而 4096B 直通——池边界的实测(比读文档快且永远与当前构建一致)。② 这是微观测(计数级)——时间维度的测量走 bench 工具链;但很多"性能问题"在计数层就有答案(backing 异常高=池配置不匹配业务尺寸)。③ 方法论位置:stats 是零成本常开的观测(第 135 章健康面板),也是性能调查的第一站——先看计数再测时间。
第二个完整程序:确定性输出的自检基准
第二个程序来自 examples/core/version_limits——基准的可信形态:
*/ #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; }
刚才发生了什么。 ① 这个示例演示确定性输出——与时间基准的随机性对照:基准工程里"可复现"分两种(确定性输出=每次同值;统计性输出=分布稳定)。② 资源边界默认值(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"就没到测量时。
常见错误
症状:"优化后快了 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(); }
症状:CI 数字总比本地好 20%——不知道该信谁。
原因:基线不同(CPU/电源/亲和/负载)。先对齐基线(同机同策略同构建)再比;跨环境只比结构结论(如批量曲线形状)不比绝对数。
/* 本地 9M、CI 11M items/s */ if ( ci_faster() ) { trust_ci(); } /* 基线不同:无意义 */
/* 同机同策略 A/B:环境恒等只变代码 */ measure_ab(same_machine(), build_a(), build_b()); /* 跨机只比 sweep 曲线形态(结构性结论) */
症状:优化让基准快了 30% 生产没变化——基准的热路径与真实负载不同。
原因:基准问题错了(提问纪律)。真实负载里批量占比多少?容量分布如何?先 profile 真实(stats/计数)再设计基准——基准的问题来自真实。
/* 只测单条路径 */ bench_single_enqueue(); /* 优化它 */ /* 生产负载 90% 是批量调用——优化无感 */
/* stats 先行:生产行为计数 → 批量为主 */ /* 基准含批量档 + sweep 定批量——问题来自真实 */ bench_batch_sweep();
编程练习
measure_performance.py --profiles task(或对应档)——读报告的基线与统计节。验收标准:能指出十四要素里你机器的取值;离差门限是否通过。
对任一基准源做一个小改动(如加一个分支)——档案化 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 个条目)
点击查看完整签名、参数约束、返回值与错误说明