Ориентация главы
Том 12 закрывается: глава 136 дала методологию; эта глава — практическое руководство — метод, применённый к самому XRT. Четыре блока: лестница оптимизации (счётчики stats → детализация debug → временные бенчмарки — фиксированный порядок «сначала распределения, потом время»; каждая ступень отвечает на другой вопрос); два рычага (пулинг — переиспользование объектов горячего пути (выбор между формами pool/pool_page); батчинг — амортизация системных вызовов и атомной координации (цепочка бенчмарк-доказательств глав 21/68)); отладка памяти на практике (тройка Reset/Snapshot/VisitLive локализует утечки; карантин ловит двойное освобождение/UAF; stats всегда включён для тревог — три слоя инструментов каждый на месте); выбор бэкенда (запрос и бенчмарк переключения корутин — основа выбора, когда переключение контекста миллион раз в секунду). Нити производительности всей книги (экономика скелета главы 62, диагностические поля главы 99, производительная грань матрицы владения главы 111) собираются здесь.
137.1引入
Здравый коэффициент оптимизации: большинство «проблем производительности» — не проблемы алгоритмов — это избыточные выделения (контейнер на каждый запрос), слишком частые системные вызовы (посылка по одному) или само измерение неверно (шум главы 136). Поэтому первое действие практики — всегда смотреть счётчики (stats: malloc_calls/backing — сколько выделилось, сколько дошло до системы), а не время — счётчики детерминированы (без шума и зависимости от среды) и прямо указывают причину; время говорит лишь «медленно», не «почему». XRT фиксирует эту лестницу в инструментах: stats (всегда включённые счётчики) → memory_debug (детализация событий) → bench (временная статистика) — каждая ступень отвечает на свой класс вопросов: сколько / которое / как долго.
Количественное наполнение двух рычагов приходит из бенчмарков, цитированных в главе 135: батчинг — MPSC одиночно около 8M items/s, батч 32 около 98M (в 12 раз: стоимость координации амортизирована); пулинг — малый блок попадает в пул с нулём системных вызовов (чтения pooled/backing в stats видны напрямую). Общая суть рычагов: амортизировать постоянные издержки — пул амортизирует выделение/освобождение, батч — синхронизацию/вызовы. Практический порядок: сперва спросить «можно ли батчами» (форма бизнеса позволяет — максимальная отдача), потом «стоит ли пулить» (жизненный цикл объектов стабилен — эффективно).
137.2概念
Лестница оптимизации: три ступени наблюдения
Дисциплина лестницы: перескакивать ступени (сразу мерить время) — самый частый окольный путь: аномалия выделений видна уже на первой. Стоимость инструментов растёт по ступеням (счётчики почти ноль / детализация дорогая / бенчмарки минутного уровня) — стоимость диктует политику постоянного включения: первая всегда, вторая при разборе, третья при изменениях.
Рычаг первый: пулинг
Выбор между формами (pool vs pool_page): многостраничный pool — ёмкость растёт автоматически (страница заполнилась — добавляется), LIFO-список свободных (переиспользование последнего освобождённого слота — кэш ещё тёплый); одностраничный pool_page — один непрерывный блок + чистая арифметика указателей (без цепочки страниц и межстраничных адресаций), ёмкость останавливается на пределе — первый выбор для встраиваемых/объектов горячего пути (узлы AST, кадры сообщений). Предпосылка пулинга: равновеликие объекты + стабильный жизненный цикл (ритм построил-использовал-освободил повторяется); когда не выполнено (размеры вперемешку/жизнь хаотична), пул только добавляет издержек — практическая перепроверка выбора главы 22. Проверка stats: после пулинга доля pooled растёт, backing падает — прямое чтение работы рычага.
Рычаг второй: батчинг
Батчинг амортизирует два класса издержек: системные вызовы (сетевая отправка по одному → семейство Vec за раз — батч-ступень пяти передач главы 68) и атомная координация (очередь по одному → пакетное резервирование — 12-кратное доказательство бенчмарка очереди). Проектное следствие: когда вызывающий может естественно группировать (много сообщений в кадре/много задач в запросе), предпочитать пакетные интерфейсы — это выбор API, а не трюк производительности; группировать нельзя — откат на одиночные (семантика интерфейсов равноценна — пакетная редакция «выучил одно применение, три очередиуниверсальны» главы 21). sweep фиксирует батч: отдача батча насыщается (свип очереди выравнивается после 32) — слепое увеличение только добавляет задержку, не пропускную способность — вывод свип-эксперимента главы 136 как выбор.
Практический поток отладки памяти
Поток локализации утечек (боевое построение инструментов главы 6): xrtMemDebugReset до подозрительного участка → прогон участка → Snapshot → LiveCount не к нулю — утечка → VisitLive печатает site (файл:строка) → исправить → повторный прогон к нулю. Поток локализации порчи: карантин с отложенным переиспользованием делает двойное освобождение/UAF неизбежно видимыми (а не молча проходящими) — точка падения + счётчики карантина (JSON-отчёт) перекрёстно локализуют. Тревоги долгого прогона: периодическая выборка live/peak из stats — монотонный рост = тренд утечки (не обязательно дожидаться OOM). Три потока делят один принцип: доказательства ошибок памяти ждут в инструментах — угадывать не нужно.
Выбор бэкенда: образец переключения корутин
Критический путь производительности корутин — переключение контекста (стоимость одного переключения × миллион в секунду). xrtCoBackend() запрашивает текущий бэкенд (семейство ассемблер/POSIX — определяют платформа и сборка); семейство бенчмарков dev/bench/coroutine измеряет: bench_context_switch (чистая пропускная способность переключения), bench_create_destroy (стоимость построения/разрушения корутины — основа пулинга), bench_sched_post (диспетчеризация планировщика), bench_timer_churn (дрожание фаз таймеров). Поток выбора: запрос бэкенда → бенчмарк context_switch → сравнение с переключением потоков (локальная проверка вывода главы 55) — числа поддерживают проектное решение (основа выбора корутины против потока).
137.3示例
Первая полная программа: измерение рычага пулинга
Программа ниже из examples/memory/pool_page — микронаблюдение одностраничного пула:
*/ #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; }
Что только что произошло. (1) stride=32 (шаг слота) и alignment=32 — равновеликие слоты, предпосылка пулинга, выраженная прямо на грани API; capacity=256 останавливается на пределе (граничная семантика одностраничной формы). (2) Сравнение с образцом pool (многостраничный: page_capacity=256 pages=2 ... peak=300 — автоматическое добавление страниц за пределом): выбор двух форм читается бок о бок — свобода роста против скорости чистой арифметики указателей. (3) Основа выбора для объектов горячего пути (узлы AST/кадры сообщений): теория главы 22 + граница этого образца + чтение pooled из stats — три грани составляют практическое суждение.
Вторая полная программа: семантика переиспользования многостраничного пула
Вторая программа из examples/memory/pool — измерение LIFO-переиспользования и роста страниц:
*/ #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; }
Что только что произошло. (1) peak=300 > 256×1 — переполнение ёмкости вызвало добавление страницы (pages=2); после освобождения всё empty=2 live=0 — страницы есть, но пусты (следующий раунд переиспользует без выделения — память пика в обмен на последующие нулевые выделения). (2) Кэш-смысл LIFO-списка (заголовочный комментарий): освобождённый слот входит в голову, следующий Alloc получает последний освобождённый слот — кэш ещё тёплый (точка проверки pReused == pReleased). (3) Полный цикл рычага пулинга: семантика переиспользования этого образца + чтение pooled из образца stats + бенчмарк уровня queue — от семантики к наблюдению к числам, трёхстороннее доказательство рычага.
137.4契约
- Дисциплина лестницы: stats (сколько) → debug (которое) → bench (как долго) — счётчики прежде времени; стоимость диктует политику постоянного включения.
- Батчинг прежде: когда естественная группировка есть, предпочитаются пакетные интерфейсы — рычаг уровня выбора API; точку насыщения фиксирует sweep.
- Предпосылки пулинга: равновеликие объекты + стабильный жизненный цикл; не выполнено — лучше не пулить. Две формы: одностраничная (чистая арифметика/предел)/многостраничная (автоматический рост/LIFO).
- Тройка утечек: Reset→Snapshot→VisitLive (site до файл:строка) — доказательства ждут в инструментах.
- Порча видима: карантин делает двойное освобождение/UAF неизбежно видимыми — без опоры на удачу.
- Тревоги долгого прогона: периодическая выборка live/peak — монотонный рост есть тревога тренда.
- Бэкенд запрашиваем:
xrtCoBackend+ семейство бенчмарк-ов coroutine — численный выбор стоимости переключения. - Переиспользование наблюдения: поля Info главы 99 (Wire/Bytes/метки времени) — сетевой осмотр производительности, дополняющий лестницу памяти.
Типичные ошибки
Симптом: несколько раундов оптимизаций, время не изменилось — в конце оказалось, узкое место — одно системное выделение на запрос (в stats видно с одного взгляда).
Причина: перескакивание ступеней. Время говорит только «медленно»; счётчики говорят «почему» (выделений много/пул не попадает/системные вызовы часты) — первая ступень лестницы всегда дешёвая и всегда включена.
/* подозрительно медленно -> сразу править код -> гонять бенчмарк */ tweak_something(); bench(); /* время не изменилось: почему было медленно, неизвестно - правили в пустоту */
/* сперва счётчики: снимок stats - backing аномально высок */ snapshot_stats(); /* локализовать горячее выделение -> пул/батч -> потом мерить время */
Симптом: внутренняя фрагментация пула съедает больше памяти, попадания малы — после пулинга stats ухудшаются.
Причина: предпосылка пула — равновеликость. Разные размеры (буфер свой на каждый запрос) через пул = потери в каждом классе размеров + издержки управления — практическая грань «не выполнено — не пулить» из главы 22.
/* буферы сообщений 32Б..4КБ вперемешку - всё в один пул */ /* внутренняя фрагментация + низкие попадания: памяти больше, производительности меньше */ pool_init(&OnePoolForEverything, 4096);
/* равновеликие кадры/узлы - в пул; вперемешку - в стандартную кучу */ pool_init(&FramePool, sizeof(frame)); /* равновеликие: законно */ /* вперемешку: достаточно проверить stats, что backing разумен */
Симптом: батч с 32 до 512 — пропускная способность не выросла, задержка выросла (ожидание накопления).
Причина: отдача батча насыщается (за пределом амортизации — чистая стоимость ожидания); кривая sweep фиксирует перегиб — после него увеличение батча только добавляет задержку.
#define BATCH 512 /* «чем больше, тем быстрее» - пропуск не вырос, задержка выросла *//* sweep находит точку насыщения: скажем, выравнивается после 32 */ #define BATCH 32 /* перегиб и есть выбор; для чувствительных к задержке сценариев меньше */
Практические задания
На бизнес-код с выделениями: сперва снимок stats (счётчики), затем тройка debug (детализация) — что отвечает каждая ступень. Критерии приёмки: ступень счётчиков может судить «аномалия или нет»; ступень детализации может назвать site любого события.
Нагрузка типа узлов AST (равновеликие 32Б цикл построил-использовал-освободил 100 тысяч раз): стандартная куча против pool_page — сравнение backing из stats + временной бенчмарк A/B. Критерии приёмки: после пулинга backing стремится к нулю; улучшение времени согласовано с долей выделений (объяснимо).
Выбрать комбинированный сценарий (скажем, разбор JSON + сборка контейнеров): пройти всю лестницу (локализация счётчиками → две оптимизации пул/батч → бенчмарк A/B) — написать отчёт из четырёх секций (среда/находки/изменения/результаты). Критерии приёмки: каждое изменение подкреплено числами; структура отчёта как у dev/bench (архивируем).
Итоги
| Тема | Кратко |
|---|---|
| Лестница | stats сколько → debug которое → bench как долго — счётчики прежде времени |
| Рычаг первый: пул | равновеликое + стабильный жизненный цикл; одностраничный (быстрый/фиксированная ёмкость)/многостраничный (рост/LIFO) |
| Рычаг второй: батч | амортизация системных вызовов + атомной координации; sweep фиксирует насыщение |
| Количественные доказательства | батч 12 раз (бенчмарк очереди); пул backing → ноль (stats) |
| Тройка утечек | Reset→Snapshot→VisitLive (site до строки) |
| Порча видима | карантин — двойное освобождение/UAF неизбежно проявляются |
| Тревоги долгого прогона | периодическая выборка live/peak — тренд без ожидания OOM |
| Выбор бэкенда | запрос CoBackend + бенчмарк переключения — числа для решения о корутинах |
| Сетевой осмотр | поля Info главы 99 — Wire/Bytes/метки времени |
| Закрывающая позиция | методология (135) приземлена (136) — инженерная практика тома сомкнута |
📖 Справочники API в этой главе:memory_stats (5 записей)memory_debug (19 записей)core (25 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки