本章导读

卷十二收官:第 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概念

调优阶梯:三级观测

1一级 stats(常开计数)malloc_calls / pooled / direct / backing / live
2二级 memory_debug(排障短开)每笔事件+site+隔离区
3三级 bench(档案化时间)median+门限+A/B

阶梯纪律:跳级(直接测时间)是最常见的弯路——分配异常在一级就能看见。每级工具的成本递增(计数近零/明细高/基准分钟级)——成本决定常开策略:一级常开、二级排障、三级变更时。

杠杆一:池化

两形态的取舍(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——单页池的微观测:

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























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/memory/pool_page/main.c -lws2_32 -liphlpapistride=32 alignment=32 live=0 capacity=256

刚才发生了什么。 ① stride=32(槽步长)与 alignment=32——等大槽是池化的前提在 API 面的直接表达;capacity=256 到顶即止(单页形态的边界语义)。② 与 pool 示例对照(多页:page_capacity=256 pages=2 ... peak=300——自动加页越界增长):两形态的取舍用两个输出并读——增长自由 vs 纯指针算术的速度。③ 热路径对象(AST 节点/消息帧)的选型依据:第 22 章的理论 + 本例的边界 + stats 的 pooled 读数——三面合成实战判断。

第二个完整程序:多页池的复用语义

第二个程序来自 examples/memory/pool——LIFO 复用与页增长的实测:

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
























 */

#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;
}

$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/memory/pool/main.c -lws2_32 -liphlpapipage_capacity=256 pages=2 empty=2 live=0 peak=300 visited=300

刚才发生了什么。 ① 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/时间戳)是网络面的性能体检——与内存阶梯互补。

常见错误

坑 1:不看计数直接测时间

症状:优化几轮时间没变——最后发现瓶颈是每请求一次系统分配(stats 一眼可见)。

原因:跳级。时间只说"慢";计数说"为什么"(分配多/池未命中/系统调用频)——阶梯的第一级永远便宜且常开。

错误写法
/* 怀疑慢 → 直接改代码 → 跑基准 */
tweak_something();
bench();   /* 时间没变:不知道为何慢,改了个寂寞 */
正确写法
/* 先计数:stats 快照——backing 异常高 */
snapshot_stats();
/* 定位分配热 → 池化/批量 → 再测时间 */
坑 2:给杂尺寸对象上池

症状:池的内部碎片吃掉更多内存、命中率低——池化后 stats 反而变差。

原因:池的前提是等大。尺寸杂(每请求不同的缓冲)用池=每个尺寸类浪费+管理开销——这是第 22 章选型判断"不满足就不池"的实战面。

错误写法
/* 消息缓冲 32B..4KB 杂尺寸全走一个池 */
/* 内部碎片+低命中:内存反增、性能反降 */
pool_init(&OnePoolForEverything, 4096);
正确写法
/* 等大帧/节点走池;杂尺寸走默认堆 */
pool_init(&FramePool, sizeof(frame));   /* 等大:合法 */
/* 杂尺寸:stats 验证 backing 合理即可 */
坑 3:盲目加大批量

症状:批量从 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 个条目)

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