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
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:
*/ #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; }
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:
*/ #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; }
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:
Ownsdecides 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:
SetRetainpre-warms empty pages,Trimreturns them,Resetclears all; pages belong to the pool, business touches only slots. - Statistics reconciliation: verify pooling with Chapter 6's
pooled/backing; plan capacity with thelive/peakwater 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
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 */ }
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
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).
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".
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
| Topic | Quick reference |
|---|---|
| Fixed pool | same-size slots + paged growth (~64KB/page) + LIFO free-list reuse — slots never fragment |
| Reuse proof | free then allocate immediately gets the same address — the cache is still warm |
| The checkpoint | Owns decides ownership; foreign pointers are banned from Free — one slips in, the free list is polluted |
| Mark-reclaim | Sweep keeps the marked, deletes the rest / FreeMarked deletes the marked, keeps the rest |
| Page management | SetRetain pre-warms, Trim returns empty pages, Reset clears all |
| Variable-size pool | size buckets + large-block pass-through; pooling for mixed-size loads |
| Reconciliation tools | Chapter 6's pooled/backing staircase curve + live/peak water level |
| Division with arena | the pool manages "frequent take-and-put"; the arena manages "reclaim together"; the two often serve side by side |
| Three hosts | service-resident pool / request-scoped pool / allocator wholesale layer — different configuration emphases |
| Construction pairing | Alloc 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