skiplist
- title
- skiplist
- type
- toolbox
- summary
- Header-only макросы на C для lock-free skip list с опциональной splay-перебалансировкой горячих ключей
- tags
- c, data-structures, lock-free, concurrency
- language
- C
- license
- ISC OR MIT
- created
- 2026-05-19
- updated
- 2026-05-19
- lang
- ru
- translation_of
- skiplist
- source_updated
- 2026-05-19
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Header-only библиотека на C, реализующая параллельный lock-free skip-list - а именно splay-list, skip-list с опциональной адаптивной перебалансировкой, продвигающей горячие ключи ближе к вершине. Вся реализация построена на макросах препроцессора в include/sl.h, генерирующих типобезопасный код во время компиляции аналогично шаблонам C++, но без диспетчеризации функций и указателей void. Автор - Gregory Burd; двойная лицензия ISC или MIT.
Как это устроено
Основной алгоритм - Fraser/Harris: вставка через CAS на уровне 0 с последующим оптимистичным связыванием верхних уровней, удаление через пометку младшего бита прямого указателя (логическое удаление) и последующее отвязывание (физическое удаление), а также epoch-based reclamation (EBR), чтобы читатели не натыкались на освобождённую память. Уровень 0 представляет собой двусвязный список, что даёт двунаправленный обход за O(1) вместо обхода в обратном направлении за O(log n), требуемого обычными skip list'ами.
Splay-перебалансировка включается через SKIPLIST_SPLAY_REBALANCE. Она основана на работе Aksenov et al. 2020: узел с частотой обращений u/T стабилизируется на высоте K − 1 − log₂(T/u), поэтому ключ, к которому обращаются в 50% случаев, оказывается на глубине 1. Стоимость поиска для горячих ключей снижается с O(log n) до O(log(1/p)). Проход перебалансировки запускается только на путях только для чтения (поиск, проверка наличия, запросы позиции). Он никогда не вызывается при удалении, поскольку продвижение узла, готового к удалению, приведёт к висячим ссылкам на верхних уровнях после того, как EBR освободит память.
В README чётко оговорено, чего splay не даёт: "Это не делает splay-list таким же быстрым, как B+дерево" - раскладка по cache-line для этого не подходит даже с оптимизацией по высоте. Суть в том, чтобы ускорить выборку горячих ключей внутри самого skip list'а, а не обогнать другую структуру данных.
Состав API
Библиотека представляет собой набор независимых макрогенераторов - можно выбрать только нужные:
SKIPLIST_ENTRY(typename) // embed in your node struct
SKIPLIST_DECL(...) // core: init, insert, remove, search
SKIPLIST_DECL_ACCESS(...) // high-level key/value interface
SKIPLIST_DECL_SNAPSHOTS(...) // MVCC point-in-time snapshots
SKIPLIST_DECL_EBR(...) // epoch-based reclamation
SKIPLIST_DECL_POOL(...) // fixed-capacity cache-line-aligned pool allocator
SKIPLIST_DECL_ARCHIVE(...) // binary serialization
SKIPLIST_DECL_VALIDATE(...) // runtime integrity checks
SKIPLIST_DECL_DOT(...) // GraphViz visualization
Параметры времени компиляции:
| Флаг | По умолчанию | Назначение |
|---|---|---|
SKIPLIST_SINGLE_THREADED |
undefined | замена атомиков обычными операциями; отказ от <stdatomic.h> |
SKIPLIST_SPLAY_REBALANCE |
undefined | адаптивная корректировка высоты |
SKIPLIST_MAX_HEIGHT |
64 | максимальная высота башни (≤64) |
SKIPLIST_SPLAY_INTERVAL |
64 | число обращений между проходами ребалансировки (степень 2) |
SKIPLIST_EBR_MAX_THREADS |
128 | максимум параллельных потоков, зарегистрированных в EBR |
Однопоточный режим примерно на 30% быстрее lock-free режима в плотных циклах, так как в нём нет атомарных чтений и CAS. Пул-аллокатор поверх этого увеличивает пропускную способность последовательной вставки в два-три раза.
Показатели производительности
x86_64, N=100k, gcc 13 -O2:
| Операция | Пропускная способность | Задержка |
|---|---|---|
| Sequential insert | 611,852 ops/s | 1,634 ns |
| Random insert | 168,695 ops/s | 5,928 ns |
| Sequential search (hit) | 1,722,553 ops/s | 580 ns |
| Forward iteration | 22,300,984 ops/s | 45 ns |
| Pool insert (sequential) | 1,526,033 ops/s | 655 ns |
| Concurrent insert (8 threads) | 650,791 ops/s | 1,537 ns |
Другие возможности
Снимки MVCC обеспечивают фиксацию состояния на определенный момент времени с возможностью восстановления. Двоичная сериализация использует пользовательские обработчики для каждого узла - библиотека не навязывает структуру ваших узлов. Проверка целостности и визуализация DOT упрощают отладку и валидацию.
Тесты покрывают 97% строк / 99% функций / 58% ветвлений реализации: 33 модульных теста под ASan/LSan/UBSan, 7 параллельных тестов под ThreadSanitizer, 6 тестов однопоточного режима, проверка утечек через Valgrind, а также эмпирический тест, воспроизводящий предсказания целевой высоты из статьи Aksenov 2020 для различных распределений доступа. В репозитории идут три системы сборки (обычный Make, autoconf, Meson).
Где это применимо
MemTable в lsm-tree - классическая сортированная структура в памяти, требующая вставки за O(log n) и упорядоченного обхода; стандартными вариантами здесь выступают skip list'ы и красно-чёрные деревья. И RocksDB, и LevelDB используют skip list'ы для MemTable. Lock-free реализация с двусвязным уровнем 0 (дешёвое сканирование в обратном направлении) и опциональными MVCC-снимками отлично ложится на шаблон MemTable + read-side snapshots, который уже применяется в этих движках. Однопоточный режим интересен сам по себе: во многих встраиваемых сценариях (хранилища конфигурации, среды выполнения DSL, внутрипроцессные кэши) lock-free не нужен, но требуются упорядоченный обход и компактность.
Репозиторий
codeberg.org/gregburd/skiplist - ISC OR MIT.