Ориентация главы
Глава 5 закрыла «память, удерживаемую подолгу»; эта глава закрывает её двойника: множества короткоживущих объектов. Разбор одного кадра JSON порождает сотни мелких фрагментов; обработка одного HTTP-запроса собирает десятки временных строк — поблочный xrtMalloc/xrtFree не только медленен, хуже: путь отказа должен чистить блок за блоком, и объём кода взрывается. Ответ XRT — арена: отрезок памяти выделяется целиком; «выделение» внутри — лишь сдвиг указателя, «освобождение» не делает ничего, «возврат» отдаёт весь отрезок. Парсеры, протокольные стеки и планировщики корутин всей библиотеки имеют встроенные арены — поняв эту главу, вы понимаете память-позу плоскости данных XRT.
7.1引入
Каждый, кто писал парсер руками, помнит: на третьем уровне рекурсивного спуска уже держишь десяток временных буферов; падение любого уровня освобождает буферы внешних уровней по одному — перед каждым return висит цепочка Free; пропустил один — утечка, освободил лишний — падение. А жизненный цикл этих объектов на деле высоко однороден: все они живут до «конца разбора этого кадра». Платить поблочную цену управления за однородный жизненный цикл — чистое расточительство.
Арена превращает наблюдение в механизм: раз «рождаются вместе, умирают вместе» — пусть «выделяются вместе, возвращаются вместе». Цена управления падает с O(числа выделений) до O(числа областей); очистка пути отказа сжимается из цепочки Free в один End. Добавьте сюда производительность: поблочное выделение проходит через аллокатор (даже пул памяти главы 22 имеет учётные расходы), выделение в арене — одно сложение указателя; на горячих путях плоскости данных с миллионами временных выделений разница носит порядковый характер.
7.2概念
Ментальная модель: три глагола
Нет поблочного освобождения — нет фрагментации свободных блоков; нет по-блочного учёта — стоимость выделения приближается к адресации в массиве. Цена столь же ясна: отдельный объект нельзя вернуть раньше — арена возвращает только по областям. Подходит ли сценарий арене, решает однородность жизненного цикла: разбор кадра, обработка запроса, отрисовка кадра — однородны; кэши, конфигурация, сессии — долгоживущие объекты неоднородны и остаются во владеющем выделении главы 5.
Так складывается проходящая через всю книгу линия разделения труда: владеющее ведает «явно долгоживущим», арена — «обрабатываемым в этом раунде». Вы встретите эту пару в каждом следующем томе: парсер JSON строит временное дерево в арене (глава 31), сетевые запросы собирают ответы в арене (том 7), корутины несут встроенную арену (глава 56) — арена есть память-поза по умолчанию для плоскости данных XRT, а владеющее — исключение, а не правило. Пиша код, сначала спросите «может ли этот блок уйти вместе с областью видимости?» — если да, берите арену, прежде чем думать о чём-либо ещё.
Области видимости: закладки и вложенность
xrtTempBegin ставит закладку (xtempmark) на текущем уровне; последующие выделения ложатся за неё; xrtTempEnd откатывает уровень целиком до закладки. Закладки вкладываются — внутренняя откатывается первой, внешняя позже — естественно совпадая с формой рекурсивного спуска и стека вызовов.
Продвижение: результат живёт дольше области
Классическая головоломка арен: собранный в подобласти результат (скажем, готовая строка журнала) должен выжить, а величины процесса (промежуточные буферы, временные фрагменты) должны вернуться всё. xrtTempEndStr / xrtTempEndDup до возврата диапазона копируют указанное содержимое в родителя — результат выживает, остальное возвращается, обе цели одним шагом. Это лазейка против «арена не умеет выборочно сохранять», и главный герой примеров этой главы.
Арена по умолчанию и явные арены
Два маршрута использования. Арена по умолчанию: у каждого потока/корутины своя (арена корутины создаётся и возвращается вместе с ней — том 6); xrtTemp выделяет прямо из арены текущего контекста, xrtTempClear очищает — подходит для «схватить блок черновой памяти внутри функции». Явные арены (xtemparena): xrtTempInit создаёт, xrtTempUnit уничтожает — встраиваются в объекты запросов, парсеры, циклы кадров, то есть структуры «с собственным жизненным циклом». Конфигурация явной арены — тройка: размер обычного блока, удерживаемый уровень (сколько памяти остаётся на руках после Reset) и общий потолок — выделения сверх потолка падают, не давая убежавшему запросу проесть процесс. Сценарии, где в арене побывали ключи/токены, завершаются серией Secure (xrtTempSecureReset / xrtTempSecureUnit): сначала безопасно стереть байты пользовательской области, затем вернуть — продолжая семантику xrtSecureZero главы 5.
Критерий выбора маршрута — «кто владеет жизненным циклом этой партии временной памяти». Поток, очищающийся целиком раз за раунд, — проще всего арена по умолчанию; объект запроса со своим входом и выходом (возможно, переиспользуемый пулом) получает явную арену — Reset после обработки для удержания уровня, Unit при разрушении объекта для возврата всего. API двух арен идентичны по форме (полное семейство Alloc/Dup/Begin/End), смена маршрута не перестраивает бизнес-код.
7.3示例
Первая программа: минимальный образец областей и продвижения
Сначала 20-строчный минимальный образец, проговаривающий три вещи «закладка — возврат — продвижение» каждая сама по себе:
/* temp_mini.c —— области и продвижение в арене по умолчанию */ #define XRT_MODULE_TEMP_MEMORY #define XRT_IMPLEMENTATION #include <xrt.h> #include <stdio.h> #include <string.h> int main(void) { xtemparena* pArena = xrtTempCurrent(); xtempmark tScope = xrtTempBegin(pArena); char* sTemp = (char*)xrtTemp(16); char* sKeep; memcpy(sTemp, "scoped", 7); /* продвижение: копируем результат на родительский уровень до возврата */ sKeep = xrtTempEndStr(&tScope, (xstrview){ sTemp, 7 }); printf("kept=%s\n", sKeep != NULL ? sKeep : "(null)"); xrtTempClear(); return 0; }
Что только что произошло. После Begin строка sTemp живёт внутри диапазона закладки; EndStr до отката диапазона копирует 7 байт на родительский уровень и возвращает новый указатель — sTemp с этого момента мёртв, sKeep живёт до Clear. Пять коротких строк прогоняют обе ключевые дисциплины арены: величины процесса уходят вместе с областью; результаты выживают продвижением.
Вторая полная программа: области, продвижение и явная арена
Из примера репозитория examples/memory/temp/main.c, за один проход — области и продвижение арены по умолчанию, создание и безопасное завершение явной арены:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> int main(void) { const unsigned char arrData[] = { 1, 2, 3, 4 }; xtemparena tArena; xtemparena* pArena = xrtTempCurrent(); /* 线程默认 arena */ xtempconfig tConfig = { 1024, 512, 2048 }; xtempinfo tInfo; xtempmark tScope; /* 作用域书签 */ ptr pData; char* sOuter; char* sInner; char* sPromoted; if ( pArena == NULL ) { return 1; } /* 父级分配:作用域结束后仍存活。 */ sOuter = (char*)xrtTemp(32); if ( sOuter == NULL ) { return 2; } memcpy(sOuter, "outer", 6); /* 子作用域:Begin 打书签 → 分配 inner → End 整段回收。 */ tScope = xrtTempBegin(pArena); sInner = (char*)xrtTemp(32); if ( sInner == NULL ) { return 3; } memcpy(sInner, "inner", 6); printf("outer=%s inner=%s\n", sOuter, sInner); if ( !xrtTempEnd(&tScope) ) { return 4; } /* 关键验证:inner 的空间已被回收复用,outer 完好无损。 */ printf("outer_after_scope=%s\n", sOuter); */ tScope = xrtTempBegin(pArena); sInner = xrtTempStr(pArena, XRT_STR_LITERAL("promoted")); if ( sInner == NULL ) { return 5; } sPromoted = xrtTempEndStr(&tScope, (xstrview){ sInner, 8 }); if ( sPromoted == NULL ) { return 6; } printf("promoted=%s\n", sPromoted); if ( !xrtTempClear() ) { return 7; } */ memset(&tArena, 0, sizeof(tArena)); if ( !xrtTempInit(&tArena, &tConfig) ) { return 8; } if ( xrtTempAlloc(&tArena, 64) == NULL ) { xrtTempUnit(&tArena); return 9; } pData = xrtTempDup(&tArena, arrData, sizeof(arrData)); if ( (pData == NULL) || (memcmp(pData, arrData, sizeof(arrData)) != 0) ) { xrtTempUnit(&tArena); return 10; } /* 二进制提升:EndDup 与 EndStr 同构,面向字节块。 */ tScope = xrtTempBegin(&tArena); pData = xrtTempEndDup(&tScope, arrData, sizeof(arrData)); if ( (pData == NULL) || (memcmp(pData, arrData, sizeof(arrData)) != 0) ) { xrtTempUnit(&tArena); return 11; } xrtTempGet(&tArena, &tInfo); printf( "blocks=%zu retained=%zu current=%zu peak=%zu\n", tInfo.BlockCount, tInfo.RetainedBytes, tInfo.CurrentBytes, tInfo.PeakBytes ); * */ tScope = xrtTempBegin(&tArena); (void)xrtTempAlloc(&tArena, 32); if ( !xrtTempEnd(&tScope) ) { xrtTempUnit(&tArena); return 12; } if ( !xrtTempSecureReset(&tArena) ) { xrtTempUnit(&tArena); return 13; } if ( !xrtTempReset(&tArena) || !xrtTempTrim(&tArena, 0) ) { return 14; } xrtTempSecureUnit(&tArena); return 0; }
Что только что произошло. (1) xrtTempCurrent берёт арену по умолчанию текущего потока; xrtTemp(32) выделяет на родительском уровне — она не принадлежит никакому диапазону закладки и действительна до Clear. (2) После закладки Begin выделяется inner; печать подтверждает сосуществование двух блоков; End возвращает весь диапазон. (3) Снова печатаем outer: цел — закладка откатывает только память после себя, выделения родительского уровня не тронуты; в этом гарантия безопасности «вложенных областей». (4) Вторая область демонстрирует продвижение: xrtTempStr собирает "promoted", а xrtTempEndStr до возврата копирует его в родителя — третья строка вывода доказывает, что результат выжил, а прочие выделения диапазона (если были) вернулись все. (5) Секция явной арены: создана с тройкой {1024, 512, 2048}, семейство Alloc/Dup/EndDup пройдено, xrtTempGet возвращает статистику — blocks=1 значит, что все выделения легли в один обычный блок, retained=1024 — удержание блока, current=96 и peak=96 — текущий и пиковый уровни. (6) Наконец SecureReset → Reset → Trim(0) → SecureUnit: безопасное стирание, обычный возврат, отдача всех удержанных блоков, разрушение — полная последовательность завершения явной арены.
Деталь, достойная внимания: после End указатель sInner всё ещё «существует» (указатели C не исчезают), но диапазон, куда он смотрит, уже возвращён — дальнейшее использование есть повисший доступ, о нём ловушка 1. Заметьте и то, что Clear появляется лишь в конце секции арены по умолчанию: кто управляет границей области, тот и возвращает — чистить среди функции не нужно, и не дочистить до конца тоже не страшно (арена по умолчанию возвращается целиком при выходе потока), но в долгоживущих циклах остерегайтесь накопления — ловушка 2.
7.4契约
- Кто сбрасывает, тот и отвечает: области ведутся парой
Begin/End;Clearарены по умолчанию вызывает парный владелец «этого раунда обработки»; бизнес-функции внутри не делаютClear. - Закладка ведает только собой:
Endоткатывает до закладки и не трогает более ранние выделения; вложенность откатывается последним-пришёл-первым-ушёл. - Продвижение есть копирование: результаты
EndStr/EndDupпринадлежат родительскому уровню и не связаны с исходным диапазоном; копирование завершается до возврата — окна подвисания нет. - Потолок есть линия обороны: превышение потолка тройки роняет выделение (род
XERR_MEMORYиз главы 4) — как и границы ресурсов, это спецификация бизнеса, записанная кодом. - Чувствительные данные: если в арене побывали ключи, завершайте серией
Secure; обычныйResetне стирает байты пользовательской области.
Типичные ошибки
Симптомы: временный указатель, выданный из функции; вызывающий читает «случайно меняющееся» содержимое или его затирают позднейшие выделения; воспроизведение зависит от последовательности последующих выделений.
Причина: End вернул диапазон; любое следующее выделение арены может переиспользовать эту память — указатель всё ещё смотрит на легальный адрес, но содержимое уже не ваше.
const char* build_label(void) { xtempmark tScope = xrtTempBegin(xrtTempCurrent()); char* s = (char*)xrtTemp(32); fill(s); xrtTempEnd(&tScope); return s; /* диапазон возвращён при выходе: вызывающий получает память, ожидающую переиспользования */ }
const char* build_label(void) { xtempmark tScope = xrtTempBegin(xrtTempCurrent()); char* s = (char*)xrtTemp(32); fill(s); /* продвинуть в родителя: копирование, затем возврат; возвращаемое значение выживает */ return xrtTempEndStr(&tScope, (xstrview){ s, strlen(s) + 1 }); }
Симптомы: память резидентного потока сервиса медленно ползёт; xrtTempGet показывает, что current только растёт.
Причина: проектное ожидание арены по умолчанию — «каждый раунд кто-то делает Clear»; если каждый раунд только выделяет и не чистит, арена распухает до потолка (при явной конфигурации) или растёт бесконечно (при щедром потолке по умолчанию).
while ( running ) { char* sLine = (char*)xrtTemp(4096); /* выделяем каждый раунд, Clear никогда */ process(sLine); }
while ( running ) { xtempmark tScope = xrtTempBegin(xrtTempCurrent()); char* sLine = (char*)xrtTemp(4096); process(sLine); xrtTempEnd(&tScope); /* возврат отрезка каждый раунд: уровень постоянен */ }
Практические задания
Следуя примеру, выдайте три части вывода: два блока сосуществуют в области, родитель выживает после End, продвинутый результат выживает. Три строки вывода, совпадающие с примером дословно, завершают упражнение.
Напишите рекурсивную функцию разбора скобочных выражений (вроде ((a)(b))): каждый уровень рекурсии делает Begin/End, внутри уровня фрагменты хранятся в xrtTemp, а самый внутренний результат продвигается по уровням наверх через EndStr. Подсказка: продвижение случается «перед возвратом этого уровня»; естественная форма рекурсии — и есть вложенные закладки.
Создайте явную арену с тройкой {1024, 512, 2048} и проверьте статистикой главы 6 или xrtTempGet: при выделениях вплотную к потолку дальнейшее выделение возвращает NULL, а в слоте ошибки потока — XERR_MEMORY; после Reset способность выделять возвращается; после Trim(0) удержание retained равно нулю. Критерий приёмки: все три утверждения печатаются ok.
Итоги
| Тема | Краткая справка |
|---|---|
| Ментальная модель | выделить = сдвиг указателя; освободить = ничего; вернуть = откат целого отрезка; цена управления O(областей) |
| Область | Begin ставит закладку, End откатывает; вложенность — последним-пришёл-первым-ушёл |
| Продвижение | EndStr / EndDup: результат копируется в родителя, величины процесса возвращаются |
| Арена по умолчанию | xrtTempCurrent / xrtTemp / xrtTempClear; у корутин своя |
| Явная арена | Init/Unit встраиваются в объекты; тройка (блок/уровень/потолок) |
| Безопасное завершение | SecureReset / SecureUnit стирают перед возвратом |
| Критерий пригодности | однородные жизненные циклы — арена; долгоживущие объекты — владеющее главы 5 |
| Мнемоника разделения | арена ведает этим раундом; владеющее — явно долгоживущим |
📖 Справочники API в этой главе:temp (22 записей)memory (19 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки