Chapter Orientation

The pool is Chapter 5's allocator gone "wholesale": same-size objects are allocated by slot — pages are added automatically when full, frees go straight onto a LIFO free list, and the next allocation gets the most recently freed slot (cache still warm). Scenes with "thousands of the same thing" — connection objects, frame structs, message headers — are where the pool compresses allocation cost from "syscall grade" to "pop one pointer off the free list", while eliminating the fragmentation that same-size allocation never had anyway — slots never scatter because their sizes never differ. This chapter covers the fixed pool xpool's complete lifecycle (paging, LIFO reuse, two mark-reclaim directions) and the variable-size pool (for mixed sizes), and verifies the pooling payoff with Chapter 6's stats — what shape the pooled-to-backing ratio curve takes after pooling is something you'll feel after one run.

22.1引入

A gateway service maintains a hundred thousand connections, each connection object 48 bytes. One-by-one xrtMalloc/xrtFree: every disconnect-reconnect passes through the system allocator, fragmentation accumulates slowly, and Chapter 6's stats show backing counts rising linearly with traffic. Switch to a fixed pool: connection objects come out of slots and go back into slots — allocation and free are both pointer operations; the syscall happens only at the instant of "adding a page" (a page is about 64KB and holds 256 slots of 48 bytes before the next one). However heavy the traffic, the backing curve is a staircase: one step per 256 allocations. That is the pool's entire value — raising the granularity of allocation from object level to page level.

When should you not use a pool? When object sizes vary wildly (use the variable-size pool or the plain heap), when object counts are tiny (pool bookkeeping outweighs the benefit), when lifetimes are uniformly short (Chapter 7's arena fits better — the pool answers "frequent take-and-put-back", the arena answers "reclaim all together"; they answer different questions, often confused, never interchangeable). This "when not to" matters as much as the "when to" above: correct pool use begins with a correct "not using".

22.2概念

The fixed-pool model

1Initializationbuild the pool by object size + alignment; per-page capacity is computed automatically (~64KB/page)
2AllocationAlloc/Calloc take a slot from the free list; if empty, add a page
3FreeFree goes straight onto the LIFO free list - the next allocation gets the most recently freed slot
4CleanupTrim returns empty pages / Reset clears everything / Unit destroys

LIFO reuse is not an implementation detail, it is a cache-friendly design decision: the most recently freed slot is very likely still in some cache level; reusing it beats taking a long-untouched slot — the example will prove pReused == pReleased as an assertion. Owns asks whether a pointer belongs to this pool — the security gate against foreign pointers sneaking into Free (Freeing a heap pointer to a pool is undefined behavior; Owns is that checkpoint).

Two mark-reclaim directions

Freeing one by one is wasteful in batch-processing scenes — returning a thousand objects individually loses to marking them and processing once. The pool offers both directions: Sweep keeps the marked and reclaims the unmarked (the batch processor marks "still wanted", everything else is swept away — cache eviction, end-of-request cleanup); FreeMarked frees the marked and keeps the rest (those whose reference count hit zero are marked and put back — semi-automatic reclamation paired with Chapter 3's reference counting). The two directions cover "keep the few, delete the many" and "delete the few, keep the many"; choose whichever side has fewer marks.

The variable-size pool and pooling statistics

The variable-size pool (demonstrated by the memory_pool sample) handles mixed-size allocation: internally bucketed by size, small allocations go through buckets, large ones pass straight through. Chapter 6's xmemstats columns pooled (pool hits) and backing (syscalls) are the observability face of pooling — in a well-pooled service, business allocation counts far exceed backing, and the curve is a staircase (the page-add moments). Capacity planning and memory budgeting also land here: live/peak tell you the pool's actual water level.

Pages and return

SetRetain sets how many empty pages survive a Reset (pre-warming for bursty traffic); Trim proactively returns empty pages (memory-tight periods); the heap-creation forms Create/CreateLayout can pin the slots per page explicitly (aligning with your business batch size). Page lifetimes belong to the pool — you never touch pages directly, only go in and out through the slot interface.

22.3示例

Complete program: the fixed pool's full lifecycle

From the repository example examples/memory/pool/main.c — paged growth, LIFO reuse, mark-reclaim, and statistics verification in one net:

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

What just happened. (1) Allocating 300 objects: 256 slots per page, the first two pages hold 512 — pages=2, peak=300. (2) The proof of free-and-reuse: free some slot then allocate immediately, and you get the same address back — the LIFO free list turns "most recently freed, first reused" into assertable behavior. (3) After mark-reclaim, live=0 and empty=2: both pages fully idle yet still retained (the SetRetain pre-warm semantics); visited=300 comes from Visit's slot-order traversal — walking live objects for statistics or as GC mark roots is this interface. (4) Every number in the last output line reconciles from Chapter 6's statistics side — the pool's observability and the allocator chokepoint are one system.

Complete program: the variable-size pool and water-level statistics

From examples/memory/memory_pool/main.c, pooling for mixed sizes:

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

What just happened. (1) Mixed-size allocation: small objects hit size buckets (the small count), large ones pass through (the large count) — the variable-size pool pays off most on the real workload of "mostly small + a few large". (2) peak_bytes gives the measured basis for the memory budget: the pool's peak water level is the input to capacity planning. (3) After freeing everything, live=0 — the pool returns to its initial state and is reusable, and Unit returns it wholesale. Compared with the fixed pool: the variable-size pool trades a bit of slot-location speed for the management convenience of "not one pool per size" — choosing between them is just asking whether your object sizes are uniform.

The pool's relation to Chapter 5's allocator

The pool is not the allocator's replacement; it is the wholesale layer above it. The most flexible combination is exactly Chapter 5's allocator replacement: implement a custom allocator doing size-based routing to the matching pool, and the whole program's allocation behavior converges at one point — this is where the kernel memory_stats pooled count comes from (the 48-byte size-class cache hit in Chapter 6's example is the kernel's built-in small-object pool at work). Business-side pools are more direct: a connection pool, a frame pool, a message pool, each minding its own same-size objects. The two routes don't conflict: the kernel pool backstops general small objects, business pools carry domain objects.

Object construction versus slot custody

The pool manages only slot take-and-put-back, never object construction or destruction — the slot content you get is undefined (except the Calloc variant, zero-initialized). The standard pairing: Alloc takes a slot, business initialization constructs the object; destruct the object, Free returns the slot. Batch scenes needing destructor callbacks use Visit to walk live objects and finish them one by one, then Reset/Trim for bulk reclamation — traversal-finish, bulk-reclaim, page management: these three steps are the complete closed loop of pooled-object lifecycle management, and Chapter 67's connection table will use its full form.

22.4契约

  • Same-size slots: the fixed pool slots by the size + alignment given at Init; different sizes get their own pools, or switch to the variable-size pool.
  • LIFO reuse: freed slots go straight onto the free list; the next allocation preferentially reuses the most recently freed.
  • The checkpoint: Owns decides pointer ownership; foreign pointers may not enter the pool's Free.
  • Mark direction: keep-few-delete-many uses Sweep; delete-few-keep-many uses FreeMarked — choose by whichever side has fewer marks.
  • Page management: SetRetain pre-warms empty pages, Trim returns them, Reset clears all; pages belong to the pool, business touches only slots.
  • Statistics reconciliation: verify pooling with Chapter 6's pooled/backing; plan capacity with the live/peak water level.

From examples to engineering: the pool's three hosts

Where the pool handle lives decides its role. Service-resident pool: hung on the service object, pre-warmed at startup per capacity planning (the pool version of the Reserve semantics is pre-allocating the first page), doing only slot take-and-put at runtime — for a hundred-thousand-connection gateway, the connection pool is this form. Request-scoped short-lived pool: created within a batch, Reset-with-Retain at batch end, Unit when truly done — division of labor with Chapter 7's arena (the pool manages object take-and-put, the arena manages temporary fragments). Process-wide wholesale layer: Chapter 5's allocator replacement wired to pools — business code is unaware, allocation behavior pooled wholesale. The three hosts weight configuration differently: resident pools emphasize capacity planning and water-level monitoring, short-lived pools the Retain/Trim rhythm, the wholesale layer size-distribution statistics.

Common Mistakes

Pitfall 1: Freeing a heap pointer to the pool:Pitfall 1: Freeing a heap pointer to the pool

Symptom: the free list is polluted by a foreign pointer, and later allocations return wild pointers — the crash lands in completely unrelated code, the hardest kind of accident to chase.

Cause: the pool's Free merely "hangs the slot back onto the free list"; it does not validate pointer provenance (validation costs, and the hot path won't carry it); the mixed-in heap pointer gets handed out as a slot next time.

错误写法
ptr pObject = xrtMalloc(64);        /* heap allocation */
/* ... after some tangled code ... */
xrtPoolFree(&tPool, pObject);        /* heap pointer into the pool - the free list is polluted */
正确写法
ptr pObject = xrtMalloc(64);
/* unsure of provenance? ask the pool first */
if ( xrtPoolOwns(&tPool, pObject) ) {
	xrtPoolFree(&tPool, pObject);
} else {
	xrtFree(pObject);                /* return it to the right owner */
}
Pitfall 2: using a slot pointer obtained before Reset:Pitfall 2: using a slot pointer obtained before Reset

Symptom: after batch processing, accessing an old object pointer reads new data or the content of a returned page — "ghost data".

Cause: Reset/Sweep/Trim all carry bulk-reclaim semantics, invalidating all (or all unmarked) slots at once — every slot pointer previously handed out expires.

错误写法
connection* pConn = xrtPoolAlloc(&tPool);   /* take a slot */
ProcessRequest(pConn);
xrtPoolReset(&tPool);                        /* bulk reclaim: pConn has expired */
Reply(pConn);                                /* ghost access */
正确写法
connection* pConn = xrtPoolAlloc(&tPool);
ProcessRequest(pConn);
Reply(pConn);                 /* finish using it before reclaiming - pointer validity ends at the reclaim point */
xrtPoolFree(&tPool, pConn);   /* or hand it to mark-reclaim at the batch boundary */

Programming Exercises

Basic: proving LIFO reuse

Allocate 10 slots, free the 3rd and allocate immediately — assert the new address equals the freed one; then free 3 consecutive slots and observe the reuse order (the later-freed come back first); finally use Visit to count live objects — it should be 10-3-1+1 (walk through your own operation sequence before verifying).

Advanced: a request batch processor (mark-reclaim in practice)

Implement a batch of requests with "allocate-process + Sweep mark-reclaim": during processing, surviving requests are Marked one by one; at batch end, Sweep reclaims the unmarked in one stroke; output the reclaimed count and the live water level. Hint: the Sweep direction fits the shape "most requests complete, a few hang across batches".

Challenge: reconciling before and after pooling (proving the pool's value with numbers)

Simulate a hundred thousand "take connection — return connection" cycles: once with bare xrtMalloc/xrtFree, once with a fixed pool, recording backing counts and elapsed time with Chapter 6's stats in both runs. Acceptance criteria: the pooled run's backing count is a staircase (~one step per 256 allocations) and its elapsed time is clearly below the bare-heap run; output the two comparison tables with the exercise.

Summary

TopicQuick reference
Fixed poolsame-size slots + paged growth (~64KB/page) + LIFO free-list reuse — slots never fragment
Reuse prooffree then allocate immediately gets the same address — the cache is still warm
The checkpointOwns decides ownership; foreign pointers are banned from Free — one slips in, the free list is polluted
Mark-reclaimSweep keeps the marked, deletes the rest / FreeMarked deletes the marked, keeps the rest
Page managementSetRetain pre-warms, Trim returns empty pages, Reset clears all
Variable-size poolsize buckets + large-block pass-through; pooling for mixed-size loads
Reconciliation toolsChapter 6's pooled/backing staircase curve + live/peak water level
Division with arenathe pool manages "frequent take-and-put"; the arena manages "reclaim together"; the two often serve side by side
Three hostsservice-resident pool / request-scoped pool / allocator wholesale layer — different configuration emphases
Construction pairingAlloc takes the slot → business constructs; destruct → return the slot; batches finish via Visit

📖 API references in this chapter:pool (69 entries)memory_stats (5 entries)

Click for full signatures, parameter constraints, return values and error notes