skiplist
- title
- skiplist
- type
- toolbox
- summary
- Header-only C macros for a lock-free skip list with optional splay rebalancing of hot keys
- tags
- c, data-structures, lock-free, concurrency
- language
- C
- license
- ISC OR MIT
- created
- 2026-05-19
- updated
- 2026-05-19
Header-only C library implementing a concurrent lock-free skip-list โ specifically a splay-list, a skip-list with optional adaptive rebalancing that promotes hot keys toward the top. The entire implementation lives in preprocessor macros inside include/sl.h, generating type-specific code at compile time the same way C++ templates do, but without function dispatch or void pointers. Written by Gregory Burd; dual-licensed ISC or MIT.
How it works
The core algorithm is Fraser/Harris: insert by CAS at level 0 then opportunistically link upper levels, delete by marking the lowest bit of a forward pointer (logical delete) then unlinking (physical delete), use epoch-based reclamation (EBR) so readers don't observe freed memory. Level 0 is a doubly-linked list, which gives O(1) bidirectional iteration instead of the O(log n) reverse walks plain skip lists need.
Splay rebalancing is opt-in via SKIPLIST_SPLAY_REBALANCE. Based on Aksenov et al. 2020 โ a node with hit ratio u/T settles at height K โ 1 โ logโ(T/u), so a key accessed 50% of the time ends up at depth 1. Search cost goes from O(log n) to O(log(1/p)) for hot keys. The rebalance pass fires only on read-only paths (search, contains, position queries). It never fires on remove, because promoting a soon-to-be-deleted node leaves dangling upper-level references after EBR reclaims it.
The README is careful about what splay does not buy: "This does not make a splay-list as fast as a B+tree" โ the cache-line layout is wrong for that, even with the height optimization. The point is to make hot-key lookups within a skip list faster, not to outrun a different data structure.
API layout
The library is a stack of independent macro generators โ pick the ones you need:
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
Compile-time knobs:
| Flag | Default | Purpose |
|---|---|---|
SKIPLIST_SINGLE_THREADED |
undefined | replace atomics with plain ops; drop <stdatomic.h> |
SKIPLIST_SPLAY_REBALANCE |
undefined | adaptive height adjustment |
SKIPLIST_MAX_HEIGHT |
64 | maximum tower height (โค64) |
SKIPLIST_SPLAY_INTERVAL |
64 | accesses between rebalance passes (power of 2) |
SKIPLIST_EBR_MAX_THREADS |
128 | maximum concurrent EBR-registered threads |
Single-threaded mode is roughly 30% faster than lock-free mode on tight loops because the atomic loads and CAS are gone. The pool allocator on top of that doubles to triples sequential insert throughput.
Indicative performance
x86_64, N=100k, gcc 13 -O2:
| Operation | Throughput | Latency |
|---|---|---|
| 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 |
Other features
MVCC snapshots give point-in-time captures with restore. Binary serialization uses user-defined per-node handlers โ no built-in opinion on what your nodes look like. Validation and DOT visualization make integrity checks and debugging direct.
Tests cover 97% line / 99% function / 58% branch on the implementation surface: 33 unit tests under ASan/LSan/UBSan, 7 concurrent tests under ThreadSanitizer, 6 single-threaded mode tests, Valgrind leak checking, plus an empirical test that reproduces the Aksenov 2020 paper's height-target predictions across different access distributions. Three build systems ship in the repo (plain Make, autoconf, Meson).
Where it fits
The MemTable in an lsm-tree is the classic in-memory sorted structure that wants O(log n) inserts and ordered iteration โ skip lists and red-black trees are the two usual choices. RocksDB and LevelDB both use skip lists there. A lock-free implementation with a doubly-linked level 0 (cheap reverse scans) and optional MVCC snapshots is a natural fit for the MemTable + read-side snapshot pattern those engines already use. The single-threaded mode is interesting in its own right โ many embedded use cases (config stores, DSL runtimes, in-process caches) don't need lock-free but do want ordered iteration and a small footprint.
Repo
codeberg.org/gregburd/skiplist โ ISC OR MIT.