本章导读
卷十二收官:第 136 章给了方法论;本章是实战手册——把方法用在 XRT 自己身上。四块:调优阶梯(stats 计数 → debug 明细 → 时间基准——先看分配再测时间的固定次序;每级回答不同问题);两大杠杆(池化——热路径对象复用(pool/pool_page 两形态的取舍);批量——系统调用与原子协调的摊销(第 21/68 章的基准证据链));内存调试实战(Reset/Snapshot/VisitLive 三件套定位泄漏;隔离区抓双释放/UAF;stats 常开做告警——三层工具各就各位);后端选择(协程切换的 backend 查询与基准——上下文切换每秒百万次时的选型依据)。全书性能相关线索(第 62 章骨架经济学、第 99 章诊断字段、第 111 章所有权矩阵的性能面)在此收拢。
137.1引入
调优的常识率:大多数"性能问题"不是算法问题——是分配过多(每次请求建容器)、系统调用过频(逐条收发)、或测量本身错了(第 136 章的噪声)。因此实战的第一动作永远是看计数(stats:malloc_calls/backing——分配了多少、多少打到系统)而不是看时间——计数是确定性的(无噪声、无环境依赖),且直接指向原因;时间只告诉你"慢"不告诉你"为什么"。XRT 把这条阶梯固化进工具:stats(常开计数)→ memory_debug(事件明细)→ bench(时间统计)——每级回答一级问题:多少笔/哪一笔/多长时间。
两大杠杆的量化来自第 136 章引用的基准:批量——MPSC 单条约 8M items/s、批量 32 约 98M(12 倍:协调成本被摊销);池化——小块命中池零系统调用(stats 的 pooled/backing 读数直接可见)。两个杠杆的共同本质:摊销固定成本——池摊销分配/释放、批量摊销同步/调用。实战次序:先问"能批量吗"(业务形态允许即最大收益)再问"该池化吗"(对象生命周期稳定即有效)。
137.2概念
调优阶梯:三级观测
阶梯纪律:跳级(直接测时间)是最常见的弯路——分配异常在一级就能看见。每级工具的成本递增(计数近零/明细高/基准分钟级)——成本决定常开策略:一级常开、二级排障、三级变更时。
杠杆一:池化
两形态的取舍(pool vs pool_page):多页 pool——容量自动增长(页满加页)、LIFO 空闲表(复用最近释放的槽——缓存还热);单页 pool_page——一块连续+纯指针算术(无页链无跨页寻址)、容量到顶即止——嵌入式/热路径对象(AST 节点、消息帧)首选。池化的前提:等大对象+生命周期稳定(建-用-释的节奏重复);不满足(尺寸杂/生命乱)时池反而增加开销——第 22 章选型判断的实战复查。stats 验证:池化后 pooled 占比上升、backing 下降——杠杆生效的直接读数。
杠杆二:批量
批量摊销两类成本:系统调用(网络收发逐条→Vec 族一次——第 68 章发送五档的批量档)与原子协调(队列逐条→批量预留——queue 基准的 12 倍证据)。设计含义:调用方能自然分组(一帧多消息/一请求多任务)时优先批量接口——这是 API 选型而非性能技巧;不能分组时退单条(接口语义等价——第 21 章"学一种用法三种队列通用"的批量版)。sweep 定批量:批量收益有饱和点(queue sweep 显示 32 后趋平)——盲目加大只增延迟不增吞吐——第 136 章扫描实验的选型产出。
内存调试实战流
泄漏定位流(第 6 章工具的实战编队):怀疑段前 xrtMemDebugReset → 跑段 → Snapshot → LiveCount 不归零即泄漏 → VisitLive 打印 site(file:line)→ 修复 → 复跑归零。损坏定位流:隔离区(quarantine)延迟复用使双释放/UAF 必然显形(而非静默通过)——崩溃点+隔离区计数(JSON 报告)交叉定位。长运行告警:stats 的 live/peak 周期采样——单调上升=泄漏趋势(不必等到 OOM)。三流共用一个原则:内存错误的证据在工具里等着——不用猜。
后端选择:协程切换的样本
协程性能的关键路径是上下文切换(每次切换的成本 × 每秒百万次)。xrtCoBackend() 查询当前后端(汇编/POSIX 家族——平台与构建决定);dev/bench/coroutine 的基准族量化:bench_context_switch(纯切换吞吐)、bench_create_destroy(协程建销成本——池化的依据)、bench_sched_post(调度投递)、bench_timer_churn(定时器抖动)。选型流:backend 查询 → context_switch 基准 → 与线程切换对照(第 55 章结论的本地验证)——数字支持设计选择(协程化 vs 线程化的决策依据)。
137.3示例
第一个完整程序:池化杠杆的实测
下面的程序来自 examples/memory/pool_page——单页池的微观测:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xpoolpage tPage; xpoolpage* pCreated; xpoolpage* pCompact; xpoolpageinfo tInfo; int* pKeep; int* pDrop; int* pValue; size_t iIndex; /* int 槽 + 32 对齐:Stride 会被垫到 32(见输出)。 */ if ( !xrtPoolPageInitAligned(&tPage, sizeof(int), 32) ) { return 1; } pKeep = (int*)xrtPoolPageCalloc(&tPage); pDrop = (int*)xrtPoolPageAlloc(&tPage); if ( (pKeep == NULL) || (pDrop == NULL) ) { xrtPoolPageUnit(&tPage); return 2; } *pKeep = 7; *pDrop = 9; /* Sweep:标记 pKeep → 回收未标记的 pDrop(清出 1 个)。 */ if ( !xrtPoolPageMark(&tPage, pKeep) || (xrtPoolPageSweep(&tPage) != 1) ) { xrtPoolPageUnit(&tPage); return 3; } */ if ( !xrtPoolPageOwns(&tPage, pKeep) || !xrtPoolPageIndex(&tPage, pKeep, &iIndex) || (xrtPoolPageGet(&tPage, iIndex) != pKeep) ) { xrtPoolPageUnit(&tPage); return 4; } /* FreeMarked:与 Sweep 方向相反——释放标记者。 */ if ( !xrtPoolPageMark(&tPage, pKeep) || (xrtPoolPageFreeMarked(&tPage) != 1) ) { xrtPoolPageUnit(&tPage); return 5; } /* 按下标释放:边界检查内建,越界下标直接拒绝。 */ pValue = (int*)xrtPoolPageAlloc(&tPage); if ( (pValue == NULL) || !xrtPoolPageIndex(&tPage, pValue, &iIndex) || !xrtPoolPageFreeAt(&tPage, iIndex) ) { xrtPoolPageUnit(&tPage); return 6; } /* 按指针释放:内部同样做归属与边界校验。 */ pValue = (int*)xrtPoolPageAlloc(&tPage); if ( (pValue == NULL) || !xrtPoolPageFree(&tPage, pValue) ) { xrtPoolPageUnit(&tPage); return 7; } /* Reset 整页复位:清出剩余 2 个活动对象,容量不变可复用。 */ if ( (xrtPoolPageAlloc(&tPage) == NULL) || (xrtPoolPageCalloc(&tPage) == NULL) || (xrtPoolPageReset(&tPage) != 2) ) { xrtPoolPageUnit(&tPage); return 8; } xrtPoolPageGetInfo(&tPage, &tInfo); printf( "stride=%zu alignment=%zu live=%zu capacity=%zu\n", tInfo.Stride, tInfo.Alignment, tInfo.LiveCount, tInfo.Capacity ); xrtPoolPageUnit(&tPage); */ pCompact = xrtPoolPageCreateLayout(8192, 64, 4); if ( (pCompact == NULL) || (pCompact->Capacity != 4) || (pCompact->MemorySize > (8192u * 4u)) ) { xrtPoolPageDestroy(pCompact); return 9; } xrtPoolPageDestroy(pCompact); /* Create/Destroy:页对象放堆上的场景。 */ pCreated = xrtPoolPageCreate(sizeof(uint64)); if ( pCreated == NULL ) { return 10; } xrtPoolPageDestroy(pCreated); /* 默认 16 对齐 Init + 显式布局 Init + 指定对齐 Create。 */ { xpoolpage DefaultPage; xpoolpage LayoutPage; xpoolpage* pAligned; if ( !xrtPoolPageInit(&DefaultPage, sizeof(uint64)) ) { return 11; } if ( !xrtPoolPageInitLayout(&LayoutPage, sizeof(uint64), 32u, 8u) || (LayoutPage.Capacity != 8u) ) { xrtPoolPageUnit(&LayoutPage); return 12; } pAligned = xrtPoolPageCreateAligned(sizeof(uint64), 32u); if ( (pAligned == NULL) || (pAligned->Alignment < 32u) ) { xrtPoolPageDestroy(pAligned); xrtPoolPageUnit(&LayoutPage); xrtPoolPageUnit(&DefaultPage); return 13; } xrtPoolPageDestroy(pAligned); xrtPoolPageUnit(&LayoutPage); xrtPoolPageUnit(&DefaultPage); } return 0; }
刚才发生了什么。 ① stride=32(槽步长)与 alignment=32——等大槽是池化的前提在 API 面的直接表达;capacity=256 到顶即止(单页形态的边界语义)。② 与 pool 示例对照(多页:page_capacity=256 pages=2 ... peak=300——自动加页越界增长):两形态的取舍用两个输出并读——增长自由 vs 纯指针算术的速度。③ 热路径对象(AST 节点/消息帧)的选型依据:第 22 章的理论 + 本例的边界 + stats 的 pooled 读数——三面合成实战判断。
第二个完整程序:多页池的复用语义
第二个程序来自 examples/memory/pool——LIFO 复用与页增长的实测:
*/ #include <stdio.h> #include <xrt.h> /* 固定大小任务对象:32 字节(4 + 28),池按此步长切槽。 */ typedef struct examplejob { uint32 Id; char Name[28]; } examplejob; /* 访问器:按槽下标顺序回调;本例同时校验下标连续性。 */ static bool exampleVisitJob(ptr pObject, size_t iIndex, ptr pUserData) { size_t* pCount = (size_t*)pUserData; if ( (pObject == NULL) || (iIndex != *pCount) ) { return false; } (*pCount)++; return true; } int main(void) { xpool tPool; xpool* pCreated; xpool* pCompact; examplejob* arrJob[300]; examplejob* pReleased; examplejob* pReused; xpoolinfo tInfo; size_t iVisited = 0; /* 32 字节对齐:适合 SIMD 或缓存行对齐敏感的对象。 */ if ( !xrtPoolInitAligned(&tPool, sizeof(examplejob), 32) ) { return 1; } /* 300 个对象触发跨页增长(每页约 256 槽 → 2 页)。 */ for ( size_t i = 0; i < 300; i++ ) { arrJob[i] = (examplejob*)xrtPoolCalloc(&tPool); if ( arrJob[i] == NULL ) { xrtPoolUnit(&tPool); return 2; } arrJob[i]->Id = (uint32)i; } /* LIFO 复用验证:释放 10 号槽后立即取回——必须是同一个地址。 */ pReleased = arrJob[10]; if ( !xrtPoolFree(&tPool, pReleased) ) { xrtPoolUnit(&tPool); return 3; } pReused = (examplejob*)xrtPoolAlloc(&tPool); if ( pReused != pReleased ) { xrtPoolUnit(&tPool); return 4; } pReused->Id = 10; /* 归属自检 + 遍历计数:300 个全部在池且下标连续。 */ if ( !xrtPoolOwns(&tPool, pReused) ) { xrtPoolUnit(&tPool); return 5; } if ( xrtPoolVisit(&tPool, exampleVisitJob, &iVisited) != 300 ) { xrtPoolUnit(&tPool); return 6; } /* Sweep:只标记 0 号 → 回收其余 299 个。 */ if ( !xrtPoolMark(&tPool, arrJob[0]) || (xrtPoolSweep(&tPool) != 299) ) { xrtPoolUnit(&tPool); return 7; } /* FreeMarked:标记 0 号 → 释放它(与 Sweep 方向相反)。 */ if ( !xrtPoolMark(&tPool, arrJob[0]) || (xrtPoolFreeMarked(&tPool) != 1) ) { xrtPoolUnit(&tPool); return 8; } */ if ( xrtPoolTrim(&tPool, 0) == 0 ) { xrtPoolUnit(&tPool); return 9; } xrtPoolSetRetain(&tPool, 2); for ( size_t i = 0; i < 300; i++ ) { arrJob[i] = (examplejob*)xrtPoolCalloc(&tPool); if ( arrJob[i] == NULL ) { xrtPoolUnit(&tPool); return 10; } } /* Reset 批量释放全部 300 个,保留 2 个空页(见输出 empty=2)。 */ if ( xrtPoolReset(&tPool) != 300 ) { xrtPoolUnit(&tPool); return 11; } xrtPoolGet(&tPool, &tInfo); printf( "page_capacity=%zu pages=%zu empty=%zu live=%zu peak=%zu visited=%zu\n", tInfo.PageCapacity, tInfo.PageCount, tInfo.EmptyPages, tInfo.LiveCount, tInfo.PeakCount, iVisited ); xrtPoolUnit(&tPool); */ pCompact = xrtPoolCreate(8192); if ( (pCompact == NULL) || (pCompact->PageCapacity != 8) || (xrtPoolAlloc(pCompact) == NULL) || (pCompact->Pages->MemorySize > XRT_POOL_PAGE_BYTES_DEFAULT) ) { xrtPoolDestroy(pCompact); return 12; } xrtPoolDestroy(pCompact); /* 显式布局:每页恰好 128 槽(容量可查证)。 */ pCreated = xrtPoolCreateLayout(sizeof(examplejob), 32, 128); if ( pCreated == NULL ) { return 13; } if ( pCreated->PageCapacity != 128 ) { xrtPoolDestroy(pCreated); return 14; } xrtPoolDestroy(pCreated); /* 默认对齐 Init + 显式布局 Init + 指定对齐 Create。 */ { xpool DefaultPool; xpool LayoutPool; xpool* pAligned; if ( !xrtPoolInit(&DefaultPool, sizeof(examplejob)) ) { return 15; } if ( !xrtPoolInitLayout(&LayoutPool, sizeof(examplejob), 32u, 64u) || (LayoutPool.PageCapacity != 64u) ) { xrtPoolDestroy(&LayoutPool); return 16; } pAligned = xrtPoolCreateAligned(sizeof(examplejob), 32u); if ( (pAligned == NULL) || (pAligned->Alignment < 32u) ) { xrtPoolDestroy(pAligned); xrtPoolDestroy(&LayoutPool); xrtPoolDestroy(&DefaultPool); return 17; } xrtPoolDestroy(pAligned); xrtPoolDestroy(&LayoutPool); xrtPoolDestroy(&DefaultPool); } return 0; }
刚才发生了什么。 ① peak=300 > 256×1——容量越界触发了加页(pages=2);用毕清空后 empty=2 live=0——页在但空(下轮复用不再分配——峰值的内存换后续的零分配)。② LIFO 空闲表的缓存意义(头注释):Free 的槽进表头、下次 Alloc 拿到最近释放的槽——缓存还热(pReused == pReleased 的验证点)。③ 池化杠杆的完整闭环:本例的复用语义 + stats 示例的 pooled 读数 + queue 式的基准——从语义到观测到数字,杠杆的三面证据。
137.4契约
- 阶梯纪律:stats(多少)→ debug(哪笔)→ bench(多长)——先计数后时间;成本决定常开策略。
- 批量优先:能自然分组优先批量接口——API 选型级杠杆;饱和点由 sweep 定。
- 池化前提:等大对象+生命周期稳定;不满足不如不池。两形态:单页(纯算术/到顶即止)/多页(自动增长/LIFO 复用)。
- 泄漏三件套:Reset→Snapshot→VisitLive(site 到 file:line)——证据在工具里。
- 损坏显形:隔离区使双释放/UAF 必然显形——不靠运气。
- 长运行告警:live/peak 周期采样——单调上升即趋势告警。
- 后端可查:
xrtCoBackend+ coroutine 基准族——切换成本的数字选型。 - 观测复用:第 99 章 Info 字段(Wire/Bytes/时间戳)是网络面的性能体检——与内存阶梯互补。
常见错误
症状:优化几轮时间没变——最后发现瓶颈是每请求一次系统分配(stats 一眼可见)。
原因:跳级。时间只说"慢";计数说"为什么"(分配多/池未命中/系统调用频)——阶梯的第一级永远便宜且常开。
/* 怀疑慢 → 直接改代码 → 跑基准 */ tweak_something(); bench(); /* 时间没变:不知道为何慢,改了个寂寞 */
/* 先计数:stats 快照——backing 异常高 */ snapshot_stats(); /* 定位分配热 → 池化/批量 → 再测时间 */
症状:池的内部碎片吃掉更多内存、命中率低——池化后 stats 反而变差。
原因:池的前提是等大。尺寸杂(每请求不同的缓冲)用池=每个尺寸类浪费+管理开销——这是第 22 章选型判断"不满足就不池"的实战面。
/* 消息缓冲 32B..4KB 杂尺寸全走一个池 */ /* 内部碎片+低命中:内存反增、性能反降 */ pool_init(&OnePoolForEverything, 4096);
/* 等大帧/节点走池;杂尺寸走默认堆 */ pool_init(&FramePool, sizeof(frame)); /* 等大:合法 */ /* 杂尺寸:stats 验证 backing 合理即可 */
症状:批量从 32 提到 512——吞吐没涨、延迟涨了(攒批等待)。
原因:批量收益有饱和点(摊销完了就是纯等待成本);sweep 曲线定拐点——拐点后加批量只加延迟。
#define BATCH 512 /* "越多越快"——吞吐没涨延迟涨 *//* sweep 找饱和点:如 32 后趋平 */ #define BATCH 32 /* 拐点即选型;延迟敏感场景更小 */
编程练习
对一段含分配的业务代码:先 stats 快照(计数)再 debug 三件套(明细)——两级各回答什么。验收标准:计数级能判"是否异常";明细级能指出任意一笔的 site。
AST 节点式负载(等大 32B 建-用-释循环 10 万次):默认堆 vs pool_page——stats 的 backing 对比 + 时间基准 A/B。验收标准:池化后 backing 趋零;时间改善与分配占比一致(可解释)。
选一个组合场景(如 JSON 解析+容器组装):阶梯全走(计数定位→池化/批量两项优化→基准 A/B)——写四节报告(环境/发现/改动/结果)。验收标准:每项改动有数字支撑;报告结构同 dev/bench 报告(可归档)。
小结
| 知识点 | 速查 |
|---|---|
| 阶梯 | stats 多少 → debug 哪笔 → bench 多长——先计数后时间 |
| 杠杆一:池 | 等大+稳定生命;单页(快/定容)/多页(增长/LIFO 复用) |
| 杠杆二:批量 | 系统调用+原子协调摊销;sweep 定饱和点 |
| 量化证据 | 批量 12 倍(queue 基准);池化 backing 趋零(stats) |
| 泄漏三件套 | Reset→Snapshot→VisitLive(site 到行) |
| 损坏显形 | 隔离区——双释放/UAF 必现 |
| 长运行告警 | live/peak 周期采样——趋势不必等 OOM |
| 后端选型 | CoBackend 查询+切换基准——协程化决策的数字 |
| 网络面体检 | 第 99 章 Info 字段——Wire/Bytes/时间戳 |
| 收官定位 | 方法论(135)落地(136)——全卷工程实践合拢 |
📖 本章涉及的 API 参考手册:memory_stats(5 个条目)memory_debug(19 个条目)core(25 个条目)
点击查看完整签名、参数约束、返回值与错误说明