Том IV. Производительность и оптимизация
Уровень: для опытных разработчиков. Цель тома: вы умеете настраивать libmdbx под конкретный сценарий, понимаете источники write amplification и узкие места, умеете измерять и интерпретировать метрики.
Глава 21. WAF — write amplification factor
21.1. Что такое WAF и почему он важен
WAF (Write Amplification Factor) — отношение реально записанных на диск байт к объёму данных, принятых от приложения. WAF = 100 значит: на каждый записанный вами байт диск получает 100 байт. Высокий WAF означает износ SSD, меньшее время жизни диска и худшую пропускную способность.
21.2. Источники амплификации
- Страничная гранулярность. CoW не модифицирует старую страницу, а создаёт новую. Изменение одного байта в листе = запись целой страницы (например, 4 КБ).
- Путь к корню. Новая версия листа меняет указатель у родителя → родитель тоже становится
новым → и так до корня. Точечное обновление =
(высота+1) × pagesizeбайт записи. - GC-записи и мета. Освобождённые страницы описываются в GC-дереве; двухфазная мета — ещё 1–3 страницы на коммит.
- Спилл. Спиллутая страница, затем снова изменённая, записывается повторно.
- Merge/rebalance. Слияния полупустых страниц могут переписывать соседние.
21.3. Батчинг — главный рычаг
В пределах одной транзакции страница попадает в грязный список один раз. N операций на M
уникальных страницах записывают M × pagesize байт (+GC+мета). Поэтому:
- одна операция на транзакцию: WAF = высота+1 ≈ 4–5 (и выше);
- 10 000 операций в одной транзакции: WAF приближается к «1 страница на множество изменений».
Вывод: батчинг (100–10 000 операций на коммит) — самый мощный рычаг снижения WAF.
21.4. Влияние высоты дерева
Высота B+tree ~ log_B(N), где B — среднее число потомков в узле (сотни). Увеличение объёма данных в 2^k раз добавляет ~k уровней. Для point-update удельный WAF растёт логарифмически с объёмом — медленно.
21.5. Спилл как компромисс
Когда грязный список переполняет dp_limit (≈1/42 ОЗУ), часть страниц выгружается заранее.
Если такая страница снова изменяется — она пишется повторно (лишний WAF). Это компромисс
«память vs WAF».
21.6. prefer_waf_insteadof_balance и merge_threshold
Опция prefer_waf_insteadof_balance намеренно жертвует равномерностью заполнения страниц ради
меньшего числа записываемых страниц (использовать уже изменённую страницу-соседа); включена по
умолчанию. Порог слияния — MDBX_opt_merge_threshold (16.16 %; дефолт 33%, допустимый диапазон
[12.5%..50%]). Оба — прямые рычаги WAF.
21.7. Расчёт WAF для типичных сценариев
Для point-update в дереве высоты h на странице 4 КБ: запись ≈ (h+1) × 4096 байт на операцию.
При h=4 и значении 32 байта: 20 КБ / 32 Б ≈ WAF ≈ 640 (одиночные операции!). При батчинге
10 000 записей на те же 5 000 уникальных страниц: 5 000 × 4096 / (10 000 × 32) ≈ WAF ≈ 64.
С дальнейшим ростом батча WAF продолжает падать.
Внимание: числа без контекста бесполезны — всегда указывайте режим синхронизации, размер страницы, высоту дерева и размер транзакции.
Фрагмент из examples/c++/28-waf-batching.c++ — замер
записанных страниц (newly+cow из mi_pgop_stat, те же счётчики, что у mdbx_stat -p) для разных
размеров батча записи:
constexpr int total = 1000;
const auto initial = env.get_info().mi_pgop_stat;
for (int off = 0; off < total; off += (int)batch_size) {
auto txn = env.start_write();
auto table = txn.open_map(nullptr);
for (int i = off; i < off + (int)batch_size && i < total; ++i)
txn.insert(table, mdbx::slice("k" + std::to_string(i)), mdbx::slice("value"));
txn.commit();
}
const auto final = env.get_info().mi_pgop_stat;
const uint64_t pages = (final.newly - initial.newly) + (final.cow - initial.cow);
std::cout << "batch=" << batch_size << ": pages=" << pages << " ops=" << total << " pages/op=" << (double)pages / total
<< "\n";
Полный код: 28-waf-batching.c++.
Примеры к главе:
examples/c++/28-waf-batching.c++.
21.8. Резюме главы 21
- WAF = записано на диск / принято от приложения.
- Источники: гранулярность страницы, путь к корню, GC/мета, спилл, merge.
- Батчинг — главный рычаг: страница пишется раз за транзакцию.
- Рост с объёмом — логарифмический; большие значения — линейный вклад.
21.9. Упражнения
- Посчитайте WAF для point-update при высоте 3 и странице 4 КБ.
- Как повлияет батч из 1 000 обновлений на те же 500 страниц?
21.10. Чек-лист главы 21
- [ ] Могу посчитать запись для point-update как
(высота+1) × pagesizeбайт и получить из неё WAF. - [ ] Называю все источники амплификации: гранулярность страницы, путь к корню, GC/мета, спилл, merge/rebalance.
- [ ] Объясняю, почему страница попадает в грязный список один раз за транзакцию и почему батчинг — главный рычаг снижения WAF.
- [ ] Знаю, что удельный WAF растёт с объёмом логарифмически, и могу объяснить почему (высота ~ log_B(N)).
- [ ] Понимаю компромисс спилла:
dp_limit≈ 1/42 ОЗУ, повторная запись спиллнутой страницы. - [ ] Знаю эффект
prefer_waf_insteadof_balanceиMDBX_opt_merge_threshold. - [ ] Привожу числа WAF только с контекстом: режим синхронизации, размер страницы, высота дерева, размер транзакции.
Глава 22. Выбор конфигурации под сценарий
22.1. Сценарий 1: Высокая пропускная способность записи (Ethereum-подобный)
- Режим:
MDBX_SAFE_NOSYNC+ авто-sync (syncbytes/syncperiod), илиNOMETASYNC. - Геометрия:
upperс запасом;growth_stepкрупный (реже fsync). - Страница: 4–8 КБ.
- Батчинг: десятки–тысячи операций на коммит.
- Ожидаемые числа: сотни тысяч–миллионы вставок/с (зависит от железа и размера ключей).
22.2. Сценарий 2: Чтение-heavy с редкой записью (кэш, индекс)
- Режим: по умолчанию (
DURABLE); запись редкая, надёжность важна. - Страница: по размеру значений; короткие ключи.
maxreaders— по числу потоков-читателей.- Чтение линейно масштабируется по ядрам (wait-free).
22.3. Сценарий 3: Встраиваемое мобильное приложение (Isar-подобный)
- Маленькая база; страница 4 КБ.
MDBX_EXCLUSIVEпри необходимости монопольного доступа.- Аккуратные read-only транзакции; парковка для долгих чтений.
- Низкий WAF (малый churn) — продлевает жизнь flash.
22.4. Сценарий 4: Аналитика, обход всей БД
- Чтение full-scan; важно: страница 64 КБ для последовательного чтения? (Нет — читается всё; важна локальность и отсутствие долгих писателей.)
- Курсоры +
get_batch; оценка диапазонов черезestimate_*.
22.5. Сценарий 5: Многопроцессное чтение (веб-сервер + воркеры)
- Несколько процессов открывают одну базу read-only; один — write.
maxreadersучитывает все процессы; LCK-файл общий.- Аккуратно с
boot_idв контейнерах; page-cache когерентность (#269).
22.6. Сценарий 6: Низкая задержка, маленькие транзакции
- Каждая транзакция — одна операция (минимальная задержка).
- Режим:
NOMETASYNC/SAFE_NOSYNC, чтобы не платить fsync на каждую. - На Windows помнить про LockFileEx (мелкие транзакции дороги).
- Ориентир накладных: ~0.5 мкс на транзакцию сверх I/O.
Фрагмент из examples/c++/29-scenario-configs.c++ —
типовые конфигурации как функции, возвращающие operate_parameters: флаги и опции под каждый
сценарий главы:
// Запись-heavy: ленивая синхронизация, достаточно именованных таблиц.
params write_heavy() { return params().lazy_weak_tail().set_max_maps(64); }
// Чтение-heavy: максимальная долговечность, много читательских слотов.
params read_heavy() { return params().robust_synchronous().set_max_readers(1024).set_max_maps(64); }
// Мобильное устройство: малый размер файла, бережное отношение к флеш-памяти.
params mobile() { return params().lazy_weak_tail().set_max_maps(64); }
// Аналитика: пакетная запись, не мелочиться с балансировкой.
params analytics() { return params().lazy_weak_tail().set_max_maps(64); }
// Многопроцессное чтение: много читательских слотов, стабильная геометрия.
params multi_process_read() { return params().set_max_readers(4096).set_max_maps(64); }
// Низкая задержка: NOMETASYNC (данные на диске, метаданные могут отставать).
params low_latency() { return params().half_synchronous_weak_last().set_max_maps(64); }
Полный код: 29-scenario-configs.c++.
Примеры к главе:
examples/c++/29-scenario-configs.c++.
22.7. Резюме главы 22
- Режим синхронизации выбирается по требованиям к durability.
- Геометрия задаётся с запасом;
upperне занижать. - Батчинг масштабируется под сценарий (latency vs throughput).
- Платформенные особенности (Windows LockFileEx, LXC boot_id) влияют.
22.8. Упражнения
- Подберите конфигурацию для сценария «аналитика» и обоснуйте выбор страницы.
- Что изменится для «низкой задержки» на Windows по сравнению с Linux?
22.9. Чек-лист главы 22
- [ ] Для каждого из шести сценариев могу обосновать выбор режима синхронизации (от
DURABLEдоSAFE_NOSYNC/NOMETASYNC). - [ ] Знаю, что геометрия задаётся с запасом и почему
upperне занижают. - [ ] Умею подобрать размер батча под баланс latency/throughput конкретного сценария.
- [ ] Понимаю, когда важен размер
maxreaders(чтение-heavy, многопроцессное чтение). - [ ] Помню платформенные ловушки: LockFileEx на Windows,
boot_idв контейнерах, когерентность page cache (#269). - [ ] Могу объяснить, почему для full-scan переход на большую страницу не панацея.
Глава 23. Микрооптимизации
23.1. SIMD-поиск свободных страниц
Поиск плотных последовательностей в GC использует векторные ядра: SSE2/AVX2/AVX512/NEON
(ускорение ×4/×8/×16; выбор на этапе выполнения). Это внутренняя оптимизация — вы лишь влияете на
неё через rp_augment_limit/gc_time_limit.
23.2. Branchless бинарный поиск (CMOV)
Бинарный поиск по разделителям branch-страниц использует безусловные переходы (CMOV) — нет ветвлений, предсказуемость. Выигрыш заметен в хот-путях поиска.
23.3. Radix-sort и сортировочные сети
Используются во внутренних процедурах (сортировка списков страниц, слияние PNL).
23.4. C11-атомики для слабых моделей памяти
На ARM/AArch64/PPC/MIPS/RISC-V применяются аккуратные атомики (acquire/release, CAS, safe64). Корректность гарантируется сборкой (проверка атомарности).
23.5. Минимизация системных вызовов
Хот-пути избегают syscall'ов; батчинг I/O через bulk iovec; mdbx_copy использует массовую
запись.
23.6. Prefault-запись и mincore
MDBX_opt_prefault_write_enable — упреждающая запись страниц для устранения page-fault'ов и
чтений с диска при первом обращении в MDBX_WRITEMAP. Резидентность отслеживается через
mincore/madvise. Выигрыш — когда БД > ОЗУ и страницы часто вытесняются.
23.7. Auto-appending split и слияние с грязным соседом
- Auto-appending split — эффективное добавление ключей в конец (append-сценарии).
- Merge с уже грязным соседом — меньше CoW-копий (связано с
prefer_waf_insteadof_balance).
Фрагмент из examples/c++/30-append-prefault.c++ —
пакетная загрузка отсортированных ключей: prefault_write_enable + txn.append() (MDBX_APPEND):
static uint64_t load_append(const std::string &path, int count) {
mdbx::env::remove(path);
auto env = example::env_open(path);
env.set_extra_option(mdbx::env::extra_runtime_option::prefault_write_enable, 1);
const auto t0 = std::chrono::steady_clock::now();
auto txn = env.start_write();
auto table = txn.open_map(nullptr);
for (int i = 0; i < count; ++i)
txn.append(table, mdbx::slice(key_of(i)), mdbx::slice("value")); // MDBX_APPEND
txn.commit();
const auto t1 = std::chrono::steady_clock::now();
return uint64_t(std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count());
}
Полный код: 30-append-prefault.c++.
23.8. Когда эти оптимизации важны, а когда — нет
SIMD/CMOV дают проценты-десятки процентов на конкретных путях. Для большинства приложений решают батчинг и режим синхронизации, а не микрооптимизации. Не оптимизируйте вслепую — измеряйте.
Примеры к главе:
examples/c++/30-append-prefault.c++.
23.9. Резюме главы 23
- Внутренние оптимизации (SIMD, CMOV, radix, атомики) уже встроены.
- Ваши рычаги: prefault, auto-append, merge-политика.
- Микрооптимизации вторичны к батчингу и режиму синхронизации.
23.10. Упражнения
- Когда включение prefault-записи оправдано? Сформулируйте условие (размер БД vs ОЗУ).
- Почему «не измерять, а оптимизировать» — анти-паттерн?
23.11. Чек-лист главы 23
- [ ] Знаю, какие микрооптимизации уже встроены (SIMD-ядра, CMOV, radix-sort, C11-атомики), и что на SIMD-поиск влияю только через
rp_augment_limit/gc_time_limit. - [ ] Могу сформулировать условие оправданности prefault-записи: БД > ОЗУ, частые вытеснения страниц,
MDBX_WRITEMAP. - [ ] Понимаю выигрыш auto-appending split для append-сценариев и слияния с уже грязным соседом (связь с
prefer_waf_insteadof_balance). - [ ] Знаю, что хот-пути минимизируют syscall'и (bulk iovec, массовая запись в
mdbx_copy). - [ ] Могу объяснить, почему батчинг и режим синхронизации решают больше, чем микрооптимизации, и почему оптимизировать нужно по измерениям.
Глава 24. Кэш-поиск (get-cached)
24.1. Что такое get-cached
mdbx_cache_get() — сервис чтения «повторяющихся» ключей. Для ключа хранится адрес значения в
mmap + версионные метки. При повторном чтении вместо полного спуска по B+tree выполняется
ленивый (ранний) выход: спускаемся только пока встречаем страницы, изменённые после последнего
подтверждения.
24.2. Механизм
Запись кэша: {trunk_txnid, last_confirmed_txnid, offset, length} (offset == 0 = «ключа нет»).
Первый вызов — полный поиск + заполнение записи. Последующие:
- спуск останавливается на первой странице, не изменённой после
last_confirmed_txnid; - если ничего не менялось —
MDBX_CACHE_HITпосле нескольких лёгких сравнений; - если изменения есть — подтверждение актуальности (
MDBX_CACHE_CONFIRMED) или полный поиск (MDBX_CACHE_REFRESHED).
24.3. Статусы
| Статус | Значение |
|---|---|
HIT / CONFIRMED / REFRESHED |
Результат получен |
DIRTY |
Значение на грязной (незакоммиченной) странице; валидно только в текущей write-транзакции |
BEHIND |
Читатель старше версии в кэше; кэш обойти |
UNABLE |
ABA-ситуация; поиск в обход кэша |
RACE |
Параллельное обновление записи; поиск в обход |
ERROR (MDBX_CACHE_ERROR) |
Ошибка, не связанная с отсутствием ключа |
Фрагмент из examples/c++/31-get-cached.c++ — отображение
всех статусов кэша get_cached: HIT/CONFIRMED/REFRESHED, DIRTY, BEHIND, UNABLE,
RACE и ERROR:
const char *status_name(int status) {
switch (status) {
case MDBX_CACHE_ERROR:
return "ERROR";
case MDBX_CACHE_BEHIND:
return "BEHIND";
case MDBX_CACHE_UNABLE:
return "UNABLE";
case MDBX_CACHE_RACE:
return "RACE";
case MDBX_CACHE_DIRTY:
return "DIRTY";
case MDBX_CACHE_HIT:
return "HIT";
case MDBX_CACHE_CONFIRMED:
return "CONFIRMED";
case MDBX_CACHE_REFRESHED:
return "REFRESHED";
default:
return "?";
}
}
Полный код: 31-get-cached.c++.
24.4. Когда даёт ускорение
Кэш эффективен для повторяющихся ключей с малым числом изменений (конфигурация, счётчики, маркеры, lookaside-таблицы). Для уникальных ключей выгоды нет (первый поиск всё равно полный). Отношение полного поиска к HIT-пути легко достигает 10²–10³.
24.5. Когда не работает
- Уникальные ключи (нет повторов).
- Активные пишущие транзакции дают
DIRTY(не кэшируются). - Многопоточность требует
MDBX_NOSTICKYTHREADSдля многопоточного варианта; однопоточный вариант (mdbx_cache_get_SingleThreaded) — дешевле.
24.6. История
Запрос многопоточного lockfree get-cached — Gabriel RABHI («Ghost Body Object»). Многопоточный вариант реализован; статус на текущем каноне — уточнять по мере развития API.
Примеры к главе:
examples/c++/31-get-cached.c++.
24.7. Резюме главы 24
- get-cached — ленивый кэш с ранним выходом и версионными метками.
- HIT/…/RACE — статусы результата;
DIRTYв write-транзакциях. - Эффективен для повторяющихся «горячих» ключей; до 1000× быстрее поиска.
- Многопоточный вариант требует NOSTICKYTHREADS.
24.8. Упражнения
- Оцените: цикл из 1 000 000 чтений одного ключа — сколько сэкономит кэш?
- Почему
DIRTYне кэшируется?
24.9. Чек-лист главы 24
- [ ] Описываю запись кэша
{trunk_txnid, last_confirmed_txnid, offset, length}и смыслoffset == 0. - [ ] Объясняю механику ленивого выхода: спуск останавливается на первой странице, не изменённой после
last_confirmed_txnid. - [ ] Различаю статусы
HIT/CONFIRMED/REFRESHED/DIRTY/BEHIND/UNABLE/RACE/ERRORи знаю, когда поиск идёт в обход кэша. - [ ] Понимаю, что
DIRTYвалиден только в текущей write-транзакции и не кэшируется. - [ ] Умею оценить, когда кэш даст ускорение (повторяющиеся «горячие» ключи), а когда нет (уникальные ключи).
- [ ] Знаю требование
MDBX_NOSTICKYTHREADSдля многопоточного варианта и более дешёвый однопоточный вариант (mdbx_cache_get_SingleThreaded).
Глава 25. Массовые операции
25.1. bunch_delete: удаление «гроздьями»
mdbx_cursor_bunch_delete() с режимом-действием (MDBX_bunch_action_t) удаляет текущее значение /
всё мультизначение / всё до или после позиции / всё подряд. Вместо перебора элементов движок
вырезает целые страницы и ветки — стоимость пропорциональна числу страниц, а не элементов.
25.2. get_batch: пакетное чтение
mdbx_cursor_get_batch() — симметричное массовое чтение партиями (bulk iovec).
25.3. GET/PUT_MULTIPLE
Работа с мультизначениями пакетами: MDBX_GET_MULTIPLE/MDBX_PUT_MULTIPLE для DUPFIXED-таблиц.
25.4. estimate_range/distance/move
Эвристическая оценка без сканирования: mdbx_estimate_range(), mdbx_estimate_distance(),
mdbx_estimate_move(). Строится по общим страницам стеков двух позиций. Точность: в худшем случае
расхождение до 4× на каждом уровне дерева (кроме первого и последнего); на практике — единицы
процентов.
Фрагмент из examples/c++/32-bulk-ops.c++ — массовые операции:
get_batch (чтение пачками), estimate (оценка числа записей) и bunch_delete (удаление
«гроздьями» с вырезанием страниц):
// Пакетное чтение: до 100 пар за вызов.
size_t pairs = 0, chunks = 0;
cur.to_first();
bool is_last = false;
while (!is_last) {
auto batch = cur.get_batch(100, mdbx::cursor::move_operation::next, &is_last);
pairs += batch.size();
++chunks;
}
std::cout << "batch reads: " << pairs << " pairs in " << chunks << " chunks\n";
// Оценка количества записей (приблизительно).
cur.to_first();
auto est = cur.estimate(mdbx::cursor::move_operation::last);
std::cout << "estimate: ~" << est.approximate_quantity << " keys\n";
// Удаление «гроздьями»: всё от текущей позиции до конца.
cur.to_key_exact(mdbx::slice("k500"));
const size_t removed = cur.erase_bunch(mdbx::cursor::bunch_delete::delete_after_including);
std::cout << "bunch delete removed " << removed << " items\n";
Полный код: 32-bulk-ops.c++.
25.5. txn_clone: параллельная обработка на одном снапшоте
mdbx_txn_clone() размножает read-only транзакцию: несколько обработчиков на одном снапшоте без
повторного сканирования RLT.
25.6. Батчинг транзакций
100–10 000 операций на коммит — типичный диапазон для записи-heavy. Больше операций → ниже WAF и выше пропускная способность.
25.7. Когда «максимально большая транзакция» — анти-паттерн
- Транзакция в миллионы операций может переполнить лимиты dirty-страниц (
TXN_FULL), породить спилл и всплеск latency. - Долгая write-транзакция блокирует других писателей и увеличивает окно риска.
- Дробите на разумные батчи (например, по 10 000 операций) — баланс между WAF и задержкой.
Примеры к главе:
examples/c++/32-bulk-ops.c++.
25.8. Резюме главы 25
- bunch_delete/delete_range — массовое удаление вырезанием страниц.
- get_batch, GET/PUT_MULTIPLE — пакетное чтение/запись.
- estimate_* — дешёвая оценка диапазонов (до 4×/уровень в худшем случае).
- txn_clone — параллельная обработка на одном снапшоте.
- Разумный батч лучше «максимально большой транзакции».
25.9. Упражнения
- Сравните посточное удаление диапазона и
bunch_deleteна 100 000 ключах. - Когда
estimate_rangeполезен до выполнения запроса?
25.10. Чек-лист главы 25
- [ ] Объясняю, почему
bunch_deleteдешевле перебора элементов: вырезаются целые страницы и ветки, стоимость пропорциональна числу страниц. - [ ] Знаю симметричные массовые чтения/записи:
get_batch,MDBX_GET_MULTIPLE/MDBX_PUT_MULTIPLE(DUPFIXED-таблицы). - [ ] Понимаю принцип
estimate_*: оценка по общим страницам стеков двух позиций, в худшем случае до 4× на уровень дерева. - [ ] Умею применить
txn_cloneдля параллельной обработки на одном снапшоте. - [ ] Могу назвать последствия «максимально большой транзакции»:
TXN_FULL, спилл, всплеск latency, блокировка других писателей. - [ ] Выбираю разумный размер батча (например, ~10 000 операций) как баланс между WAF и задержкой.
Глава 26. Бенчмарки и измерения
26.1. ioarena
ioarena — стандартный бенчмарк-конвейер проекта. Запускается на готовых конфигурациях;
показывает TPS записи/чтения для разных движков и режимов.
26.2. Что измерять
- TPS записи — коммитов/сек в режимах синхронизации.
- get/s чтения — операций/сек.
- WAF — через счётчики страниц (
mdbx_stat -p). - Задержку коммита —
mdbx_txn_commit_ex()→MDBX_commit_latencyпо стадиям (preparation/gc_wallclock/audit/write/sync/ending/whole).
26.3. MDBX_ENABLE_PROFGC
Профилирование GC: собирается в LCK-файле, возвращается в commit_latency.gc_prof. Ключевые поля:
work_rtime_monotonic/work_xtime_cpu (время GC), work_rsteps/work_xpages (фрагментация),
work_majflt (page-faults), max_reader_lag/max_retained_pages (долгие читатели), kicks
(HSR — Handle-Slow-Readers, вытеснение застрявших читателей; см. том V, гл. 29).
Фрагмент из examples/c++/33-profiler.c++ — чтение PROFGC-полей
из commit_latency.gc_prof (wloops, coalescences, flushes, kicks, max_reader_lag,
max_retained_pages); поля заполняются в сборках с MDBX_ENABLE_PROFGC:
// Нагрузка, порождающая работу GC: вставки и удаления.
for (int round = 0; round < 5; ++round) {
auto txn = env.start_write();
auto table = txn.open_map(nullptr);
for (int i = 0; i < 500; ++i) {
const auto key = "k" + std::to_string(round * 1000 + i);
txn.insert(table, mdbx::slice(key), mdbx::slice("payload"));
}
txn.commit();
auto del = env.start_write();
auto dtable = del.open_map(nullptr);
for (int i = 0; i < 500; ++i)
del.erase(dtable, mdbx::slice("k" + std::to_string(round * 1000 + i)));
del.commit();
}
auto txn = env.start_write();
auto table = txn.open_map(nullptr);
txn.insert(table, mdbx::slice("last"), mdbx::slice("v"));
const auto lat = txn.commit_get_latency();
const auto &gc = lat.gc_prof;
std::cout << "gc_prof: wloops=" << gc.wloops << " coalescences=" << gc.coalescences << " flushes=" << gc.flushes
<< " kicks=" << gc.kicks << " max_reader_lag=" << gc.max_reader_lag
<< " max_retained_pages=" << gc.max_retained_pages << "\n";
Полный код: 33-profiler.c++.
26.4. Типичные ошибки бенчмаркинга
- Ассерты: отладочная сборка (
MDBX_CHECKING>0) замедляет кратно. - Некогерентность page cache (#269) на Linux при нескольких процессах.
- Методология: «мелкие транзакции + DURABLE на Windows» — это LockFileEx, а не движок.
- Сравнение без контекста: режим, страница, высота дерева, оборудование.
26.5. Почему libmdbx может быть «в 6–7 раз медленнее LMDB»
Обычно означает: включены ассерты, либо некорректный бенчмарк (мелкие транзакции на Windows с LockFileEx), либо измеряется single-op режим, где libmdbx сознательно платит за более строгие гарантии (двухфазная мета, проверки). При корректной методологии и тех же режимах разрыв исчезает или обращается в пользу libmdbx.
26.6. Ориентиры
- Запись: ~200 TPS одиночных durable-транзакций на обычном SSD; с батчингом и SAFE_NOSYNC — десятки– сотни тысяч и миллионы операций/с.
- Чтение: 1–3 млн get/с (короткие ключи, тёплый кэш); линейное масштабирование по ядрам.
- Вставки: 20K–10M/с в зависимости от режима и железа.
Внимание: всегда указывайте контекст числа — режим sync, размер страницы, размер транзакции, железо.
26.7. Масштабирование чтения
Чтение wait-free и линейно масштабируется по ядрам — до упора в память (bandwidth/TLB). Узкое место обычно не движок, а кэш/память.
Примеры к главе:
examples/c++/33-profiler.c++.
26.8. Резюме главы 26
- ioarena — стандартный бенчмарк; TPS/get/s/WAF/latency.
- commit_latency + PROFGC — диагностика узких мест.
- Ассерты и методология — главные ловушки.
- «6–7× медленнее LMDB» почти всегда ошибка измерения.
- Ориентиры: 200 TPS durable, 1–3M get/s, 20K–10M вставок/с.
26.9. Упражнения
- Измерьте
commit_latencyна своей конфигурации и определите доминирующую стадию. - Объясните, почему сравнение «одна транзакция на операцию» некорректно для оценки движка.
26.10. Чек-лист главы 26
- [ ] Знаю, что измерять: TPS записи, get/s чтения, WAF (через счётчики страниц), задержку коммита по стадиям.
- [ ] Умею читать
MDBX_commit_latency(preparation/gc_wallclock/audit/write/sync/ending/whole) и определять доминирующую стадию. - [ ] Понимаю, о чём говорят поля
gc_prof(max_reader_lag,max_retained_pages,kicks) — о долгих читателях и срабатываниях HSR. - [ ] Избегаю типичных ошибок: замер на сборке с ассертами, «мелкие транзакции + DURABLE на Windows», сравнение без контекста.
- [ ] Могу объяснить, почему «в 6–7 раз медленнее LMDB» почти всегда ошибка измерения или плата за более строгие гарантии.
- [ ] Знаю ориентиры (~200 TPS durable, 1–3 млн get/s) и всегда указываю контекст числа.
- [ ] Объясняю, почему чтение масштабируется линейно по ядрам и где реальное узкое место (память/кэш, а не движок).
Итог тома
Вы умеете измерять и настраивать libmdbx: понимаете WAF, выбираете конфигурацию под сценарий, используете микрооптимизации, get-cached и массовые операции, читаете профиль GC и latency.
Что дальше: Том V — экспертные темы: правила и советы из базы знаний, платформенные нюансы, HSR, диагностика и отладка, миграция с LMDB, паттерны проектирования, roadmap.