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

Глава 5 закрыла «память, удерживаемую подолгу»; эта глава закрывает её двойника: множества короткоживущих объектов. Разбор одного кадра JSON порождает сотни мелких фрагментов; обработка одного HTTP-запроса собирает десятки временных строк — поблочный xrtMalloc/xrtFree не только медленен, хуже: путь отказа должен чистить блок за блоком, и объём кода взрывается. Ответ XRT — арена: отрезок памяти выделяется целиком; «выделение» внутри — лишь сдвиг указателя, «освобождение» не делает ничего, «возврат» отдаёт весь отрезок. Парсеры, протокольные стеки и планировщики корутин всей библиотеки имеют встроенные арены — поняв эту главу, вы понимаете память-позу плоскости данных XRT.

7.1引入

Каждый, кто писал парсер руками, помнит: на третьем уровне рекурсивного спуска уже держишь десяток временных буферов; падение любого уровня освобождает буферы внешних уровней по одному — перед каждым return висит цепочка Free; пропустил один — утечка, освободил лишний — падение. А жизненный цикл этих объектов на деле высоко однороден: все они живут до «конца разбора этого кадра». Платить поблочную цену управления за однородный жизненный цикл — чистое расточительство.

Арена превращает наблюдение в механизм: раз «рождаются вместе, умирают вместе» — пусть «выделяются вместе, возвращаются вместе». Цена управления падает с O(числа выделений) до O(числа областей); очистка пути отказа сжимается из цепочки Free в один End. Добавьте сюда производительность: поблочное выделение проходит через аллокатор (даже пул памяти главы 22 имеет учётные расходы), выделение в арене — одно сложение указателя; на горячих путях плоскости данных с миллионами временных выделений разница носит порядковый характер.

7.2概念

Ментальная модель: три глагола

1Выделитьсдвинуть указатель, вернуть начало диапазона — ноль метаданных, ноль фрагментации
2Освободитьне делать ничего — промежуточное состояние никогда не возвращается поблочно
3Вернутьоткатить весь отрезок до закладки — один раз на область видимости

Нет поблочного освобождения — нет фрагментации свободных блоков; нет по-блочного учёта — стоимость выделения приближается к адресации в массиве. Цена столь же ясна: отдельный объект нельзя вернуть раньше — арена возвращает только по областям. Подходит ли сценарий арене, решает однородность жизненного цикла: разбор кадра, обработка запроса, отрисовка кадра — однородны; кэши, конфигурация, сессии — долгоживущие объекты неоднородны и остаются во владеющем выделении главы 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;
}
$ gcc -O1 -DXRT_MODULE_TEMP_MEMORY -DXRT_IMPLEMENTATION -I single temp_mini.c -lws2_32 -liphlpapi$ ./a.exekept=scoped

Что только что произошло. После Begin строка sTemp живёт внутри диапазона закладки; EndStr до отката диапазона копирует 7 байт на родительский уровень и возвращает новый указатель — sTemp с этого момента мёртв, sKeep живёт до Clear. Пять коротких строк прогоняют обе ключевые дисциплины арены: величины процесса уходят вместе с областью; результаты выживают продвижением.

Вторая полная программа: области, продвижение и явная арена

Из примера репозитория examples/memory/temp/main.c, за один проход — области и продвижение арены по умолчанию, создание и безопасное завершение явной арены:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/memory/temp/main.c -lws2_32 -liphlpapiouter=outer inner=innerouter_after_scope=outerpromoted=promotedblocks=1 retained=1024 current=96 peak=96

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

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

Ловушка 1: хранение выделения из области за пределами области:Ловушка 1: хранение выделения из области за пределами области

Симптомы: временный указатель, выданный из функции; вызывающий читает «случайно меняющееся» содержимое или его затирают позднейшие выделения; воспроизведение зависит от последовательности последующих выделений.

Причина: 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 });
}
Ловушка 2: вечная нечистка арены по умолчанию в долгом цикле:Ловушка 2: вечная нечистка арены по умолчанию в долгом цикле

Симптомы: память резидентного потока сервиса медленно ползёт; 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 записей)

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