Ориентация главы
Пул — аллокатор главы 5, ушедший «в опт»: равновеликие объекты выделяются слотами — страницы добавляются автоматически по заполнению, освобождения мгновенно уходят в LIFO-список свободных, а следующее выделение получает недавно освобождённый слот (кеш ещё тёплый). Сцены «одна и та же вещь десятками тысяч» — объекты соединений, кадровые структуры, заголовки сообщений — вот где пул сжимает стоимость выделения с «уровня системного вызова» до «вынуть один указатель из списка свободных», попутно устраняя присущую равновеликим выделениям фрагментацию — слоты никогда не разбрасываются, потому что их размеры не различаются. Глава покрывает полный жизненный цикл фиксированного пула xpool (страничность, LIFO-переиспользование, два направления сборки с пометкой) и пул переменного размера (для смешанных размеров) и проверяет выгоду пулования статистикой главы 6 — какую форму примет кривая отношения pooled к backing после пулования, вы почувствуете за один прогон.
22.1引入
Шлюзовый сервис держит сто тысяч соединений, каждый объект соединения — 48 байт. По одному xrtMalloc/xrtFree: каждый разрыв-переподключение проходит системный аллокатор, фрагментация копится понемногу, и в статистике главы 6 счёт backing растёт линейно с трафиком. Смените на фиксированный пул: объекты соединений берутся из слотов и возвращаются в слоты — выделение и освобождение обе операции с указателями; системный вызов случается только в мгновение «добавления страницы» (страница около 64 КБ вмещает 256 слотов по 48 байт до следующей). Каким бы ни был трафик, кривая backing — лестница: одна ступень на 256 выделений. В этом вся ценность пула — поднятие гранулярности выделений с уровня объектов до уровня страниц.
Когда пулом пользоваться не следует? Размеры объектов разбросаны (берите пул переменного размера или прямую кучу), количество объектов ничтожно (бухгалтерия пула дороже выгоды), время жизни предельно коротко и равномерно (арена главы 7 подходит лучше — пул отвечает «частое взять-вернуть», арена отвечает «убрать всё разом»; они отвечают на разные вопросы, их часто путают, но никогда не взаимозаменяют). Это «когда не» так же важно, как «когда да» выше: правильное употребление пула начинается с правильного «не употребляю».
22.2概念
Модель фиксированного пула
LIFO-переиспользование — не деталь реализации, а дружелюбное к кешу проектное решение: недавно освобождённый слот с высокой вероятностью ещё в каком-то уровне кеша; переиспользовать его быстрее, чем брать давно нетронутый — образец докажет pReused == pReleased утверждением. Owns спрашивает, принадлежит ли указатель этому пулу — контрольная рамка против посторонних указателей в Free (освободить кучевый указатель пулу — неопределённое поведение; Owns — тот пропускной пункт).
Два направления сборки с пометкой
По одному Free в пакетных сценах расточительно — тысячу объектов возвращать поштучно проигрывает пометке и одной обработке. Пул даёт оба направления: Sweep оставляет помеченное, собирает непомеченное (пакетный обработчик помечает «ещё нужное», остальное сметается — вытеснение кеша, чистка по концу запроса); FreeMarked освобождает помеченное, оставляет остальное (обнулённые по счётчику ссылок помечаются и возвращаются — полуавтоматическая сборка в паре с подсчётом ссылок главы 3). Два направления покрывают «оставить немногих — удалить многих» и «удалить немногих — оставить многих»; выбирайте, где пометок меньше.
Пул переменного размера и статистика пулования
Пул переменного размера (демонстрирует образец memory_pool) обрабатывает смешанные по размеру выделения: внутри — бакеты по размерам, малые идут через бакеты, большие напрямую. Колонки pooled (попадания в пул) и backing (системные вызовы) у xmemstats главы 6 — наблюдаемая грань пулования: в хорошо пулованном сервисе счёт бизнесовых выделений далеко превышает backing, а кривая — лестница (моменты добавления страниц). Планирование ёмкости и мемориальный бюджет тоже приземляются здесь: live/peak сообщают фактический уровень воды пула.
Страницы и возврат
SetRetain задаёт, сколько пустых страниц переживает Reset (прогрев под пиковый трафик); Trim активно возвращает пустые страницы (периоды тесной памяти); кучевые формы Create/CreateLayout могут явно закрепить число слотов на страницу (выровнять с вашим пакетным размером). Жизненный цикл страниц принадлежит пулу — вы никогда не трогаете страницы напрямую, только входите и выходите через слотовый интерфейс.
22.3示例
Первая полная программа: полный жизненный цикл фиксированного пула
Из репозиторного образца examples/memory/pool/main.c — страничный рост, 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) Выделено 300 объектов: 256 слотов на страницу, первые две страницы вмещают 512 — pages=2, peak=300. (2) Доказательство освободи-и-переиспользуй: освободите некоторый слот и сразу выделите — получите тот же адрес: LIFO-список свободных превращает «недавно освобождённый — первым переиспользуемым» в утверждаемое поведение. (3) После сборки с пометкой live=0, empty=2: обе страницы полностью свободны, но удерживаются (семантика прогрева SetRetain); visited=300 приходит из обхода Visit по порядку слотов — обходить живые объекты для статистики или как корни пометок GC и есть этот интерфейс. (4) Каждое число последней строки вывода сверяется со стороны статистики главы 6 — наблюдаемость пула и стягивание аллокатора в одну точку — одна система.
Вторая полная программа: пул переменного размера и уровневая статистика
Из 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; }
Что только что произошло. (1) Смешанные по размеру выделения: малые объекты попадают в бакеты размеров (счёт small), большие проходят напрямую (счёт large) — пул переменного размера наиболее выгоден на реальной нагрузке «в основном малые + немногие большие». (2) peak_bytes даёт измеренную основу мемориального бюджета: пиковый уровень воды пула и есть вход планирования ёмкости. (3) После освобождения всего live=0 — пул возвращается в исходное состояние и переиспользуем, Unit возвращает его целиком. Сравните с фиксированным пулом: переменный платит чуть больше скорости слотовой локации за управленческое удобство «не по пулу на каждый размер» — выбор между ними — это вопрос, ровно ли распределены размеры ваших объектов.
Отношение пула к аллокатору главы 5
Пул — не замена аллокатору, а оптовый слой над ним. Самая гибкая комбинация — та же замена аллокатора главы 5: реализуйте собственный аллокатор с маршрутизацией по размеру в соответствующий пул, и поведение выделений всей программы сходится в одной точке — отсюда и происходит счётчик pooled в memory_stats ядра (попадание 48-байтового размерного класса в кеш из образца главы 6 — работа встроенного в ядро пула малых объектов). Прикладные пулы прямее: пул соединений, пул кадров, пул сообщений — каждый ведает своими равновеликими объектами. Два маршрута не конфликтуют: пул ядра подстраховывает общие малые объекты, прикладные пулы несут доменные.
Конструирование объектов и слотовая опека
Пул ведает только взятием-возвратом слотов, никогда конструированием и разрушением объектов — содержимое взятого из пула слота не определено (кроме Calloc-версии, нуль-инициализированной). Стандартная пара: Alloc берёт слот, бизнесовая инициализация конструирует объект; разрушить объект, Free возвращает слот. Пакетным сценам, требующим деструкторных обратных вызовов, служит Visit — обойти живые объекты и завершить каждый, затем Reset/Trim для массовой сборки — обход-завершение, массовая сборка, управление страницами: эти три шага — полный замкнутый цикл управления жизненным циклом пулованных объектов; таблица соединений главы 67 употребит его полную форму.
22.4契约
- Равновеликие слоты: фиксированный пул нарезает по размеру + выравниванию, заданным в
Init; разным размерам — свои пулы либо пул переменного размера. - LIFO-переиспользование: освобождённые слоты мгновенно в список свободных; следующее выделение переиспользует недавнее.
- Пропускной пункт:
Ownsрешает принадлежность указателя; посторонним указателям вход в Free запрещён. - Направление пометок: оставить-немногих-удалить-многих — Sweep; удалить-немногих-оставить-многих — FreeMarked — выбирайте по стороне, где пометок меньше.
- Управление страницами:
SetRetainпрогревает пустые страницы,Trimвозвращает,Resetочищает всё; страницы принадлежат пулу, бизнес трогает только слоты. - Статистическая сверка: выгоду пулования проверяйте
pooled/backingглавы 6; ёмкость планируйте по уровню водыlive/peak.
От образцов к инженерии: три хозяина пула
Где живёт дескриптор пула, такова его роль. Пул-резидент сервиса: висит на объекте сервиса, прогрет при старте по плану ёмкости (пуловая версия семантики Reserve — предвыделение первой страницы), в рантайме — только взятие-возврат слотов; для шлюза на сто тысяч соединений пул соединений и есть эта форма. Короткоживущий пул запроса: создан внутри партии, Reset с Retain в конце партии, Unit при настоящем конце — разделение труда с ареной главы 7 (пул ведает взять-вернуть объектов, арена — временными фрагментами). Оптовый слой всего процесса: замена аллокатора главы 5, подключённая к пулам, — бизнесовый код не замечает, поведение выделений пулуется оптом. Три хозяина нагружают конфигурацию по-разному: резидентные пулы — планированием ёмкости и мониторингом уровня, короткоживущие — ритмом Retain/Trim, оптовый слой — статистикой распределения размеров.
Типичные ошибки
Симптом: список свободных загрязнён посторонним указателем, последующие выделения возвращают дикие указатели — падение в совершенно не относящемся к делу коде, самый трудноловимый класс аварий.
Причина: Free пула лишь «подвешивает слот обратно в список свободных», не проверяя происхождение указателя (проверка стоит, и горячий путь её не понесёт); подмешанный кучевый указатель в следующий раз выдаётся как слот.
ptr pObject = xrtMalloc(64); /* кучевое выделение */ /* ... после запутанного кода ... */ xrtPoolFree(&tPool, pObject); /* кучевый указатель в пул - список свободных загрязнён */
ptr pObject = xrtMalloc(64); /* не уверены в происхождении - спросите пул */ if ( xrtPoolOwns(&tPool, pObject) ) { xrtPoolFree(&tPool, pObject); } else { xrtFree(pObject); /* вернуть настоящему владельцу */ }
Симптом: после пакетной обработки обращение к старому указателю объекта читает новые данные или содержимое возвращённой страницы — «данные-призраки».
Причина: Reset/Sweep/Trim все несут семантику массовой сборки, разом инвалидируя все (или все непомеченные) слоты — каждый ранее выданный слотовый указатель истекает.
connection* pConn = xrtPoolAlloc(&tPool); /* взять слот */ ProcessRequest(pConn); xrtPoolReset(&tPool); /* массовая сборка: pConn истёк */ Reply(pConn); /* доступ к призраку */
connection* pConn = xrtPoolAlloc(&tPool); ProcessRequest(pConn); Reply(pConn); /* закончить использование прежде сборки - срок указателя кончается в точке сборки */ xrtPoolFree(&tPool, pConn); /* либо отдать пометочной сборке на границе пакета */
Практические задания
Выделите 10 слотов, освободите 3-й и сразу выделите — утвердите, что новый адрес равен освобождённому; затем освободите 3 последовательных слота и пронаблюдайте порядок переиспользования (позже освобождённые возвращаются первыми); напоследок пересчитайте Visit-ом живые объекты — должно быть 10-3-1+1 (прогоните свою последовательность операций прежде проверки).
Пакет запросов на «выделить-обработать + Sweep-сборке»: в обработке выживающие запросы помечаются по одному; в конце пакета Sweep разом собирает непомеченных; выведите число собранных и уровень live. Подсказка: направление Sweep подходит форме «большинство запросов завершается, немногие висят сквозь пакеты».
Сымитируйте сто тысяч «взять соединение — вернуть соединение»: сперва голым xrtMalloc/xrtFree, затем фиксированным пулом, обе версии со статистикой главы 6 по счёту backing и времени. Критерии приёмки: счёт backing у пулованной версии — лестница (~ступень на 256 выделений), время явно ниже голой кучи; две сравнительные таблицы приложите к упражнению.
Итоги
| Тема | Кратко |
|---|---|
| Фиксированный пул | равновеликие слоты + страничный рост (~64 КБ/страница) + переиспользование LIFO-списком свободных — слоты не фрагментируются никогда |
| Доказательство переиспользования | освободи и сразу выдели — вернётся тот же адрес: кеш ещё тёплый |
| Пропускной пункт | Owns решает принадлежность; посторонним указателям вход в Free запрещён — один проскочил, список свободных загрязнён |
| Сборка с пометкой | Sweep оставляет помеченное, удаляет остальное / FreeMarked удаляет помеченное, оставляет остальное |
| Управление страницами | SetRetain прогревает, Trim возвращает пустые страницы, Reset очищает всё |
| Пул переменного размера | бакеты размеров + прямой проход больших блоков; пулование смешанной нагрузки |
| Инструменты сверки | лестница pooled/backing главы 6 + уровень воды live/peak |
| Разделение с ареной | пул ведает «частым взять-вернуть»; арена — «убрать всё разом»; часто служат плечом к плечу |
| Три хозяина | пул-резидент сервиса / пул запроса / оптовый слой аллокатора — акценты конфигурации различны |
| Пара конструирования | Alloc берёт слот → бизнес конструирует; разрушить → вернуть слот; пакеты завершает Visit |
📖 Справочники API в этой главе:pool (69 записей)memory_stats (5 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки