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

Глава ставит рядом два «контринтуитивных, но предельно ходовых» контейнера и завершает кратким наброском их комбинации. После неё ваш ящик инструментов «позиции и идентификаторы» полон: индексы массива, узлы списка, дескрипторы с поколением — каждый на своём месте. Интрузивный двусвязный список xlist встраивает узел внутрь объекта-носителя — подвешивание без выделений, один объект на нескольких цепях, снятие с цепи не уничтожает объект; поддержание порядка LRU за O(1) — его учебниковое применение. slot_map xslotmap решает классическую ловушку «дескриптор-индекс»: после переиспользования слота старый дескриптор ошибочно попадает в новый объект — счётчик поколений автоматически инвалидирует устаревший дескриптор. Оба контейнера отвечают на один вопрос: когда «позицию» объекта меняет система, как внешней ссылке остаться правильной — ответ списка «узел — часть объекта», ответ slot_map «дескриптор несёт номер версии».

17.1引入

Два места крушения. Первое: кэш LRU собран как «массив + пересортировка на каждое попадание» — кэш подрос, и сортировка стала узким местом пропускной способности; переход на список вскрыл, что каждое перемещение узла выделяет новый и освобождает старый — аллокатор стал занятее сортировки. Интрузивный список решает одним движением: узел — поле объекта, перемещение лишь перецепляет два указателя, O(1) и ноль выделений. Второе: таблица соединений раздаёт индексы массива бизнес-слой как дескрипторы; соединение 1 закрылось, новое соединение заняло слот 1, бизнес-слой работает со старым дескриптором и попадает в чужое соединение — перекрёст данных и превышение прав растут именно отсюда. Дескрипторы с поколением slot_map автоматически инвалидируют старый дескриптор после переиспользования слота; ошибочное попадание отвергается на уровне контейнера.

Техническая суть обоих мест одна: идентификатор обязан нести достаточно информации для борьбы с «переиспользованием позиции». Список уходит от позиции «встроенной идентичностью», slot_map версионирует позицию «индекс + поколение».

Стоит проговорить: ни один из контейнеров не управляет памятью объектов за вас — интрузивный список ничего не выделяет и не освобождает, slot_map хранит лишь ptr. Они поставщики «структуры и порядка», а не «владения»; владение всегда принадлежит режиму главы 5. Это разделение обязанностей позволяет без опаски ставить их поверх любой стратегии памяти (куча, пул, арена) — и означает, что дисциплина пути уничтожения целиком на вызывающем; ловушка 1 главы — место исполнения этой дисциплины.

17.2概念

Интрузивность: узел живёт в объекте

1Объект-носительбизнесовая структура встраивает одно поле xlistnode
2ПодвешиваниеPushFront только перецепляет указатели - ноль выделений, ноль копий
3Обратный поискXRT_CONTAINER_OF вычитает смещение поля из адреса узла и возвращает носителя
4Несколько цепейвстроите ещё одно поле-узел - объект висит на второй цепи одновременно
5СнятиеClear только расцепляет; жизненный цикл носителя целиком принадлежит вызывающему

В отличие от традиционного списка, где «контейнер владеет узлами и хранит указатели на элементы», интрузивный стиль переворачивает отношение владения: контейнер не владеет объектами; объекты несут собственные крючки. Цена — структуре надо зарезервировать поле «на случай, если когда-нибудь повисну на цепи»; выигрыш — бесплатное подвешивание, принадлежность одного объекта нескольким группам сразу и снятие, не затрагивающее жизненный цикл объекта.

LRU: MoveFront и есть повышение при попадании

Голова — самый свежий, хвост — самый давний. При попадании xrtListMoveFront переносит узел в голову за O(1) — меняются только два двунаправленных указателя, независимо от длины цепи. Вытеснение «самого давнего» — взятие хвоста. В паре с map главы 20, ведающим «ключ → узел», полный LRU — эти два контейнера плюс один MoveFront.

Дескрипторы с поколением: индекс + версия

Сперва растолкуем «зачем версия»: 32-битный индекс сам по себе — совершенный идентификатор позиции — одношаговая локация, доступ O(1); его единственный изъян — он не несёт истории: после переиспользования слота старый и новый индексы неразличимы. Добавка 32-битного поколения закрывает пазл: позиция отвечает «где», поколение — «который по счёту». Дескриптор xslotmap (xslot) — упаковка в 64 бита: поколение << 32 | индекс. Каждое переиспользование слота инкрементирует поколение. Contains проверяет индекс и поколение вместе — старый дескриптор (поколение отстаёт) признаётся недействительным, даже если его индекс указывает на слот, где сейчас живёт новый объект. Remove валидирует так же; несовпадение поколения отказывает удалению. Это стандартный фундамент «таблиц соединений, пулов ресурсов, управления сущностями ECS»: внешний мир держит дескрипторы, контейнер гарантирует семантическую точность дескриптора.

Когда списком не пользуются

O(1) вставки-удаления списка имеет предпосылку: вы уже держите тот узел. Поиск по индексу или по значению — всё ещё O(n) — сам акт «найти элемент в списке» съедает преимущество O(1) целиком. Три контрправила: нужна случайная адресация → массив (глава 14); нужен поиск по ключу → map (глава 18); нужен упорядоченный обход → сортированный массив или AVL (глава 20). Незаменимые сценарии списка сужаются до: поддержания порядка (LRU, очереди планирования, свободные списки) и «объект известен — нужно лишь сменить групповую принадлежность». Правило-пароль: сначала спросите «как я получаю узел», потом «часты ли вставки-удаления» — где у первого нет ответа O(1), список вам не поможет.

Обход и портрет сложности

Обход списка идёт от головы к хвосту узел за узлом (First/Next); вставка и удаление O(1); но нет случайного доступа по индексу — поиск узла по индексу O(n). slot_map наоборот: дескриптор и есть индекс, доступ O(1), но обход должен перепрыгивать пустые слоты. Сила одного — слабость другого, и совместное применение (slot_map хранит объекты + список держит порядок) — классическая архитектура менеджера ресурсов; таблица соединений главы 67 — та же комбинация в масштабе: локация по дескриптору O(1), поддержание порядка событий O(1), ясная принадлежность жизненного цикла.

17.3示例

Первая полная программа: поддержание порядка LRU за O(1)

Из репозиторного образца examples/containers/list/main.c:

examples/containers/list/main.c源码 ↗






















 */

#include <stdio.h>

#include <xrt.h>



/* 缓存条目:Recent 内嵌节点让它可以直接挂进 LRU 链。 */
typedef struct cacheitem {
	int Key;
	int Value;
	xlistnode Recent;
} cacheitem;



/* 打印从最近到最久未使用的缓存条目。 */
static void printCache(const xlist* pCache)
{
	for ( xlistnode* pNode = xrtListFirst(pCache);
		  pNode != NULL;
		  pNode = xrtListNext(pNode) ) {
		/* 由节点地址减去字段偏移,反查宿主 cacheitem 地址。 */
		cacheitem* pItem = XRT_CONTAINER_OF(pNode, cacheitem, Recent);

		printf("%d=%d ", pItem->Key, pItem->Value);
	}
	printf("\n");
}



int main(void)
{
	xlist Cache = XRT_LIST_INIT;    /* 空链表,无分配 */
	cacheitem Items[] = {
		{ 1, 100, XRT_LIST_NODE_INIT },
		{ 2, 200, XRT_LIST_NODE_INIT },
		{ 3, 300, XRT_LIST_NODE_INIT }
	};

	/* 依次压到链头:最终顺序 3(最新) 2 1(最旧)。 */
	for ( size_t i = 0; i < 3u; i++ ) {
		if ( !xrtListPushFront(&Cache, &Items[i].Recent) ) {
			return 1;
		}
	}
	printCache(&Cache);

	


	 */
	if ( !xrtListMoveFront(&Cache, &Items[0].Recent) ) {
		return 2;
	}
	printCache(&Cache);

	/* 摘除全部节点;Items 是栈数组,无需也不应释放。 */
	if ( !xrtListClear(&Cache) ) {
		return 3;
	}
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/containers/list/main.c -lws2_32 -liphlpapi3=300 2=200 1=1001=100 3=300 2=200

Что только что произошло. (1) Статическая инициализация XRT_LIST_INIT и XRT_LIST_NODE_INIT — список и узел стартуют с нулевой стоимостью, без вызовов инициализации времени исполнения. (2) Три записи последовательно PushFront: последний лежит в голове, вывод 3 2 1. (3) После обращения к записи 1 — MoveFront: вывод становится 1 3 2 — запись 1 перешла из старейших в новейшие, операция тронула только указатели. (4) XRT_CONTAINER_OF(pNode, cacheitem, Recent) — обратный поиск носителя из адреса узла; сопутствующая идиома интрузивности, смещение поля вычислено на этапе компиляции. (5) Clear снимает все узлы; Items — стековый массив, освобождать не нужно и не следует — снятие с цепи не уничтожает объекты.

Вторая полная программа: поколения против ложных попаданий

Из examples/containers/slot_map/main.c:

examples/containers/slot_map/main.c源码 ↗




















 */

#include <stdio.h>
#include <xrt.h>



/* 连接对象仅用于演示稳定代际句柄。 */
typedef struct connection {
	int ID;
} connection;



int main(void)
{
	xslotmap tConnections;
	connection tFirst = { 100 };
	connection tSecond = { 200 };
	connection tReplacement = { 300 };
	xslot First;
	xslot Second;
	xslot Replacement;

	if ( !xrtSlotMapInit(&tConnections) ) {
		return 1;
	}

	/* 插入两个连接:得到两个不同下标的句柄。 */
	First = xrtSlotMapInsert(&tConnections, &tFirst);
	Second = xrtSlotMapInsert(&tConnections, &tSecond);
	if ( (First == XRT_SLOT_INVALID) || (Second == XRT_SLOT_INVALID) ) {
		xrtSlotMapUnit(&tConnections);
		return 2;
	}

	/* 删除第一个连接:末参可回调被摘除的指针,这里传 NULL 不需要。 */
	if ( !xrtSlotMapRemove(&tConnections, First, NULL) ) {
		xrtSlotMapUnit(&tConnections);
		return 3;
	}

	




	 */
	Replacement = xrtSlotMapInsert(&tConnections, &tReplacement);
	if ( Replacement == XRT_SLOT_INVALID ) {
		xrtSlotMapUnit(&tConnections);
		return 4;
	}

	




	 */
	printf(
		"old=%llu replacement=%llu same-index=%s stale-valid=%s\n",
		(unsigned long long)First,
		(unsigned long long)Replacement,
		xrtSlotIndex(First) == xrtSlotIndex(Replacement) ? "yes" : "no",
		xrtSlotMapContains(&tConnections, First) ? "yes" : "no"
	);
	xrtSlotMapUnit(&tConnections);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/containers/slot_map/main.c -lws2_32 -liphlpapiold=4294967297 replacement=8589934593 same-index=yes stale-valid=no

Что только что произошло. (1) Два вставленных соединения дают два дескриптора, First = 0x1_00000001 (поколение 1, индекс 1). (2) После удаления First и вставки замены свободный слот 1 переиспользуется, новый дескриптор 0x2_00000001: индекс тот же, поколение уже выросло с 1 до 2 (same-index=yes). (3) Ключевое утверждение в конце: Contains старого дескриптора First возвращает no — он ни при каких условиях не укажет на замену. Сравните десятичные значения двух дескрипторов в выводе (4294967297 и 8589934593): разница — ровно 2 в 32-й степени, поколение перенесло разряд. Перепишите программу с «голыми индексами» и перезапустите — stale-valid станет yes: собственными глазами увидеть место крушения запоминается лучше, чем десять раз прочесть документацию. Перепись меняет лишь дескриптор на голый индекс и Contains на проверку границ — меньше десяти строк, и безопасный контейнер деградировал в место аварии; этот контрольный опыт — лучший материал для разбора на ревью «почему дескриптор нельзя разбирать».

Набросок комбинированной архитектуры

Индивидуальные слабости двух контейнеров (список не находит, slot_map без порядка) взаимно гасятся в комбинации — стоит приколотить типовую форму наброском: соединение прибыло → вставка в slot_map даёт дескриптор, одновременно узел PushFront в цепь активных; событие сработало → дескриптор локализует объект за O(1), после обработки MoveFront поднимает время; соединение закрылось → сперва снятие с цепи, затем Remove, дескриптор инвалидируется вместе с ним (поколение уже записано); внешняя сторона работает со старым дескриптором → контейнер отказывает, перекрёст невозможен. Четыре шага покрывают будни менеджера ресурсов; таблица соединений главы 67 масштабирует это в конкурентный движок, но скелет — строки в точности эти, ни больше ни меньше.

17.4契约

  • Интрузивное владение: список никогда не владеет объектами; Clear/снятие не освобождает носителей; перед уничтожением объект обязан быть снят с цепи.
  • Статическая инициализация: XRT_LIST_INIT / XRT_LIST_NODE_INIT без инициализации времени исполнения; есть и парные версии времени исполнения.
  • Семантика поколений: переиспользование слота инкрементирует поколение; валидация дескриптора требует двойного совпадения «индекс + поколение»; XRT_SLOT_INVALID — недействительный сигнальное значение; разобрать дескриптор, оставив только индекс, — значит отказаться от защиты.
  • Сложность доступа: список по индексу O(n), вставка/удаление O(1); slot_map по дескриптору O(1); комбинированная архитектура берёт лучшее от каждого.
  • Потоки: ни тот ни другой не потокобезопасные контейнеры — межпотоковый доступ требует внешней синхронизации (правильные примитивы — том 6).

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

Ловушка 1: уничтожение объекта, ещё висящего на цепи:Ловушка 1: уничтожение объекта, ещё висящего на цепи

Симптом: обход списка касается освобождённой памяти; либо снятие пишет в освобождённые поля узла — точка падения далеко от места аварии.

Причина: цена интрузивности — «жизненный цикл объекта обязан покрывать период подвешенности»: до уничтожения носителя его встроенный узел всё ещё на цепи, и указатели цепи целятся в память, которая вот-вот исчезнет.

错误写法
cacheitem* pItem = find_expired(&Cache);
xrtFree(pItem);                      /* узел всё ещё на Cache - цепь повисла */
正确写法
cacheitem* pItem = find_expired(&Cache);
xrtListRemove(&Cache, &pItem->Recent);   /* сперва снять с цепи */
xrtFree(pItem);                           /* потом уничтожить */
Ловушка 2: разбор дескриптора slot_map и использование только индекса:Ловушка 2: разбор дескриптора slot_map и использование только индекса

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

Причина: xslot — упакованное значение поколения и индекса; битовые операции, извлекающие индекс и собирающие его обратно, теряют информацию о поколении.

错误写法
size_t iIndex = First & 0xFFFFFFFFu;         /* оставить только 32-битный индекс */
connection* pConn = (connection*)xrtSlotMapGet(&tMap, iIndex);  /* индекс в роли дескриптора */
正确写法
connection* pConn = (connection*)xrtSlotMapGet(&tMap, First);   /* дескриптор передаётся как есть */
if ( pConn == NULL ) {
	/* поколение не совпало: дескриптор устарел - трактуем как устаревшую ссылку */
}

Практические задания

Базовое: воспроизведение порядка цепи

Воспроизведите две строки вывода главного образца (сперва вывод на бумаге, потом прогон и сверка); добавьте шаг «вытеснение хвоста» (изъятие старейшей записи со снятием с цепи) и печать порядка цепи после вытеснения.

Продвинутое: полный LRU

Храните «ключ → cacheitem» в slot_map или map, порядок свежести — списком; реализуйте get (попадание — MoveFront, промах — пустой возврат) и put (превышение ёмкости — вытеснение хвоста). Подсказка: ёмкость 3, обращения 1/2/3/1/4 — вытеснена должна быть 2; сперва ручная выкладка, затем проверка кодом.

Вызов: прототип таблицы соединений (комплексный экзамен главы)

Таблица соединений на slot_map: open вставляет и возвращает дескриптор, close удаляет по дескриптору (несовпадение поколения — неудача), send берёт объект по дескриптору. Сымитируйте «старый дескриптор прибывает после того, как новое соединение заняло слот», и проверьте, что send отказывает, а не перекрёстно работает. Критерии приёмки: в сценарии переиспользования все операции со старым дескриптором терпят неудачу с различимыми ошибками; нормальный цикл открыть-закрыть проходит целиком; ноль утечек (проверка статистикой главы 6).

Итоги

ТемаКратко
Интрузивностьузел встроен в носителя; подвешивание без выделений; снятие не уничтожает объект
Обратный поискXRT_CONTAINER_OF(节点, 类型, 字段) возвращает носителя
LRUголова — новейший; MoveFront поднимает за O(1); вытеснение берёт хвост
Дескриптор с поколением代际<<32 / 下标; переиспользование слота поднимает поколение; старые дескрипторы инвалидируются сами
Сигнальное значениеXRT_SLOT_INVALID; Contains/Get двойная валидация
Комбинированная архитектураslot_map локализует O(1) + список упорядочивает O(1) — стандарт менеджера ресурсов (таблица соединений главы 67)
Потокине потокобезопасны; межпотоковое — внешняя синхронизация
Формула обратного поискаXRT_CONTAINER_OF считает смещение поля на этапе компиляции, из узла в носителя без накладных; сколько полей-узлов — столько групп принадлежности

📖 Справочники API в этой главе:list (32 записей)slot_map (19 записей)

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