Chapter Orientation
Volume 12 closes: Chapter 136 gave the methodology; this chapter is the field manual — applying the method to XRT itself. Four blocks: the tuning ladder (stats counters → debug detail → time benchmarks — the fixed order of allocations first, time later; each rung answers a different question); the two levers (pooling — hot-path object reuse (the pool/pool_page trade); batching — amortizing system calls and atomic coordination (the benchmark evidence chain of Chapters 21/68)); memory debugging in practice (the Reset/Snapshot/VisitLive trio locating leaks; the quarantine catching double-free/UAF; stats always on for alarms — three tool layers each in place); backend choice (querying and benchmarking coroutine-switch backends — the selection evidence when context switches run a million times a second). The book's performance threads (Chapter 62's skeleton economics, Chapter 99's diagnostic fields, Chapter 111's ownership matrix's performance face) gather here.
137.1引入
The common-sense rate of tuning: most "performance problems" are not algorithm problems — they are over-allocation (building containers per request), over-frequent system calls (sending item by item), or the measurement itself being wrong (Chapter 136's noise). Hence practice's first action is always look at counters (stats: malloc_calls/backing — how many allocations, how many reached the system), not time — counters are deterministic (no noise, no environment dependency) and point straight at the cause; time only tells you "slow", never "why". XRT freezes this ladder into tools: stats (always-on counters) → memory_debug (event detail) → bench (time statistics) — each rung answers one class of question: how many / which one / how long.
The two levers' quantification comes from the benchmarks cited in Chapter 136: batching — MPSC single-item about 8M items/s, batch 32 about 98M (12×: coordination cost amortized); pooling — small blocks hitting the pool at zero system calls (stats' pooled/backing readings show it directly). The levers' shared essence: amortize fixed costs — pooling amortizes allocation/free, batching amortizes synchronous coordination/calls. Practical order: first ask "can it batch?" (business shape permitting, the biggest win), then "should it pool?" (effective when the object lifecycle is stable).
137.2概念
The tuning ladder: three observation rungs
Ladder discipline: skipping rungs (going straight to timing) is the most common detour — allocation anomalies are visible at rung one. Tool cost rises per rung (counters near zero / detail high / benchmarks minutes) — cost dictates the always-on policy: rung one always, rung two during troubleshooting, rung three on change.
Lever one: pooling
The two-form trade (pool vs pool_page): multi-page pool — capacity grows automatically (pages added when full), LIFO free list (reusing the most recently freed slot — the cache is still warm); single-page pool_page — one contiguous block + pure pointer arithmetic (no page chain, no cross-page addressing), capacity stops at the top — the first choice for embedded/hot-path objects (AST nodes, message frames). Pooling's precondition: equal-sized objects + stable lifetimes (the build-use-free rhythm repeats); when unmet (mixed sizes/messy lifetimes), pooling adds overhead instead — Chapter 22's selection judgment revisited in practice. Verified by stats: after pooling, the pooled share rises and backing falls — the lever working, read directly.
Lever two: batching
Batching amortizes two cost classes: system calls (network sends item by item → the Vec family in one — Chapter 68's batch rung of the five send gears) and atomic coordination (queues item by item → batch reservation — the queue benchmark's 12× evidence). Design implication: when the caller can naturally group (many messages per frame / many tasks per request), prefer batch interfaces — this is API selection, not a performance trick; when grouping is impossible, fall back to single items (interface semantics equivalent — Chapter 21's "learn one usage, three queues share it", batch edition). Sweep fixes the batch size: batch returns saturate (the queue sweep flattens past 32) — blindly enlarging only adds latency, not throughput — Chapter 136's sweep experiment as selection output.
The memory-debugging practice flows
Leak-localization flow (Chapter 6's tools in battle formation): xrtMemDebugReset before the suspect segment → run it → Snapshot → a LiveCount that won't reach zero means a leak → VisitLive prints the site (file:line) → fix → rerun to zero. Corruption-localization flow: the quarantine's delayed reuse makes double-free/UAF necessarily visible (instead of passing silently) — crash point + quarantine counts (the JSON report) cross-locate. Long-running alarms: periodic sampling of stats' live/peak — a monotonic rise = a leak trend (no need to wait for OOM). The three flows share one principle: the evidence for memory errors waits inside the tools — no guessing required.
Backend choice: the coroutine-switch sample
Coroutine performance's critical path is context switching (cost per switch × a million per second). xrtCoBackend() queries the current backend (assembly/POSIX family — decided by platform and build); the benchmark family of dev/bench/coroutine quantifies: bench_context_switch (pure switch throughput), bench_create_destroy (coroutine build/destroy cost — the basis for pooling), bench_sched_post (scheduler posting), bench_timer_churn (timer churn). The selection flow: backend query → context_switch benchmark → contrast with thread switching (Chapter 55's conclusion verified locally) — numbers support the design choice (the evidence base for coroutine-ize vs thread-ize).
137.3示例
First complete program: the pooling lever, measured
The program below is from examples/memory/pool_page — single-page pool micro-observation:
*/ #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; }
What just happened. (1) stride=32 (slot stride) and alignment=32 — equal-sized slots, pooling's precondition expressed directly on the API face; capacity=256 stops at the top (the single-page form's boundary semantics). (2) Against the pool sample (multi-page: page_capacity=256 pages=2 ... peak=300 — automatic page addition beyond capacity): the two-form trade read side by side — freedom to grow versus pure pointer arithmetic's speed. (3) The selection basis for hot-path objects (AST nodes/message frames): Chapter 22's theory + this example's boundary + stats' pooled reading — three faces composing the practical judgment.
Second complete program: multi-page pool reuse semantics
The second program is from examples/memory/pool — LIFO reuse and page growth, measured:
*/ #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) peak=300 > 256×1 — capacity overflow triggered page addition (pages=2); after clearing, empty=2 live=0 — pages present but empty (the next round reuses without allocating — peak memory traded for later zero allocations). (2) The LIFO free list's cache meaning (header comment): freed slots enter at the head, the next Alloc gets the most recently freed slot — cache still warm (the pReused == pReleased verification point). (3) The pooling lever's complete loop: this example's reuse semantics + the stats sample's pooled reading + a queue-style benchmark — from semantics to observation to numbers, the lever's three-way evidence.
137.4契约
- Ladder discipline: stats (how many) → debug (which one) → bench (how long) — counters before time; cost dictates the always-on policy.
- Batching first: when natural grouping exists, prefer batch interfaces — an API-selection-grade lever; the saturation point comes from sweep.
- Pooling preconditions: equal-sized objects + a stable lifecycle; when unmet, don't pool. Two forms: single-page (pure arithmetic / capped) / multi-page (auto growth / LIFO reuse).
- Leak trio: Reset→Snapshot→VisitLive (site to file:line) — the evidence waits in the tools.
- Corruption visible: the quarantine makes double-free/UAF necessarily visible — no relying on luck.
- Long-running alarms: periodic live/peak sampling — a monotonic rise is a trend alarm.
- Backend queryable:
xrtCoBackend+ the coroutine benchmark family — numeric selection of switch cost. - Observation reuse: Chapter 99's Info fields (Wire/Bytes/timestamps) are the network-face health check — complementary to the memory ladder.
Common Mistakes
Symptom: several optimization rounds, time unchanged — eventually finding the bottleneck is one system allocation per request (visible at a glance in stats).
Cause: skipping rungs. Time says only "slow"; counters say "why" (too many allocations / pool misses / frequent system calls) — the ladder's first rung is always cheap and always on.
/* suspect slow -> change code directly -> run the benchmark */ tweak_something(); bench(); /* time unchanged: never knew why it was slow - changed nothing */
/* counters first: a stats snapshot - backing abnormally high */ snapshot_stats(); /* locate the allocation hot spot -> pool/batch -> then time */
Symptom: the pool's internal fragmentation eats more memory, hit rate low — after pooling, stats actually worsen.
Cause: pooling's precondition is equal sizes. Mixed sizes (a different buffer per request) through a pool = waste per size class + management overhead — the practical face of Chapter 22's "when unmet, don't pool".
/* message buffers 32B..4KB, mixed sizes, all through one pool */ /* internal fragmentation + low hits: memory up, performance down */ pool_init(&OnePoolForEverything, 4096);
/* equal-sized frames/nodes through the pool; mixed sizes through the default heap */ pool_init(&FramePool, sizeof(frame)); /* equal size: legitimate */ /* mixed sizes: just verify backing is reasonable via stats */
Symptom: batch raised from 32 to 512 — throughput flat, latency up (waiting to fill batches).
Cause: batch returns saturate (past amortization it's pure waiting cost); the sweep curve fixes the inflection — beyond it, more batch only adds latency.
#define BATCH 512 /* "more is faster" - throughput flat, latency up *//* sweep finds the saturation point: flattens past, say, 32 */ #define BATCH 32 /* the inflection is the choice; latency-sensitive scenes go smaller */
Programming Exercises
On business code with allocations: first a stats snapshot (counters), then the debug trio (detail) — what does each rung answer? Acceptance criteria: the counter rung can judge "abnormal or not"; the detail rung can name any single event's site.
An AST-node-like workload (equal-sized 32B build-use-free looped 100k times): default heap vs pool_page — stats' backing comparison + a time benchmark A/B. Acceptance criteria: backing trends to zero after pooling; the time improvement consistent with the allocation share (explainable).
Pick a combined scenario (say JSON parsing + container assembly): walk the whole ladder (counter localization → pooling/batching optimizations → benchmark A/B) — write a four-section report (Environment/Findings/Changes/Results). Acceptance criteria: every change backed by numbers; the report structured like a dev/bench report (archivable).
Summary
| Topic | Quick reference |
|---|---|
| Ladder | stats how many → debug which one → bench how long — counters before time |
| Lever one: pooling | equal-sized + stable lifetimes; single-page (fast/fixed capacity) / multi-page (growth/LIFO reuse) |
| Lever two: batching | amortizes system calls + atomic coordination; sweep fixes the saturation point |
| Quantified evidence | batching 12× (queue benchmark); pooling backing → zero (stats) |
| Leak trio | Reset→Snapshot→VisitLive (site to line) |
| Corruption visible | the quarantine — double-free/UAF always shows |
| Long-running alarms | periodic live/peak sampling — trends without waiting for OOM |
| Backend selection | CoBackend query + switch benchmark — numbers for the coroutine decision |
| Network-face check | Chapter 99's Info fields — Wire/Bytes/timestamps |
| Closing position | methodology (135) landing (136) — the volume's engineering practice joined |
📖 API references in this chapter:memory_stats (5 entries)memory_debug (19 entries)core (25 entries)
Click for full signatures, parameter constraints, return values and error notes