本章导读

池是第 5 章分配器的"批发放电商":等大对象按槽分配——页满自动加页、释放即时进 LIFO 空闲表、下一次分配拿到的是最近释放的槽(缓存还热)。连接对象、帧结构、消息头这类"同一种东西成千上万个"的场景,池把分配成本从"系统调用级"压到"空闲表弹出一个指针",同时消除了等大分配天然的碎片问题——槽永远不会因为大小不齐而散布。本章讲固定池 xpool 的完整生命周期(分页、LIFO 复用、两种标记回收)与变长池(大小混杂的场景),并用第 6 章统计验证池化收益——pooled 与 backing 的比值曲线在池化后是什么形状,跑一遍就有直觉。

22.1引入

一个网关服务维护十万条连接,每条连接对象 48 字节。逐个 xrtMalloc/xrtFree:每次断连重连都过系统分配器,碎片慢慢累积,第 6 章统计里 backing 次数随流量线性上涨。换成固定池:连接对象从槽里取、还回槽里——分配与释放都是指针操作;系统调用只发生在"加页"那一瞬间(一页约 64KB,装 256 个 48 字节的槽之后才有下一页)。流量再大,backing 曲线是台阶状:每 256 次分配才跳一格。这就是池的全部价值——把分配的粒度从对象级提到页级。

什么时候不该用池?对象大小五花八门(用变长池或直接堆)、对象数量极少(池的管理成本大于收益)、生命周期极短且整齐(第 7 章 arena 更合适——池解决"频繁取放",arena 解决"一起回收",两者是不同问题的答案,常被混淆却从不可互换)。这段"什么时候不用"与前面的"什么时候用"同样重要:池的正确使用从正确的"不用"开始。

22.2概念

固定池模型

1初始化按对象大小+对齐建池,每页容量自动计算(约 64KB/页)
2分配Alloc/Calloc 从空闲表取槽;表空则加页
3释放Free 即时入 LIFO 空闲表——下次分配拿到最近释放的槽
4收尾Trim 归还空页 / Reset 全量清空 / Unit 销毁

LIFO 复用不是实现细节,是缓存友好的设计决策:最近释放的槽大概率还在各级缓存里,复用它比取一个久未触碰的槽更快——示例会实证 pReused == pReleased。Owns 判断指针是否属于本池——防御外来指针混入释放(把堆指针 Free 给池是未定义行为,Owns 是那道安检门)。

两种标记回收

逐个 Free 在批量处理场景是浪费——一千个对象逐个还,不如标记后一次处理。池提供两个方向:Sweep 保留标记、回收未标记(批量处理器标记"还要的",其余一扫而空——缓存逐出、请求结束清理);FreeMarked 释放标记、保留其余(引用计数归零者被标记后放回——与第 3 章引用计数配合的半自动回收)。两个方向覆盖了"留少数删多数"与"删少数留多数"两种形态,选方向看哪边标记更少。

变长池与池化统计

变长池(memory_pool 范例演示)处理大小混杂的分配:内部按尺寸分桶,小分配走桶、大分配直通。第 6 章 xmemstats 的 pooled(池化命中)与 backing(系统调用)两列就是池化效果的可观测面——池化良好的服务,业务分配次数远大于 backing,且曲线呈台阶状(加页时刻)。容量规划与内存预算也在这一步落地:live/peak 告诉你池的实际水位。

页与归还

SetRetain 设定 Reset 后保留多少空页(应对突发流量的预热);Trim 主动归还空页(内存紧张期);Create/CreateLayout 堆创建形态可显式指定每页槽数(对齐你的业务批量)。页的生命周期归池——你从不直接碰页,只通过槽接口进出。

22.3示例

完整程序:固定池全生命周期

来自仓库范例 examples/memory/pool/main.c——分页增长、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 impl.c examples/memory/pool/main.c -lws2_32 -liphlpapipage_capacity=256 pages=2 empty=2 live=0 peak=300 visited=300

刚才发生了什么。 ① 分配 300 个对象:每页 256 槽,前两页装 512——pages=2、peak=300。② 释放与复用的实证:释放某槽后立刻分配,拿回同一个地址——LIFO 空闲表把"最近释放优先复用"写进了可断言的行为。③ 标记回收后 live=0、empty=2:两页全部空闲但页还保留着(SetRetain 的预热语义);visited=300 来自 Visit 的按槽序遍历——遍历活动对象做统计、做 GC 标记起点都是这个接口。④ 输出最后一行的每个数字都能从第 6 章统计侧对上——池的可观测性与分配器收口是同一套体系。

完整程序:变长池与水位统计

来自 examples/memory/memory_pool/main.c,大小混杂场景的池化:

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






















 */

#include <stdio.h>
#include <string.h>

#include <xrt.h>



/* 访问器:每块回调一次(指针/大小/对齐);返回 false 终止遍历。 */
static bool exampleVisitMemory(
	ptr pMemory,
	size_t iSize,
	size_t iAlignment,
	ptr pUserData
)
{
	size_t* pCount = (size_t*)pUserData;

	if ( (pMemory == NULL) || (iSize == 0) || (iAlignment == 0) ) {
		return false;
	}
	(*pCount)++;
	return true;
}



int main(void)
{
	xmempool tPool;          /* 栈句柄;128 为初始保留字节提示 */
	xmempool* pCreated;
	char* sText;
	bytes pPacket;
	bytes pAligned;
	xmempoolinfo tInfo;
	size_t iVisited = 0;

	if ( !xrtMemPoolInit(&tPool, 128) ) {
		return 1;
	}

	/* 三种分配形态:清零小块 / 4KB 大块 / 64 字节对齐块。 */
	sText = (char*)xrtMemPoolCalloc(&tPool, 1, 24);
	pPacket = (bytes)xrtMemPoolAlloc(&tPool, 4096);
	pAligned = (bytes)xrtMemPoolAllocAligned(&tPool, 7, 64);
	if ( (sText == NULL) || (pPacket == NULL) || (pAligned == NULL) ) {
		xrtMemPoolUnit(&tPool);
		return 2;
	}

	/* Realloc 扩容:内容保留("xrt memory pool" 原样可读)。 */
	memcpy(sText, "xrt memory pool", 16);
	sText = (char*)xrtMemPoolRealloc(&tPool, sText, 80);
	if ( (sText == NULL) || (strcmp(sText, "xrt memory pool") != 0) ) {
		xrtMemPoolUnit(&tPool);
		return 3;
	}

	/* 三项自检:池归属 / 块大小 ≥ 80 / 对齐块地址 % 64 == 0。 */
	if (
		!xrtMemPoolOwns(&tPool, sText) ||
		(xrtMemPoolSize(&tPool, sText) < 80) ||
		(((uintptr_t)pAligned % 64) != 0)
	) {
		xrtMemPoolUnit(&tPool);
		return 4;
	}

	/* 访问器应数到 3 个活动块。 */
	if ( xrtMemPoolVisit(&tPool, exampleVisitMemory, &iVisited) != 3 ) {
		xrtMemPoolUnit(&tPool);
		return 5;
	}

	




	 */
	if (
		!xrtMemPoolMark(&tPool, sText) ||
		!xrtMemPoolMark(&tPool, pPacket) ||
		(xrtMemPoolSweep(&tPool) != 1)
	) {
		xrtMemPoolUnit(&tPool);
		return 6;
	}

	


	 */
	if (
		!xrtMemPoolMark(&tPool, pPacket) ||
		(xrtMemPoolFreeMarked(&tPool) != 1) ||
		!xrtMemPoolFree(&tPool, sText)
	) {
		xrtMemPoolUnit(&tPool);
		return 7;
	}

	/* Reset 批量释放活动块(返回释放数);Trim 把尺寸类空页还给系统。 */
	if (
		(xrtMemPoolAlloc(&tPool, 16) == NULL) ||
		(xrtMemPoolCalloc(&tPool, 2, 32) == NULL) ||
		(xrtMemPoolReset(&tPool) != 2)
	) {
		xrtMemPoolUnit(&tPool);
		return 8;
	}
	(void)xrtMemPoolTrim(&tPool, 0);

	/* 状态查询:reset 后 live 归零,峰值仍记录历史最高水位。 */
	xrtMemPoolGet(&tPool, &tInfo);
	printf(
		"visited=%zu small=%zu large=%zu live=%zu peak_bytes=%zu\n",
		iVisited,
		tInfo.SmallCount,
		tInfo.LargeCount,
		tInfo.LiveCount,
		tInfo.PeakBytes
	);
	xrtMemPoolUnit(&tPool);

	/* Create/Destroy:池对象本身放堆上的场景(跨函数传递)。 */
	pCreated = xrtMemPoolCreate(0);
	if ( pCreated == NULL ) {
		return 9;
	}
	xrtMemPoolDestroy(pCreated);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/memory/memory_pool/main.c -lws2_32 -liphlpapivisited=3 small=0 large=0 live=0 peak_bytes=4215

刚才发生了什么。 ① 混合大小分配:小对象命中尺寸桶(small 计数)、大对象直通(large 计数)——变长池对"大部分小 + 少数大"的真实负载最有性价比。② peak_bytes 给出内存预算的实测口径:池的水位峰值就是容量规划的输入。③ 全部释放后 live=0——池回到初始状态可复用,Unit 时整体归还。对比固定池:变长池牺牲一点槽定位的速度,换来"不必为每种大小建一个池"的管理简便——两种池的选择就看你的对象大小分布是否整齐。

池与第 5 章分配器的关系

池不是分配器的替代品,是它上面的批发层。最灵活的组合方式正是第 5 章的分配器替换:把自定义分配器实现成按大小路由到对应池,整个程序的分配行为就在一处收口——这正是内核 memory_stats 里 pooled 计数的来源(第 6 章示例的 48 字节命中尺寸类缓存,就是内核内建的小对象池在工作)。业务侧自建池则更直接:连接池、帧池、消息池各管各的等大对象。两条路线不冲突:内核池兜底通用小对象,业务池承载领域对象。

对象构造与槽的分工

池只管槽的取放,不管对象的构造与析构——从池里拿到的槽内容是未定义的(Calloc 版本除外,零初始化)。取放与构造的标准配对是:Alloc 拿槽、业务初始化构造对象;析构对象、Free 还槽。需要析构回调的批量场景用 Visit 遍历活动对象逐个收尾,再 Reset/Trim 批量回收——遍历收尾、批量回收、页管理这三步是池化对象生命周期管理的完整闭环,第 67 章连接表会用到它的完全体。

22.4契约

  • 等大槽:固定池按 Init 时的大小+对齐分槽;不同大小各自建池或改用变长池。
  • LIFO 复用:释放的槽即时回空闲表,下次分配优先复用最近释放者。
  • 安检:Owns 判指针归属;外来指针不得进池的 Free。
  • 标记方向:留少删多数用 Sweep;删少留多数用 FreeMarked——按标记少的一边选。
  • 页管理:SetRetain 预热空页、Trim 归还、Reset 全清;页归池管,业务只碰槽。
  • 统计对账:池化效果用第 6 章 pooled/backing 验证;水位用 live/peak 规划容量。

从示例到工程:池的三个宿主

池句柄的宿主位置决定它的角色。服务级常驻池:挂在服务对象上,启动时按容量规划预热(Reserve 语义的池版本是预分配首页),运行期只做槽取放——十万连接的网关,连接池就是这种形态。请求级短命池:一批处理内创建、批末 Reset 留 Retain 页、彻底结束时 Unit——配合第 7 章 arena 分工(池管对象取放、arena 管临时片段)。全进程批发层:第 5 章分配器替换接池——业务代码无感知,分配行为整体池化。三种宿主对池的配置侧重不同:常驻池重容量规划与水位监控、短命池重 Retain/Trim 节奏、批发层重大小分布统计。

常见错误

坑 1:把堆指针 Free 给池

症状:空闲表被外来指针污染,后续分配返回野指针——崩溃点在完全无关的代码里,是最难追的一类事故。

原因:池的 Free 只是"把槽挂回空闲表",它不校验指针来源(校验有成本,热路径不背);混入的堆指针下次被当槽分配出去。

错误写法
ptr pObject = xrtMalloc(64);        /* 堆分配 */
/* ... 混乱的代码之后 ... */
xrtPoolFree(&tPool, pObject);        /* 堆指针进池——空闲表被污染 */
正确写法
ptr pObject = xrtMalloc(64);
/* 不确定来源就先问池 */
if ( xrtPoolOwns(&tPool, pObject) ) {
	xrtPoolFree(&tPool, pObject);
} else {
	xrtFree(pObject);                /* 归还给正确的所有者 */
}
坑 2:Reset 后继续用之前拿到的槽指针

症状:批量处理后访问旧对象指针,读到新数据或已归还页的内容——"幽灵数据"。

原因:Reset/Sweep/Trim 都是批量回收语义,一次性作废全部(或未标记的)槽——此前发出的槽指针全部过期。

错误写法
connection* pConn = xrtPoolAlloc(&tPool);   /* 拿到槽 */
ProcessRequest(pConn);
xrtPoolReset(&tPool);                        /* 批量回收:pConn 已过期 */
Reply(pConn);                                /* 幽灵访问 */
正确写法
connection* pConn = xrtPoolAlloc(&tPool);
ProcessRequest(pConn);
Reply(pConn);                 /* 用完再回收——指针有效期止于回收点 */
xrtPoolFree(&tPool, pConn);   /* 或交给标记回收在批次边界统一处理 */

编程练习

基础:LIFO 复现实证

分配 10 个槽,释放第 3 个后立刻分配——断言新地址等于被释放地址;再释放 3 个连续槽,观察复用顺序(后释放的先回来);最后用 Visit 清点活动对象数应为 10-3-1+1(按你的操作序列推一遍再验证)。

进阶:请求批处理器(标记回收实战)

用"分配处理 + Sweep 标记回收"实现一批请求:处理过程中存活请求逐个 Mark,批次结束 Sweep 一次性回收未标记者;输出回收数与 live 水位。提示:Sweep 方向适合"多数请求完成、少数挂起跨批"的形态。

挑战:池化前后对账(用数字证明池的价值)

写一个十万次"取连接-还连接"的模拟:先用裸 xrtMalloc/xrtFree 跑一遍、再用固定池跑一遍,两遍都用第 6 章统计记录 backing 次数与耗时。验收标准:池化版 backing 次数呈台阶(约每 256 次分配一跳)、耗时显著低于裸堆版;两张对照表随练习输出。

小结

知识点速查
固定池等大槽 + 分页增长(约 64KB/页)+ LIFO 空闲表复用,槽永不碎片化
复用实证释放后立即分配拿回同地址——缓存还热
安检Owns 判归属;外来指针禁止进 Free——混入即空闲表污染
标记回收Sweep 留标记删其余 / FreeMarked 删标记留其余
页管理SetRetain 预热、Trim 归还空页、Reset 全清
变长池尺寸分桶 + 大块直通;大小混杂负载的池化
对账工具第 6 章 pooled/backing 台阶曲线 + live/peak 水位
与 arena 分工池管"频繁取放";arena 管"一起回收";两者常在同一个服务里并肩
三宿主服务常驻池 / 请求短命池 / 分配器批发层——配置侧重各不同
构造配对Alloc 拿槽→业务构造;析构→还槽;批量用 Visit 收尾

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

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