Ориентация главы

Том 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概念

Лестница оптимизации: три ступени наблюдения

1Ступень 1 stats (всегда включённые счётчики)malloc_calls / pooled / direct / backing / live
2отвечает «сколько»масштаб выделений, попадания в пул, тренд живого (пороги тревог)
3Ступень 2 memory_debug (кратко при разборе)каждое событие + site + карантин
4отвечает «которое»точка утечки (файл:строка), сцены двойного освобождения/UAF
5Ступень 3 bench (профилированное время)median + пороги + A/B
6отвечает «как долго»до/после оптимизации, сравнение выбора, мониторинг регрессии

Дисциплина лестницы: перескакивать ступени (сразу мерить время) — самый частый окольный путь: аномалия выделений видна уже на первой. Стоимость инструментов растёт по ступеням (счётчики почти ноль / детализация дорогая / бенчмарки минутного уровня) — стоимость диктует политику постоянного включения: первая всегда, вторая при разборе, третья при изменениях.

Рычаг первый: пулинг

Выбор между формами (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 — микронаблюдение одностраничного пула:

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























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/memory/pool_page/main.c -lws2_32 -liphlpapistride=32 alignment=32 live=0 capacity=256

Что только что произошло. (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-переиспользования и роста страниц:

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 -include xrt.h impl.c examples/memory/pool/main.c -lws2_32 -liphlpapipage_capacity=256 pages=2 empty=2 live=0 peak=300 visited=300

Что только что произошло. (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/метки времени) — сетевой осмотр производительности, дополняющий лестницу памяти.

Типичные ошибки

Ловушка 1: мерить время, не глядя на счётчики:Ловушка 1: мерить время, не глядя на счётчики

Симптом: несколько раундов оптимизаций, время не изменилось — в конце оказалось, узкое место — одно системное выделение на запрос (в stats видно с одного взгляда).

Причина: перескакивание ступеней. Время говорит только «медленно»; счётчики говорят «почему» (выделений много/пул не попадает/системные вызовы часты) — первая ступень лестницы всегда дешёвая и всегда включена.

错误写法
/* подозрительно медленно -> сразу править код -> гонять бенчмарк */
tweak_something();
bench();   /* время не изменилось: почему было медленно, неизвестно - правили в пустоту */
正确写法
/* сперва счётчики: снимок stats - backing аномально высок */
snapshot_stats();
/* локализовать горячее выделение -> пул/батч -> потом мерить время */
Ловушка 2: пул для объектов вперемешку размеров:Ловушка 2: пул для объектов вперемешку размеров

Симптом: внутренняя фрагментация пула съедает больше памяти, попадания малы — после пулинга stats ухудшаются.

Причина: предпосылка пула — равновеликость. Разные размеры (буфер свой на каждый запрос) через пул = потери в каждом классе размеров + издержки управления — практическая грань «не выполнено — не пулить» из главы 22.

错误写法
/* буферы сообщений 32Б..4КБ вперемешку - всё в один пул */
/* внутренняя фрагментация + низкие попадания: памяти больше, производительности меньше */
pool_init(&OnePoolForEverything, 4096);
正确写法
/* равновеликие кадры/узлы - в пул; вперемешку - в стандартную кучу */
pool_init(&FramePool, sizeof(frame));   /* равновеликие: законно */
/* вперемешку: достаточно проверить stats, что backing разумен */
Ловушка 3: слепое увеличение батча:Ловушка 3: слепое увеличение батча

Симптом: батч с 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки