From 3770224eaf3f3a9a69c6eac92d3eb9af79e57f31 Mon Sep 17 00:00:00 2001 From: Efim Beshmenev <71215959+Loochik@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:42:26 +0300 Subject: [PATCH] New adaptation policy --- CMakeLists.txt | 4 + README.md | 80 +- benchmark.bat | 52 +- docs/architecture.md | 200 ++-- docs/benchmarking.md | 188 ++-- docs/findings.md | 53 +- .../universal_container/adaptation_policy.hpp | 83 ++ .../universal_container/adaptive_sequence.hpp | 563 ++++++++--- .../universal_container/flat_hash_index.hpp | 182 +++- .../universal_container/tiered_storage.hpp | 100 +- .../20260813-focused-resize/README.md | 82 ++ .../20260813-focused-resize/baseline.csv | 190 ++++ src/benchmark.cpp | 2 +- src/focused_benchmark.cpp | 896 ++++++++++++++++++ tests/adaptive_sequence_tests.cpp | 345 +++++-- tools/audit_build_flags.ps1 | 2 +- 16 files changed, 2533 insertions(+), 489 deletions(-) create mode 100644 results/benchmarks/20260813-focused-resize/README.md create mode 100644 results/benchmarks/20260813-focused-resize/baseline.csv create mode 100644 src/focused_benchmark.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f5a361a..c680aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,4 +89,8 @@ if(UC_BUILD_BENCHMARKS) add_executable(uc_bench src/benchmark.cpp) target_link_libraries(uc_bench PRIVATE universal_container) set_property(TARGET uc_bench PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE) + + add_executable(uc_focused_bench src/focused_benchmark.cpp) + target_link_libraries(uc_focused_bench PRIVATE universal_container) + set_property(TARGET uc_focused_bench PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE) endif() diff --git a/README.md b/README.md index f0d46bd..926a47c 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,35 @@ # Universal Container -Экспериментальный C++20-контейнер индексируемой последовательности, который во -время работы выбирает представление и его геометрию: +Экспериментальный C++20-контейнер индексируемой последовательности с двумя +представлениями. Текущая default-policy привязывает все автоматические +перестройки к изменению логической вместимости: ```text -contiguous vector <-> segmented tiered storage - | - +-> leaf 64 / 128 / 256 / 512 / 1024 - +-> минимально достаточная глубина каталога +capacity boundary + |-- grow: capacity *= 2 when the current capacity is exhausted + `-- shrink: capacity /= 2 at 12.5% occupancy + | + +-- small N: contiguous vector + `-- large N: tiered storage, leaf = ceil(sqrt(N)) ``` -Это не контейнер с одним «лучшим» размером блока. `CostModelPolicy` собирает -выборочную статистику чтений и полную статистику структурных изменений, -оценивает стоимость кандидатов и выдаёт `AdaptationDecision`: целевой режим и -`TieredConfig`. Переход выполняется, только если прогнозируемая экономия на -будущем горизонте превышает стоимость O(N)-перестройки с запасом. +Между изменениями вместимости контейнер не меняет ни представление, ни размер +leaf. На границе вместимости auto-mode заново выбирает `vector` или tiered, +поэтому обратный переход `tiered -> vector` также возможен. Focused cutoff +равен 4096 элементов; он выбран по baseline focused-run 7 x 100000 для +текущего `uint32_t` workload и остаётся калибруемой, а не универсальной +константой. -Проект исследовательский. Сохранённые измерения показывают, что оптимальный -leaf действительно меняется с `N`, размером элемента и локальностью правок, но -пока не доказывают превосходство adaptive-варианта во всех или в среднем по -финальной holdout-матрице. Ограничения честно перечислены в +Поиск по значению не удалён: `AdaptiveSequence` использует flat hash. +Число hash buckets рассчитывается из логической вместимости с запасом под +worst-case load не выше 70% и пересматривается только на той же границе +вместимости. Это исключает независимый rehash посреди обычной серии операций. + +Старые многомерные benchmark-матрицы и сделанные по ним выводы сохранены только +как **historical / retired**. Активная постановка и ограничения описаны в +[docs/benchmarking.md](docs/benchmarking.md) и [docs/findings.md](docs/findings.md). -`std::deque` и `std::list` присутствуют только как внешние benchmark-baselines. -Они не являются режимами контейнера: `AdaptiveSequence` переключается только -между contiguous vector и tiered storage. - ## Быстрый старт Из обычной командной строки Windows: @@ -50,7 +54,8 @@ C++». `build.bat` сам находит комплектный CMake из Visua - `uc_demo.exe` — минимальный пример; - `uc_tests.exe` — differential/property-тесты; -- `uc_bench.exe` — benchmark runner. +- `uc_focused_bench.exe` — текущий focused benchmark runner; +- `uc_bench.exe` — historical / retired matrix runner. Все Release-цели собираются с `/O2` и статическим MSVC runtime (`/MT`). Профиль `scalar` — проверенный для MSVC 19.51 режим `project-novec`: внутренний ключ @@ -61,16 +66,27 @@ AVX2-тестов и benchmark выполняется проверка CPU/OS; безопасно пропускается. Подробности приведены в [docs/benchmarking.md](docs/benchmarking.md). -Полный воспроизводимый запуск: +Текущий воспроизводимый запуск: ```bat -benchmark.bat smoke -benchmark.bat quick -benchmark.bat full +benchmark.bat ``` -Для multi-core сравнения `std::vector`, пяти fixed-tiered geometry и adaptive -вплоть до 100 миллионов `uint32_t`: +Он собирает и тестирует только профиль `baseline`, создаёт timestamp-папку и +один раз запускает `uc_focused_bench` с 100000 операций и 7 повторами. В +benchmark сравниваются три варианта одного типа +`AdaptiveSequence`: forced vector, forced tiered и auto-mode; +flat hash включён у всех трёх. Mixed trace содержит 97% индексных чтений, 1% +hash-поисков `find_one` (50% hit / 50% miss), 1% вставок и 1% удалений. Runner +также печатает медианную пропускную способность каждого типа операций отдельно. + +Raw CSV, metadata и медианная таблица `summary.md` сохраняются в +`results\benchmarks\-focused\`. Подтверждённая таблица публикуется +вместе с результатами запуска, а не вшивается в документацию до завершения +серии. + +Следующие команды относятся к **historical / retired benchmark matrix** и не +являются текущим способом калибровки: ```bat large_benchmark.bat smoke 12 @@ -78,13 +94,9 @@ large_benchmark.bat full 12 3 large_benchmark.bat full 12 3 huge ``` -Третий аргумент ограничивает число одновременно работающих N=100M-ячеек; -значение 3 — эвристически более безопасный вариант для машины с 32 GiB RAM. -Четвёртый аргумент оставляет в матрице только N=100M. - -Каждый запуск создаёт отдельную timestamp-папку в `results\benchmarks\`, куда -пишутся raw CSV, сводки и описание окружения. `out\` полностью игнорируется -Git, результаты исследования — намеренно нет. +Старые CSV оставлены для аудита истории, но напрямую не сравнимы с focused +постановкой: в ней другой workload и hash обязателен у каждого кандидата. +`out\` полностью игнорируется Git, результаты исследования — намеренно нет. ## API @@ -100,7 +112,7 @@ auto x = values[0]; values.force_vector_mode(); values.force_tiered_mode({256, 64, 4}); values.enable_auto_mode(); -values.adapt_now(); // явная безопасная maintenance point +values.adapt_now(); // default-policy не меняет mode между resize ``` Опциональный flat hash index со stable IDs: diff --git a/benchmark.bat b/benchmark.bat index f4685de..85103bf 100644 --- a/benchmark.bat +++ b/benchmark.bat @@ -2,56 +2,28 @@ setlocal EnableExtensions cd /d "%~dp0" -set "UC_SCALE=%~1" -if not defined UC_SCALE set "UC_SCALE=smoke" -if /I not "%UC_SCALE%"=="smoke" if /I not "%UC_SCALE%"=="quick" if /I not "%UC_SCALE%"=="full" ( - echo Usage: benchmark.bat [smoke^|quick^|full] +if not "%~1"=="" ( + echo Usage: benchmark.bat + echo. + echo The retired smoke/quick/full matrix is no longer run by this script. exit /b 2 ) -set "UC_PROFILES=scalar baseline" -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\test_avx2.ps1" -if not errorlevel 1 set "UC_PROFILES=scalar baseline avx2" +call build.bat baseline test +if errorlevel 1 exit /b 1 -for %%P in (%UC_PROFILES%) do ( - call build.bat %%P test - if errorlevel 1 exit /b 1 -) - -for /f "delims=" %%I in ('powershell.exe -NoProfile -Command "Get-Date -Format yyyyMMdd-HHmmss"') do set "UC_RUN_ID=%%I-%UC_SCALE%" +for /f "delims=" %%I in ('powershell.exe -NoProfile -Command "Get-Date -Format yyyyMMdd-HHmmss"') do set "UC_RUN_ID=%%I-focused" set "UC_RESULT_DIR=results\benchmarks\%UC_RUN_ID%" mkdir "%UC_RESULT_DIR%" 2>nul -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\write_benchmark_environment.ps1" -ResultDirectory "%UC_RESULT_DIR%" -RunId "%UC_RUN_ID%" -Scale "%UC_SCALE%" -Profiles "%UC_PROFILES%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\write_benchmark_environment.ps1" -ResultDirectory "%UC_RESULT_DIR%" -RunId "%UC_RUN_ID%" -Scale "focused-100k-7x" -Profiles "baseline" if errorlevel 1 exit /b 1 -for %%P in (%UC_PROFILES%) do ( - echo. - echo === Tuning %%P /O2 profile === - "out\bin\%%P\Release\uc_bench.exe" --suite tune --scale "%UC_SCALE%" --fanout 64 --output "%UC_RESULT_DIR%\%%P-tune.csv" --summary "%UC_RESULT_DIR%\%%P-tune-summary.csv" - if errorlevel 1 exit /b 1 - - echo === Adaptive phase transitions for %%P === - "out\bin\%%P\Release\uc_bench.exe" --suite adapt --scale "%UC_SCALE%" --leaf 512 --fanout 64 --levels 4 --output "%UC_RESULT_DIR%\%%P-adapt.csv" --summary "%UC_RESULT_DIR%\%%P-adapt-summary.csv" - if errorlevel 1 exit /b 1 - - echo === Comparing %%P /O2 profile === - "out\bin\%%P\Release\uc_bench.exe" --suite compare --scale "%UC_SCALE%" --leaf 512 --fanout 64 --levels 4 --output "%UC_RESULT_DIR%\%%P-compare.csv" --summary "%UC_RESULT_DIR%\%%P-compare-summary.csv" - if errorlevel 1 exit /b 1 - - echo === Hash index and stable-ID overhead for %%P === - "out\bin\%%P\Release\uc_bench.exe" --suite index --scale "%UC_SCALE%" --leaf 512 --fanout 64 --levels 4 --output "%UC_RESULT_DIR%\%%P-index.csv" --summary "%UC_RESULT_DIR%\%%P-index-summary.csv" - if errorlevel 1 exit /b 1 -) - -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\analyze_results.ps1" -InputDirectory "%UC_RESULT_DIR%" -Pattern "*-compare.csv" -OutputFile "%UC_RESULT_DIR%\aggregate-compare.csv" -CellMedianFile "%UC_RESULT_DIR%\cell-medians-compare.csv" -if errorlevel 1 exit /b 1 - -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\analyze_results.ps1" -InputDirectory "%UC_RESULT_DIR%" -Pattern "*-adapt.csv" -OutputFile "%UC_RESULT_DIR%\aggregate-adapt.csv" -CellMedianFile "%UC_RESULT_DIR%\cell-medians-adapt.csv" -if errorlevel 1 exit /b 1 - -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\validate_adaptation.ps1" -CompareCellMedianFile "%UC_RESULT_DIR%\cell-medians-compare.csv" -AdaptCellMedianFile "%UC_RESULT_DIR%\cell-medians-adapt.csv" +echo. +echo === Focused resize benchmark: baseline /O2, 100000 operations, 7 repeats === +"out\bin\baseline\Release\uc_focused_bench.exe" --min-n 256 --max-n 65536 --operations 100000 --repeats 7 --output "%UC_RESULT_DIR%\baseline-focused.csv" > "%UC_RESULT_DIR%\summary.md" if errorlevel 1 exit /b 1 +type "%UC_RESULT_DIR%\summary.md" echo. echo Benchmark run completed: %UC_RESULT_DIR% diff --git a/docs/architecture.md b/docs/architecture.md index aaebad4..adcd787 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,101 +2,137 @@ ## Представления -`AdaptiveSequence` хранит `variant, TieredStorage>`. Векторный -режим предназначен для random/sequential read и append. Tiered-режим состоит из -независимо выделенных циклических leaf-блоков (`RingBlock`) и многоуровневого -каталога весов. Каталог строит только необходимое число уровней до заданного -максимума; поиск спускается по уровням, а отдельный Fenwick index восстанавливает -логическую позицию stable ID. +`AdaptiveSequence` хранит `variant, TieredStorage>`. +Contiguous vector выгоден для малых последовательностей и индексного чтения. +Tiered storage состоит из независимо выделенных циклических leaf-блоков +(`RingBlock`) и многоуровневого каталога весов. -Текущий `TieredStorage` — сегментированный исследовательский вариант, а не -полная реализация implicit tiered vector с offsets на каждом внутреннем узле. -Offsets реально используются внутри leaf, но split/merge меняет массив -дескрипторов leaf и перестраивает каталог. Это важное ограничение: некоторые -uniform-edit результаты отражают O(number_of_leaves) обслуживание split, а не -теоретическую границу полноценного tiered vector. +У обоих backend есть единая **логическая вместимость** `capacity()`. Это не +сумма физического slack в leaf-блоках, а управляющая граница, на которой +разрешена автоматическая O(N)-перестройка. -## Адаптивное решение +Текущий `TieredStorage` остаётся сегментированным исследовательским вариантом, а +не полной реализацией implicit tiered vector с offsets/carry на каждом +внутреннем узле. Offsets используются внутри leaf, но split/merge может менять +массив leaf-дескрипторов и перестраивать каталог за O(number_of_leaves). Это +ограничение нужно учитывать при интерпретации результатов. -Storage и policy разделены. Контейнер передаёт `OperationSample`, policy -возвращает: +## Текущая default-policy: перестройка только с capacity -```cpp -struct AdaptationDecision { - StorageMode target; - TieredConfig tiered_config; - double expected_saving; -}; -``` +`ResizePolicy` не пытается распознать workload после каждой операции. В +auto-mode представление и геометрия пересматриваются только вместе с изменением +логической вместимости: -Для каждого окна оцениваются vector и все tiered-кандидаты. Главная форма -модели edit-cost: +1. Перед вставкой, которая не помещается, capacity удваивается до достаточного + значения. Для обычного роста это одна граница `C -> 2C`. +2. После удаления при `size <= capacity / 8`, то есть при заполнении не более + 12.5%, capacity уменьшается на один шаг `C -> C/2` (но не ниже `size`). +3. Внутри той же перестройки выбираются mode, leaf geometry и размер hash + table. Между такими границами они остаются неизменными. + +В auto-mode целевое представление определяется размером на момент перестройки: ```text -vector: fixed + move_unit * sizeof(T) * (N - position) - -tiered: fixed - + move_unit * sizeof(T) * distance_inside_leaf - + directory_unit * locality_multiplier * N / leaf - + lookup(levels) +target_size < 4096 -> vector +target_size >= 4096 -> tiered ``` -Первая переменная часть tiered растёт с leaf, вторая — с `N/leaf`. Поэтому -минимум сдвигается к большим блокам при росте N. Для близких последовательных -edit-позиций directory multiplier уменьшается: горячая область не создаёт -split во множестве разных leaf, и policy выбирает меньший блок. +`4096` — cutoff, подтверждённый текущим focused-run 7 x 100000 для +`AdaptiveSequence` и заданного mix; это не универсальный +результат для всех типов и workload. Само пересечение `size=4096` немедленного перехода не +вызывает: mode меняется только на следующей capacity-boundary. Например, при +обычном последовательном росте контейнер остаётся vector при capacity 4096 и +переходит в tiered, когда следующая вставка меняет capacity на 8192. На shrink +boundary правило применяется заново, поэтому возможен обратный переход +`tiered -> vector`. -Для каждого leaf вычисляется минимально достаточная глубина при текущих `N` и -fanout. Сравниваются три альтернативы: остаться, перейти в другое -представление, либо перестроить tiered с другой геометрией. Решение требует: +Явные `force_vector_mode()`, `force_tiered_mode()` и `reserve()` остаются +пользовательскими управляющими операциями и не являются автоматической +адаптацией. + +## Геометрия tiered storage + +При capacity-boundary размер малого массива вычисляется из фактического размера +последовательности на этой границе: + +```text +leaf_capacity = max(4, ceil(sqrt(target_size))) +``` + +Значения fanout и допустимой глубины каталога берутся из `TieredConfig`. +`leaf_capacity` не подстраивается после каждой вставки или удаления: новое +`sqrt(N)` применяется только в общей перестройке. Поэтому rebuild одновременно +может выполнить `vector -> tiered`, `tiered -> vector` или +`tiered -> tiered` с новой геометрией. + +## Flat hash index + +`AdaptiveSequence` использует open-addressed flat table +`value -> {head_id,count}`. Дубликаты связаны через компактные intrusive links в +ID metadata. `find_one` возвращает любой экземпляр, `find_all_ids` — unordered +stable IDs, а `find_all` сортирует логические позиции и может стоить +O(k log k). + +Внутри цепочек используются 32-битные slot IDs. Освобождённые metadata slots +переиспользуются через free list, а публичный 64-битный stable ID кодирует +`generation + slot`. Поэтому сбалансированный insert/erase churn не наращивает +metadata без границ и старый ID не оживает после повторного использования slot. + +Hash table следует той же capacity-policy, что и storage: + +```text +required_buckets = ceil(logical_capacity / 0.70) +bucket_count = next_power_of_two(max(16, required_buckets)) +``` + +Иными словами, таблица заранее рассчитана на худший случай «один уникальный +ключ на элемент» при load factor не выше 70%. При росте bucket reserve/rehash +выполняется перед перестройкой storage, при shrink — после неё. Целевое число +buckets пересчитывается только при изменении логической capacity; если оно не +изменилось, отдельного rehash нет. Обычные вставки и удаления между границами не +запускают независимое увеличение hash table. + +Такой выбор сохраняет O(1) expected lookup и включает стоимость редкого rehash +в ту же измеряемую capacity-перестройку, где уже оплачивается перенос storage. +Flat hash остаётся compile-time optional: вариант `AdaptiveSequence` +не несёт его память; тот же API поиска по значению работает линейным обходом. + +## Инвалидация + +- const-чтение и hash lookup ничего не инвалидируют; +- `set()` не меняет логические позиции, но ссылка на заменённое значение не + должна использоваться как ссылка на старый объект; +- вставка или удаление инвалидирует позиционные references, pointers и + iterators согласно структурному изменению; capacity-boundary дополнительно + может перестроить весь backend; +- conversion, geometry rebuild, `force_*`, `reserve`, `clear` и + `make_contiguous()` инвалидируют всё позиционное; +- stable ID переживает shifts, split и смену представления и перестаёт быть + живым только после удаления соответствующего элемента или `clear()`. + +Stable ID имеет область действия конкретного состояния конкретного контейнера. +Copy/move assignment заменяет это состояние: ранее полученные ID обеих сторон +после assignment использовать нельзя (их числовое значение может совпасть с ID +элемента нового состояния). Move construction переносит состояние целиком и +сохраняет его ID в новом объекте. + +Debug iterator хранит structural generation и бросает `logic_error` после +инвалидирования. + +## Historical / retired: operation-window CostModelPolicy + +До текущей постановки default-policy собирала `OperationSample`, оценивала +vector и набор фиксированных leaf 64/128/256/512/1024, применяла EWMA, +hysteresis и прогноз окупаемости O(N)-перехода: ```text (current_cost - candidate_cost) * forecast > rebuild_cost * safety_factor ``` -Для `vector→tiered`, `tiered→vector` и `tiered→tiered` используются разные -safety factor. Дополнительно действуют minimum residency, минимальное улучшение -shape, EWMA и два подтверждающих окна. После перехода статистическое окно -начинается заново. Все коэффициенты находятся в `AdaptationConfig` и могут -заменяться другой policy. - -## Почему чтение по умолчанию не перестраивает storage - -`ReadAdaptationMode::deferred` лишь записывает статистику. Pending-решение -выполняется на следующей мутации или при явном `adapt_now()`. Это сохраняет -важный контракт: обычное чтение не инвалидирует уже выданную ссылку. - -`eager_nonconst` оставлен только как benchmark-эксперимент. Он может выполнить -переход вокруг non-const `operator[]`, добавляет проверку в горячий read path и -имеет более слабую семантику. Первые измерения не оправдали его как default. - -Для read-only фазы, после которой мутаций нет, вызывающий код может поставить -явную maintenance point: - -```cpp -values.adapt_now(); -``` - -## Инвалидация - -- const-чтение и сбор статистики в deferred mode ничего не инвалидируют; -- `set()` не меняет логические позиции, но ссылка на заменённое значение не - должна использоваться как ссылка на старый объект; -- любая структурная операция в auto mode может также выполнить rebuild, поэтому - инвалидирует все references, pointers и iterators; -- conversion, shape rebuild, `force_*`, `reserve`, `clear` и - `make_contiguous()` инвалидируют всё позиционное; -- stable ID переживает shifts, split и смену представления; он перестаёт быть - живым только после удаления элемента или `clear()`. - -Debug-проверка iterator хранит structural generation и бросает `logic_error` -после инвалидирования. - -## Hash index - -`AdaptiveSequence` использует open-addressed flat table -`value -> {head_id,count}`. Дубликаты связаны через компактные intrusive links в -ID metadata. `find_one` означает любой экземпляр. `find_all` возвращает -логически отсортированные позиции и потому может стоить O(k log k), тогда как -`find_all_ids` возвращает unordered stable IDs за O(k). +Также исследовались deferred/eager read adaptation, minimum residency и +отдельные `vector -> tiered`, `tiered -> vector`, `tiered -> tiered` решения. +Эта схема и соответствующая benchmark-матрица **retired**: они сохранены для +воспроизводимости старых CSV и для экспериментов с явно выбранной +`CostModelPolicy`, но не описывают текущую default-policy и не используются для +нового cutoff. diff --git a/docs/benchmarking.md b/docs/benchmarking.md index d3d8a0f..ab0d98a 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -1,99 +1,137 @@ # Сборка и benchmark-методика -## Профили кода +## Активная focused-постановка -Все сравниваемые бинарники — Release x64 `/O2`, `/MT`, без IPO/LTO. Отличается -только ось SIMD: +Текущий benchmark отвечает на один практический вопрос: при каком `N` для +фиксированной малой доли правок становится выгодно переходить от contiguous +vector к tiered storage. Старую широкую матрицу типов, SIMD-профилей, leaf и +workload для этой калибровки не используют. -| Профиль | Ключи проекта | Назначение | +Все три кандидата имеют один и тот же тип данных и один и тот же flat hash: + +| Имя в CSV | Реализация | Режим | |---|---|---| -| `scalar` | `/O2 /d2Qvec- /Qvec-report:1 /Oi-` | MSVC-specific `project-novec` | -| `baseline` | `/O2`, без `/arch:AVX*` | обычный MSVC x64 | -| `avx2` | `/O2 /arch:AVX2` | разрешён AVX2 | +| `forced_vector_hash` | `AdaptiveSequence` | принудительно vector | +| `forced_tiered_hash` | `AdaptiveSequence` | принудительно tiered | +| `adaptive_hash` | `AdaptiveSequence` | текущая resize-bound auto-policy | -`/d2Qvec-` — внутренний, недокументированный ключ MSVC, а не переносимая -гарантия. Командная строка проверена автоматическим аудитом для MSVC 19.51; -вывод `/Qvec-report` пока не разбирается как отдельное доказательство. `scalar` -не означает «ни одной SIMD-инструкции в процессе»: x64 ABI имеет SSE2-baseline, -а CRT `memmove` может runtime-dispatch-ить SIMD. Дополнительно задаётся -`_USE_STD_VECTOR_ALGORITHMS=0`, чтобы STL не включала свои явно -векторизованные алгоритмы. Это честнее называть `project-novec`. +Таким образом, поиск по значению не даёт одному кандидату скрытого преимущества: +стоимость хранения и обновления hash index присутствует у vector, tiered и +adaptive. Для forced tiered начальный leaf равен `ceil(sqrt(N))`. Auto-mode +выбирает mode и leaf только при capacity-boundary. -## Матрица +## Трасса -Benchmark сравнивает: +Mixed trace содержит ровно 100000 операций в default-запуске: -- reserved `std::vector`; -- `std::deque`; -- `std::list` с index semantics; -- forced tiered для каждого leaf 64/128/256/512/1024; -- adaptive deferred; -- experimental adaptive eager-read. +| Операция | Доля | Default count | Детали | +|---|---:|---:|---| +| индексное чтение | 97% | 97000 | случайный существующий индекс | +| hash find по значению | 1% | 1000 | `find_one`, 50% hit / 50% miss | +| вставка | 1% | 1000 | случайная позиция | +| удаление | 1% | 1000 | случайная существующая позиция | -`std::deque` и `std::list` — только внешние фиксированные baselines; -`AdaptiveSequence` никогда в них не переключается. Для `std::list` runner -эмулирует index semantics линейным проходом, поэтому доступ по индексу имеет -O(N). +Виды операций перемешиваются детерминированным seed. Вставки и удаления +сбалансированы, поэтому benchmark измеряет steady-size workload, но любая +реально достигнутая capacity-boundary и стоимость перехода остаются внутри +mixed timer. Построение исходного контейнера находится вне timer. -Типы: `uint32_t`, `uint64_t`, trivially-copyable 16/32/64-byte значения и -non-trivial movable string wrapper. Workloads включают random access, traversal, -append, 99.99/99/80/50% reads, uniform/localized edits, bursty edits и отдельный -phase trace: +Кроме общей `mixed ops/s`, runner измеряет отдельные homogeneous batches и +выводит `read ops/s`, `find ops/s`, `insert ops/s`, `erase ops/s`. Быстрые read +и find batches содержат не менее 100000 вызовов для устойчивости таймера; +фактические batch counts записываются в CSV. Поэтому отдельные колонки — это +пропускная способность соответствующего типа операции, а не время его 1%-доли +в mixed trace. -```text -uniform edits -> maintenance -localized edits -> maintenance -reads -> maintenance +Один и тот же материализованный trace и seed применяются к трём кандидатам. +Порядок кандидатов циклически меняется между paired repeats, чтобы фиксированная +первая или последняя позиция не принадлежала всегда одному представлению. +Checksum проверяет совпадение состояния и результатов операций. + +## Capacity, leaf и hash во время измерения + +Focused benchmark проверяет текущий контракт контейнера, а не внешнюю +эмуляцию: + +- capacity увеличивается `x2`, когда очередная вставка не помещается; +- при `size <= capacity / 8` выполняется один shrink-шаг `capacity / 2`; +- mode и `leaf = ceil(sqrt(size))` пересчитываются только внутри этой + capacity-перестройки; +- focused cutoff auto-mode равен 4096; +- hash buckets рассчитываются из новой логической capacity с worst-case load + не выше 70% и не меняются независимо между capacity-boundaries. + +Raw CSV сохраняет initial/final mode, leaf и logical capacity mixed-контейнера, +что позволяет увидеть включённый в mixed throughput переход `vector -> tiered` +или обратный переход. Для четырёх отдельных homogeneous batches поля +`read_mode`, `find_mode`, `insert_mode`, `erase_mode` и соответствующие +`*_leaf` отдельно фиксируют их фактический режим/геометрию; эти batches +стартуют с новых контейнеров и потому не обязаны повторять final mode/leaf +mixed-трассы. + +## Воспроизводимый default-run + +Из обычной командной строки Windows: + +```bat +benchmark.bat ``` -Последний trace нужен именно для проверки смены leaf, а не только -`vector↔tiered`. Время rebuild остаётся внутри измеряемого end-to-end интервала. +Скрипт выполняет ровно один focused-run: -Tuning перебирает leaf 64…1024 и maximum directory levels 2/3/4. Каталог сам -останавливает построение на минимально достаточной глубине. +1. `build.bat baseline test` — Release x64 `/O2`, `/MT`, без `/arch:AVX*` и + без IPO/LTO, затем correctness-тесты. +2. Создаёт `results\benchmarks\-focused\` и записывает metadata и + source/binary manifests. +3. Запускает `uc_focused_bench` для степеней двойки `N=256...65536`, 100000 + mixed operations и 7 paired repeats. +4. Печатает таблицу медиан operations/second, сохраняет её в `summary.md`, а + raw repeat data — в `baseline-focused.csv`. + +Эквивалентный вызов runner после сборки: + +```bat +out\bin\baseline\Release\uc_focused_bench.exe --min-n 256 --max-n 65536 --operations 100000 --repeats 7 --output results\benchmarks\focused.csv +``` + +`--operations` должен быть положительным числом, кратным 100. Доступны также +`--seed`, `--min-n` и `--max-n`; границы диапазона округляются до степеней +двойки. + +Финальную таблицу следует публиковать в каталоге соответствующего запуска в +`results/benchmarks/`. Документация хранит только постановку и подтверждённые +выводы, а не копию ещё незавершённой серии. ## CSV -Raw CSV содержит profile/type/workload/N/seed, initial и final tier config, -total/ns-per-op, p50/p95/p99/max edit latency, checksum, allocated bytes, -storage switches, shape rebuilds и final mode. Checksum должен совпасть у всех -контейнеров одной трассы. +Каждая raw-строка содержит: -`tools/analyze_results.ps1` сначала берёт median повторов, затем сравнивает с -лучшим фиксированным baseline в каждой ячейке. Итоговый geometric mean считается -иерархически по sizes → workloads → types → profiles, чтобы семейство с большим -числом строк не получило лишний вес. +- `profile`, `n`, `container`, `repeat`, `seed`; +- initial/final mixed `mode`, `leaf`, `logical_capacity` и отдельные mode/leaf + поля read/find/insert/erase batches; +- mixed counts, включая hit/miss hash find; +- реальные размеры отдельных read/find/insert/erase batches; +- `mixed_ops_per_sec`, `read_ops_per_sec`, `find_ops_per_sec`, + `insert_ops_per_sec`, `erase_ops_per_sec`; +- оценку allocated bytes и checksum. -## Масштабы запуска +Публикуемая таблица использует медиану семи повторов по каждой паре +`(N, container)`. Значения отдельных повторов не отбрасываются и остаются в +raw CSV. -```bat -benchmark.bat smoke rem проверка всего конвейера -benchmark.bat quick rem рабочее исследование -benchmark.bat full rem финальные повторы, длительный запуск -``` +## Historical / retired benchmark matrix -Отдельная large-матрица с обязательным reserved vector, fixed leaf 64…1024 и -adaptive запускается внешним process pool. Второй аргумент — число workers: +Старый `uc_bench`, вызовы `benchmark.bat smoke|quick|full`, multi-profile +сравнение `scalar/baseline/avx2`, типы от `uint32_t` до blob/string wrapper, +fixed leaf 64...1024, `std::deque`, `std::list`, phase traces и +`large_benchmark.bat` до N=100M относятся к **historical / retired** методике. -```bat -large_benchmark.bat smoke 12 -large_benchmark.bat full 12 3 -large_benchmark.bat full 12 3 huge -``` +Их CSV сохранены для аудита истории проекта, но не участвуют в выборе текущего +cutoff: набор кандидатов, mix операций и наличие hash index отличаются от +focused постановки. Старые aggregate scores и speedup нельзя смешивать с новой +таблицей operations/second. -Каждый worker закрепляется за отдельным logical CPU. Третий аргумент ограничивает -число одновременно работающих N=100M-ячеек и лишь снижает риск pagefile; это -эвристика, а не измеритель доступной памяти. Четвёртый аргумент `huge` оставляет -только N=100M. Large raw, -per-cell partial/log, план, environment и сводки сохраняются в отдельной папке -`results/benchmarks/`. - -`large_benchmark.bat full 12 3 huge` воспроизводит только N=100M с удлинённым -512-edit horizon и сам проверяет поддержку AVX2. Прямой PowerShell runner такой -CPU-проверки не выполняет: при его ручном запуске `avx2` надо исключить на -неподдерживаемой машине. - -Для публикации финального числа нужны одинаковый power plan, отсутствие тяжёлой -фоновой нагрузки, закрепление на физическом ядре, randomized paired order и -holdout workloads/seeds. Текущие quick-данные являются calibration set, а не -финальным доказательством. +Профили `scalar` и `avx2` по-прежнему можно собирать для отдельных инженерных +экспериментов. `scalar` использует внутренний MSVC `/d2Qvec-` и не является +универсальной гарантией отсутствия SIMD в CRT/ABI; это ещё одна причина не +расширять текущий baseline-run до старой матрицы без отдельной задачи. diff --git a/docs/findings.md b/docs/findings.md index 42c0f56..fc65702 100644 --- a/docs/findings.md +++ b/docs/findings.md @@ -1,6 +1,49 @@ # Наблюдения и ограничения -## Что уже установлено +## Текущая focused-постановка + +Активное исследование больше не использует старую benchmark-матрицу. Оно +сравнивает только три варианта `AdaptiveSequence` с одинаковым +flat hash: forced vector, forced tiered и resize-bound adaptive. Mixed workload +фиксирован: 97% индексных чтений, 1% hash find (50% hit / 50% miss), 1% вставок +и 1% удалений. + +Automatic policy меняет mode, `leaf = ceil(sqrt(N))` и размер hash table только +при изменении логической capacity. Рост capacity идёт `x2`; один shrink-шаг +`/2` выполняется при заполнении 12.5%. Поэтому цена перехода входит в измерение, +но ни storage, ни hash не перестраиваются после каждой отдельной операции. + +## Final focused run и cutoff + +Baseline focused-run с 7 повторами и 100000 mixed operations подтвердил +локальный crossover между двумя соседними размерами. При `N=2048` mixed median +forced vector равна **31.98M ops/s**, а forced tiered — **32.18M ops/s**: +преимущество tiered по медиане составляет лишь **0.618%**, при этом tiered +быстрее в **4 из 7**, а vector — в **3 из 7** paired repeats. Эта граница +считается неустойчивой и попадающей в шум измерения. При `N=4096` forced tiered +дал **32.53M ops/s**, forced vector — **17.20M ops/s**, а adaptive с включённым +в timer переходом `vector -> tiered` — **32.64M ops/s**. Медиана tiered выше +vector в **1.8911 раза**, и tiered быстрее во всех **7 из 7** paired repeats. + +Поэтому для текущего `uint32_t` workload принят **cutoff 4096** как первый +однозначный и устойчивый размер в sweep. Это подтверждённый результат +focused-серии, но всё ещё калибруемая граница, а не +универсальная константа для других типов и workload. Полная таблица медиан по +mixed/read/find/insert/erase и методика опубликованы в +[`20260813-focused-resize/README.md`](../results/benchmarks/20260813-focused-resize/README.md); +raw повторы находятся в +[`20260813-focused-resize/baseline.csv`](../results/benchmarks/20260813-focused-resize/baseline.csv). +В CSV 189 строк: у всех строк корректные счётчики операций, checksums совпадают +между тремя кандидатами для каждой пары `(N, repeat)`, а режимы и leaf отдельных +read/find/insert/erase batches записаны отдельными столбцами. + +## Historical / retired benchmark evidence + +Все результаты ниже относятся к прежним workload, кандидатам и policy. Они +сохранены для аудита истории, но **не участвуют** в выборе текущего cutoff и не +сравнимы напрямую с focused benchmark. + +### Что было установлено старой методикой На Ryzen 9 5900X / MSVC 19.51 quick-tuning дал разные оптимумы, поэтому фиксированный `leaf=512` отвергнут как итоговая стратегия. Для baseline @@ -26,7 +69,7 @@ доказательство, а сигнал, что старую таблицу leaf нужно заново проверить на одинаковом quick holdout с тремя профилями. -## Hysteresis и итоговый quick-checkpoint +### Hysteresis и итоговый quick-checkpoint После v13 выполнены три контрольные серии: @@ -138,7 +181,7 @@ adaptive-deferred sequential `uint32` найден устойчивый AVX2 cod 1.85–1.98x относительно baseline во всех пяти повторах при верных checksums. Он требует отдельного анализа generated code, а не постфактум объяснения SIMD. -## Large-scale sweep до 100 миллионов элементов +### Large-scale sweep до 100 миллионов элементов Серии [`large v1`](../results/benchmarks/20260811-large-full-12c-v1/README.md) и [`seed-jittered v2`](../results/benchmarks/20260811-large-n100m-512edit-12c-v2/README.md) @@ -160,7 +203,7 @@ fixed oracle: до первого перехода он успевал выпо делят L3/DRAM, а N>1M имеет один repeat на профиль. Поэтому SIMD-профили сохранены отдельно, но точный AVX2 speedup по этой серии не утверждается. -## Чего пока нельзя утверждать +### Чего нельзя было утверждать по retired-сериям - Не доказано, что adaptive implementation уже имеет geometric mean > 1 против лучшего фиксированного контейнера на независимом full holdout. @@ -183,7 +226,7 @@ fixed oracle: до первого перехода он успевал выпо зафиксированной версии компилятора project-novec, но не публичный контракт MSVC и не доказательство отсутствия SIMD внутри CRT или x64 runtime. -## Следующая исследовательская граница +### Бывшая исследовательская граница Phase-aware hysteresis завершён и прошёл трёхпрофильный quick-validator. Следующий этап — независимый full holdout с несколькими seeds, affinity/pinning diff --git a/include/universal_container/adaptation_policy.hpp b/include/universal_container/adaptation_policy.hpp index 6d24c31..6fa005e 100644 --- a/include/universal_container/adaptation_policy.hpp +++ b/include/universal_container/adaptation_policy.hpp @@ -151,6 +151,89 @@ struct AdaptationTelemetry { std::size_t last_evidence_windows = 0; }; +// The production policy is deliberately small. It does not try to infer a +// workload phase from every operation; AdaptiveSequence consults these limits +// only when its logical capacity grows or shrinks. The crossover value is a +// calibratable constant and is updated from the focused benchmark rather than +// from the retired benchmark matrix. +struct ResizePolicyConfig { + std::size_t minimum_tiered_size = 4 * 1024; + std::size_t shrink_denominator = 8; // shrink at 12.5% occupancy +}; + +class ResizePolicy { +public: + explicit ResizePolicy(ResizePolicyConfig config = {}, TieredConfig tiered = {}) noexcept + : config_(normalize(config)), active_tiered_(normalize_tiered(tiered)) {} + + void set_tiered_config(TieredConfig tiered) noexcept { + active_tiered_ = normalize_tiered(tiered); + } + + // Kept as no-op compatibility hooks so a custom CostModelPolicy can still + // be substituted for experiments without putting policy work on the + // default read/edit paths. + void observe(const OperationSample&) noexcept {} + [[nodiscard]] bool decision_ready() const noexcept { return false; } + [[nodiscard]] std::optional + recommended_decision(StorageMode, std::size_t, TieredConfig) noexcept { + return std::nullopt; + } + + void on_transition(StorageMode from, + StorageMode to, + TieredConfig active_config) noexcept { + const auto normalized = normalize_tiered(active_config); + if (from == StorageMode::vector && to == StorageMode::tiered) { + ++telemetry_.vector_to_tiered; + } else if (from == StorageMode::tiered && to == StorageMode::vector) { + ++telemetry_.tiered_to_vector; + } else if (from == StorageMode::tiered && to == StorageMode::tiered) { + ++telemetry_.tiered_rebuilds; + if (normalized.leaf_capacity == active_tiered_.leaf_capacity) { + ++telemetry_.tiered_directory_rebuilds; + } else { + ++telemetry_.tiered_leaf_rebuilds; + } + } + active_tiered_ = normalized; + } + + void on_transition(StorageMode from, StorageMode to) noexcept { + on_transition(from, to, active_tiered_); + } + + void reset() noexcept { telemetry_ = {}; } + + [[nodiscard]] const ResizePolicyConfig& config() const noexcept { return config_; } + [[nodiscard]] const AdaptationTelemetry& telemetry() const noexcept { + return telemetry_; + } + +private: + static ResizePolicyConfig normalize(ResizePolicyConfig config) noexcept { + config.minimum_tiered_size = std::max(1, + config.minimum_tiered_size); + config.shrink_denominator = std::max(2, + config.shrink_denominator); + return config; + } + + static TieredConfig normalize_tiered(TieredConfig config) noexcept { + config.leaf_capacity = std::clamp( + config.leaf_capacity, 4, 1U << 20U); + config.directory_fanout = std::clamp( + config.directory_fanout, 2, 1U << 16U); + config.directory_levels = std::clamp( + config.directory_levels, 1, 8); + return config; + } + + ResizePolicyConfig config_{}; + TieredConfig active_tiered_{}; + AdaptationTelemetry telemetry_{}; +}; + class CostModelPolicy { public: static constexpr std::size_t candidate_count = 5; diff --git a/include/universal_container/adaptive_sequence.hpp b/include/universal_container/adaptive_sequence.hpp index 76019d7..9655a02 100644 --- a/include/universal_container/adaptive_sequence.hpp +++ b/include/universal_container/adaptive_sequence.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,7 @@ namespace uc { template , class Equal = std::equal_to> class AdaptiveSequence { @@ -54,8 +55,10 @@ private: struct IdMetadata { internal_id previous = invalid_internal_id; internal_id next = invalid_internal_id; + internal_id free_next = invalid_internal_id; std::uint32_t primary = 0; // vector index or stable leaf id std::uint32_t secondary = 0; // local leaf index + std::uint32_t generation = 0; std::uint8_t flags = 0; }; @@ -65,6 +68,7 @@ private: struct IndexedState { detail::FlatDuplicateIndex values; std::vector ids; + internal_id free_head = invalid_internal_id; }; struct NoIndexState {}; @@ -237,9 +241,53 @@ public: } AdaptiveSequence(const AdaptiveSequence&) = default; - AdaptiveSequence(AdaptiveSequence&&) noexcept = default; - AdaptiveSequence& operator=(const AdaptiveSequence&) = default; - AdaptiveSequence& operator=(AdaptiveSequence&&) noexcept = default; + + AdaptiveSequence(AdaptiveSequence&& other) noexcept + : tiered_config_(std::move(other.tiered_config_)), + policy_(std::move(other.policy_)), + storage_(std::move(other.storage_)), + index_state_(std::move(other.index_state_)), + equal_(std::move(other.equal_)), + residency_(other.residency_), + read_adaptation_(other.read_adaptation_), + logical_capacity_(other.logical_capacity_), + read_sample_rate_(other.read_sample_rate_), + read_sample_countdown_(other.read_sample_countdown_), + edit_sample_rate_(other.edit_sample_rate_), + edit_sample_countdown_(other.edit_sample_countdown_), + generation_(other.generation_) { + other.reset_after_move(); + } + + AdaptiveSequence& operator=(const AdaptiveSequence& other) { + if (this != &other) { + AdaptiveSequence replacement(other); + *this = std::move(replacement); + } + return *this; + } + + AdaptiveSequence& operator=(AdaptiveSequence&& other) noexcept { + if (this == &other) { + return *this; + } + const auto invalidated_generation = generation_ + 1; + tiered_config_ = std::move(other.tiered_config_); + policy_ = std::move(other.policy_); + storage_ = std::move(other.storage_); + index_state_ = std::move(other.index_state_); + equal_ = std::move(other.equal_); + residency_ = other.residency_; + read_adaptation_ = other.read_adaptation_; + logical_capacity_ = other.logical_capacity_; + read_sample_rate_ = other.read_sample_rate_; + read_sample_countdown_ = other.read_sample_countdown_; + edit_sample_rate_ = other.edit_sample_rate_; + edit_sample_countdown_ = other.edit_sample_countdown_; + generation_ = invalidated_generation; + other.reset_after_move(); + return *this; + } ~AdaptiveSequence() = default; [[nodiscard]] size_type size() const noexcept { @@ -249,6 +297,10 @@ public: } [[nodiscard]] bool empty() const noexcept { return size() == 0; } + // Logical capacity is shared by both backends. It is the only trigger for + // automatic representation/geometry changes; TieredStorage's internal + // leaf slack is intentionally not exposed as container capacity. + [[nodiscard]] size_type capacity() const noexcept { return logical_capacity_; } [[nodiscard]] StorageMode mode() const noexcept { return std::holds_alternative(storage_) ? StorageMode::vector @@ -258,13 +310,7 @@ public: [[nodiscard]] const TieredConfig& tiered_config() const noexcept { return tiered_config_; } reference operator[](size_type index) { - if (read_adaptation_ == ReadAdaptationMode::eager_nonconst) { - apply_pending_adaptation(); - } observe_random_read(); - if (read_adaptation_ == ReadAdaptationMode::eager_nonconst) { - apply_pending_adaptation(); - } if constexpr (HashIndexEnabled) { return reference_proxy(*this, index); } else { @@ -295,7 +341,6 @@ public: void erase(size_type index) { check_index(index); - apply_pending_adaptation(); const auto old_size = size(); const auto id = record_id(record_at_unchecked(index)); if constexpr (HashIndexEnabled) { @@ -311,13 +356,11 @@ public: (void)std::get(storage_).erase(index, relocate); } if constexpr (HashIndexEnabled) { - auto& metadata = index_state_.ids[static_cast(id)]; - metadata.flags &= static_cast(~alive_flag); - metadata.previous = invalid_internal_id; - metadata.next = invalid_internal_id; + release_id_slot(id); } ++generation_; observe_structural_edit(OperationKind::erase, old_size, index); + shrink_after_erase(); } void set(size_type index, const T& value) { set_impl(index, T(value)); } @@ -325,14 +368,11 @@ public: void clear() { if constexpr (HashIndexEnabled) { - for (auto& metadata : index_state_.ids) { - metadata.flags &= static_cast(~alive_flag); - metadata.previous = invalid_internal_id; - metadata.next = invalid_internal_id; - } index_state_.values.clear(); + rebuild_free_id_list(); } storage_.template emplace(); + logical_capacity_ = 0; residency_ = ResidencyMode::automatic; policy_.reset(); read_sample_countdown_ = read_sample_rate_; @@ -342,9 +382,12 @@ public: void reserve(size_type capacity) { force_vector_mode(); - std::get(storage_).reserve(capacity); + const auto requested = std::max(capacity, size()); + std::get(storage_).reserve(requested); + logical_capacity_ = std::max(logical_capacity_, requested); if constexpr (HashIndexEnabled) { - const auto additional = capacity > size() ? capacity - size() : 0; + index_state_.values.reserve_for_elements(logical_capacity_); + const auto additional = requested > size() ? requested - size() : 0; index_state_.ids.reserve(index_state_.ids.size() + additional); } ++generation_; @@ -364,14 +407,23 @@ public: if (!entry || entry->head == invalid_internal_id) { return std::nullopt; } - return resolve_id(entry->head); + return resolve_internal_id(entry->head); } else { - for (size_type i = 0; i < size(); ++i) { - if (equal_(value_at_unchecked(i), value)) { - return i; + if (mode() == StorageMode::vector) { + const auto& values = std::get(storage_); + for (size_type i = 0; i < values.size(); ++i) { + if (equal_(record_value(values[i]), value)) { + return i; + } } + return std::nullopt; } - return std::nullopt; + const auto found = std::get(storage_).find_if( + [&](const record_type& record) { + return equal_(record_value(record), value); + }); + return found == tiered_storage::npos + ? std::nullopt : std::optional(found); } } @@ -385,15 +437,26 @@ public: result.reserve(entry->count); auto id = entry->head; while (id != invalid_internal_id) { - result.push_back(resolve_id(id)); + result.push_back(resolve_internal_id(id)); id = index_state_.ids[static_cast(id)].next; } std::sort(result.begin(), result.end()); } else { - for (size_type i = 0; i < size(); ++i) { - if (equal_(value_at_unchecked(i), value)) { - result.push_back(i); + if (mode() == StorageMode::vector) { + const auto& values = std::get(storage_); + for (size_type i = 0; i < values.size(); ++i) { + if (equal_(record_value(values[i]), value)) { + result.push_back(i); + } } + } else { + std::get(storage_).for_each_match( + [&](const record_type& record) { + return equal_(record_value(record), value); + }, + [&](size_type index, const record_type&) { + result.push_back(index); + }); } } return result; @@ -411,7 +474,7 @@ public: result.reserve(entry->count); auto id = entry->head; while (id != invalid_internal_id) { - result.push_back(static_cast(id)); + result.push_back(stable_id_for_slot(id)); id = index_state_.ids[static_cast(id)].next; } return result; @@ -432,11 +495,11 @@ public: if (!entry) { return 0; } - std::vector ids; + std::vector ids; ids.reserve(entry->count); auto id = entry->head; while (id != invalid_internal_id) { - ids.push_back(id); + ids.push_back(stable_id_for_slot(id)); id = index_state_.ids[static_cast(id)].next; } for (const auto current : ids) { @@ -460,14 +523,22 @@ public: [[nodiscard]] stable_id id_at(size_type index) const requires(HashIndexEnabled) { check_index(index); - return static_cast(record_at_unchecked(index).id); + return stable_id_for_slot(record_at_unchecked(index).id); } [[nodiscard]] bool id_alive(stable_id id) const noexcept requires(HashIndexEnabled) { - return id <= static_cast(std::numeric_limits::max()) - && id < index_state_.ids.size() - && (index_state_.ids[static_cast(id)].flags & alive_flag) != 0; + if (id == invalid_id) { + return false; + } + const auto slot = slot_from_stable_id(id); + if (slot == invalid_internal_id + || static_cast(slot) >= index_state_.ids.size()) { + return false; + } + const auto& metadata = index_state_.ids[static_cast(slot)]; + return metadata.generation == generation_from_stable_id(id) + && (metadata.flags & alive_flag) != 0; } void erase_by_id(stable_id id) @@ -532,12 +603,11 @@ public: return read_adaptation_; } - bool adapt_now() { - if (residency_ != ResidencyMode::automatic) { - return false; - } - return apply_pending_adaptation(); - } + // Automatic rebuilds are capacity-boundary operations. This compatibility + // hook therefore never changes the representation between two resize + // events; explicit force_* calls remain available for callers that need an + // immediate manual conversion. + bool adapt_now() noexcept { return false; } [[nodiscard]] T* data() noexcept requires(!HashIndexEnabled) { @@ -625,10 +695,29 @@ public: return bytes; } + [[nodiscard]] std::size_t hash_bucket_count() const noexcept + requires(HashIndexEnabled) { + return index_state_.values.bucket_count(); + } + [[nodiscard]] AdaptationPolicy& policy() noexcept { return policy_; } [[nodiscard]] const AdaptationPolicy& policy() const noexcept { return policy_; } private: + void reset_after_move() noexcept { + storage_.template emplace(); + if constexpr (HashIndexEnabled) { + index_state_.values.reset_after_move(); + index_state_.ids.clear(); + index_state_.free_head = invalid_internal_id; + } + residency_ = ResidencyMode::automatic; + logical_capacity_ = 0; + read_sample_countdown_ = read_sample_rate_; + edit_sample_countdown_ = edit_sample_rate_; + ++generation_; + } + static void set_location(IdMetadata& metadata, bool tiered, std::uint32_t primary, @@ -667,14 +756,88 @@ private: } } + static constexpr unsigned stable_id_slot_bits = + std::numeric_limits::digits; + static_assert(stable_id_slot_bits == 32); + + [[nodiscard]] static internal_id slot_from_stable_id(stable_id id) noexcept { + return static_cast(id); + } + + [[nodiscard]] static std::uint32_t generation_from_stable_id( + stable_id id) noexcept { + return static_cast(id >> stable_id_slot_bits); + } + + [[nodiscard]] stable_id stable_id_for_slot(internal_id slot) const noexcept + requires(HashIndexEnabled) { + const auto generation = + index_state_.ids[static_cast(slot)].generation; + return (static_cast(generation) << stable_id_slot_bits) + | static_cast(slot); + } + + [[nodiscard]] internal_id acquire_id_slot() + requires(HashIndexEnabled) { + if (index_state_.free_head != invalid_internal_id) { + const auto id = index_state_.free_head; + auto& metadata = index_state_.ids[static_cast(id)]; + index_state_.free_head = metadata.free_next; + assert(metadata.generation + < std::numeric_limits::max()); + const auto next_generation = metadata.generation + 1; + metadata = {}; + metadata.generation = next_generation; + return id; + } + + if (index_state_.ids.size() >= static_cast(invalid_internal_id)) { + throw std::length_error("stable id slot space exhausted"); + } + const auto id = static_cast(index_state_.ids.size()); + index_state_.ids.emplace_back(); + return id; + } + + void release_id_slot(internal_id id) noexcept + requires(HashIndexEnabled) { + assert(id != invalid_internal_id); + assert(static_cast(id) < index_state_.ids.size()); + auto& metadata = index_state_.ids[static_cast(id)]; + const auto generation = metadata.generation; + metadata = {}; + metadata.generation = generation; + if (generation != std::numeric_limits::max()) { + metadata.free_next = index_state_.free_head; + index_state_.free_head = id; + } + } + + void rebuild_free_id_list() noexcept + requires(HashIndexEnabled) { + index_state_.free_head = invalid_internal_id; + for (size_type index = index_state_.ids.size(); index != 0; --index) { + const auto id = static_cast(index - 1); + auto& metadata = index_state_.ids[index - 1]; + const auto generation = metadata.generation; + metadata = {}; + metadata.generation = generation; + if (generation != std::numeric_limits::max()) { + metadata.free_next = index_state_.free_head; + index_state_.free_head = id; + } + } + } + record_type make_record(T value) { if constexpr (HashIndexEnabled) { - if (index_state_.ids.size() >= static_cast(invalid_internal_id)) { - throw std::length_error("stable id space exhausted"); + const auto id = acquire_id_slot(); + try { + return IndexedRecord{std::move(value), id}; + } catch (...) { + release_id_slot(id); + throw; } - const auto id = static_cast(index_state_.ids.size()); - index_state_.ids.emplace_back(); - return IndexedRecord{std::move(value), id}; } else { return value; } @@ -707,23 +870,40 @@ private: } void push_back_impl(T value) { - apply_pending_adaptation(); const auto old_size = size(); + grow_before_insert(old_size + 1); auto record = make_record(std::move(value)); const auto id = record_id(record); auto relocate = relocation_callback(); - if (mode() == StorageMode::vector) { - auto& values = std::get(storage_); - values.push_back(std::move(record)); - refresh_vector_locations(old_size); + if constexpr (HashIndexEnabled) { + try { + // Keep an unmoved lookup key so a failed storage insertion can + // roll the duplicate chain back before the slot is recycled. + T rollback_key(record.value); + link_value(record.value, id); + try { + if (mode() == StorageMode::vector) { + auto& values = std::get(storage_); + values.push_back(std::move(record)); + refresh_vector_locations(old_size); + } else { + std::get(storage_).push_back( + std::move(record), relocate); + } + } catch (...) { + unlink_value(rollback_key, id); + throw; + } + index_state_.ids[static_cast(id)].flags |= alive_flag; + } catch (...) { + release_id_slot(id); + throw; + } + } else if (mode() == StorageMode::vector) { + std::get(storage_).push_back(std::move(record)); } else { std::get(storage_).push_back(std::move(record), relocate); } - if constexpr (HashIndexEnabled) { - auto& metadata = index_state_.ids[static_cast(id)]; - metadata.flags |= alive_flag; - link_value(value_at_unchecked(old_size), id); - } ++generation_; policy_.observe({OperationKind::append, old_size, old_size, 1, sizeof(T)}); } @@ -732,23 +912,41 @@ private: if (index > size()) { throw std::out_of_range("AdaptiveSequence insertion index out of range"); } - apply_pending_adaptation(); const auto old_size = size(); + grow_before_insert(old_size + 1); auto record = make_record(std::move(value)); const auto id = record_id(record); auto relocate = relocation_callback(); - if (mode() == StorageMode::vector) { + if constexpr (HashIndexEnabled) { + try { + T rollback_key(record.value); + link_value(record.value, id); + try { + if (mode() == StorageMode::vector) { + auto& values = std::get(storage_); + values.insert(values.begin() + static_cast(index), + std::move(record)); + refresh_vector_locations(index); + } else { + std::get(storage_).insert( + index, std::move(record), relocate); + } + } catch (...) { + unlink_value(rollback_key, id); + throw; + } + index_state_.ids[static_cast(id)].flags |= alive_flag; + } catch (...) { + release_id_slot(id); + throw; + } + } else if (mode() == StorageMode::vector) { auto& values = std::get(storage_); - values.insert(values.begin() + static_cast(index), std::move(record)); - refresh_vector_locations(index); + values.insert(values.begin() + static_cast(index), + std::move(record)); } else { std::get(storage_).insert(index, std::move(record), relocate); } - if constexpr (HashIndexEnabled) { - auto& metadata = index_state_.ids[static_cast(id)]; - metadata.flags |= alive_flag; - link_value(value_at_unchecked(index), id); - } ++generation_; observe_structural_edit(OperationKind::insert, old_size, index); } @@ -837,13 +1035,28 @@ private: [[nodiscard]] size_type resolve_id(stable_id id) const requires(HashIndexEnabled) { - if (id >= index_state_.ids.size()) { + if (id == invalid_id) { throw std::out_of_range("unknown stable id"); } - const auto& metadata = index_state_.ids[static_cast(id)]; - if ((metadata.flags & alive_flag) == 0) { + const auto slot = slot_from_stable_id(id); + if (slot == invalid_internal_id + || static_cast(slot) >= index_state_.ids.size()) { + throw std::out_of_range("unknown stable id"); + } + const auto& metadata = index_state_.ids[static_cast(slot)]; + if (metadata.generation != generation_from_stable_id(id) + || (metadata.flags & alive_flag) == 0) { throw std::out_of_range("stable id no longer refers to an element"); } + return resolve_internal_id(slot); + } + + [[nodiscard]] size_type resolve_internal_id(internal_id id) const noexcept + requires(HashIndexEnabled) { + assert(id != invalid_internal_id); + assert(static_cast(id) < index_state_.ids.size()); + const auto& metadata = index_state_.ids[static_cast(id)]; + assert((metadata.flags & alive_flag) != 0); if ((metadata.flags & tiered_flag) == 0) { return metadata.primary; } @@ -852,51 +1065,171 @@ private: metadata.secondary); } - bool apply_pending_adaptation() { - if (residency_ != ResidencyMode::automatic || !policy_.decision_ready()) { - return false; + [[nodiscard]] static size_type ceil_sqrt(size_type value) noexcept { + if (value <= 1) { + return value; + } + auto root = static_cast(std::sqrt(static_cast(value))); + while (root > value / root) { + --root; + } + while (root < value / root + || (root <= value / root && root * root < value)) { + ++root; + } + return root; + } + + [[nodiscard]] size_type minimum_tiered_size() const noexcept { + if constexpr (requires(const AdaptationPolicy& p) { + p.config().minimum_tiered_size; + }) { + return std::max(1, policy_.config().minimum_tiered_size); + } else { + return 4 * 1024; + } + } + + [[nodiscard]] size_type shrink_denominator() const noexcept { + if constexpr (requires(const AdaptationPolicy& p) { + p.config().shrink_denominator; + }) { + return std::max(2, policy_.config().shrink_denominator); + } else { + return 8; + } + } + + [[nodiscard]] StorageMode automatic_mode_for(size_type element_count) const noexcept { + return element_count >= minimum_tiered_size() + ? StorageMode::tiered : StorageMode::vector; + } + + [[nodiscard]] TieredConfig geometry_for(size_type element_count) const noexcept { + auto result = tiered_config_; + result.leaf_capacity = std::max(4, ceil_sqrt(element_count)); + return result; + } + + void grow_before_insert(size_type required) { + if (required <= logical_capacity_) { + return; + } + size_type next = logical_capacity_ == 0 ? 1 : logical_capacity_; + while (next < required) { + if (next > std::numeric_limits::max() / 2) { + next = required; + break; + } + next *= 2; + } + rebuild_for_capacity(next, required); + } + + void shrink_after_erase() noexcept { + if (logical_capacity_ <= 1 + || size() > logical_capacity_ / shrink_denominator()) { + return; + } + const auto next = std::max(size(), logical_capacity_ / 2); + // The erase has already committed. Capacity shrink is an automatic + // optimization and must not turn that successful erase into a thrown + // operation. Copyable records keep their source intact on failure; + // nothrow-movable records use preallocated rebuild paths below. + if constexpr (std::is_copy_constructible_v + || std::is_nothrow_move_constructible_v) { + try { + rebuild_for_capacity(next, size()); + } catch (...) { + // Keep the current (or already committed) capacity/layout and + // retry at a later resize boundary. + } + } + } + + void rebuild_for_capacity(size_type target_capacity, size_type target_size) { + target_capacity = std::max(target_capacity, target_size); + const bool growing = target_capacity >= logical_capacity_; + if constexpr (HashIndexEnabled) { + if (growing) { + index_state_.values.reserve_for_elements(target_capacity); + index_state_.ids.reserve( + std::max(index_state_.ids.size(), target_capacity)); + } + } + auto target_mode = mode(); + if (residency_ == ResidencyMode::forced_vector) { + target_mode = StorageMode::vector; + } else if (residency_ == ResidencyMode::forced_tiered) { + target_mode = StorageMode::tiered; + } else { + target_mode = automatic_mode_for(target_size); } - if constexpr (requires(AdaptationPolicy& p, StorageMode current, - size_type count, TieredConfig config) { - p.recommended_decision(current, count, config); - }) { - const auto recommendation = - policy_.recommended_decision(mode(), size(), tiered_config_); - if (!recommendation) { - return false; - } - if (recommendation->target == mode()) { - if (mode() == StorageMode::tiered - && recommendation->tiered_config != tiered_config_) { - reconfigure_tiered(recommendation->tiered_config); - return true; + if (target_mode == StorageMode::vector) { + vector_storage rebuilt; + rebuilt.reserve(target_capacity); + if (mode() == StorageMode::vector) { + auto& source = std::get(storage_); + if constexpr (std::is_copy_constructible_v) { + rebuilt.insert(rebuilt.end(), source.begin(), source.end()); + } else { + rebuilt.insert(rebuilt.end(), + std::make_move_iterator(source.begin()), + std::make_move_iterator(source.end())); } - return false; + } else if constexpr (std::is_copy_constructible_v) { + const auto& source = std::get(storage_); + source.for_each([&](const record_type& record) { + rebuilt.push_back(record); + }); + } else { + auto& source = std::get(storage_); + source.for_each([&](record_type& record) { + rebuilt.push_back(std::move(record)); + }); } - if (recommendation->target == StorageMode::tiered) { - const auto previous_config = tiered_config_; - tiered_config_ = recommendation->tiered_config; - try { - convert_to(recommendation->target); - } catch (...) { - // The recommended shape is policy state, not committed - // container state. A failed allocation/copy must not make - // tiered_config() describe storage that was never built. - tiered_config_ = previous_config; - throw; - } - return true; + + const auto source_mode = mode(); + storage_.template emplace(std::move(rebuilt)); + logical_capacity_ = target_capacity; + refresh_vector_locations(0); + ++generation_; + if (source_mode != StorageMode::vector) { + notify_policy_transition(source_mode, StorageMode::vector); } - convert_to(recommendation->target); - return true; } else { - const auto recommendation = policy_.recommended_mode(mode(), size()); - if (!recommendation || *recommendation == mode()) { - return false; + const auto target_config = geometry_for(target_size); + tiered_storage rebuilt; + if (mode() == StorageMode::vector) { + if constexpr (std::is_copy_constructible_v) { + rebuilt = tiered_storage::from_vector_copy( + std::get(storage_), target_config); + } else { + rebuilt = tiered_storage::from_vector( + std::move(std::get(storage_)), target_config); + } + } else if constexpr (std::is_copy_constructible_v) { + rebuilt = tiered_storage::reconfigured_copy( + std::get(storage_), target_config); + } else { + rebuilt = tiered_storage::reconfigured_move( + std::get(storage_), target_config); + } + + const auto source_mode = mode(); + tiered_config_ = rebuilt.config(); + storage_.template emplace(std::move(rebuilt)); + logical_capacity_ = target_capacity; + refresh_tiered_locations(); + ++generation_; + notify_policy_transition(source_mode, StorageMode::tiered); + } + + if constexpr (HashIndexEnabled) { + if (!growing) { + index_state_.values.reserve_for_elements(target_capacity); } - convert_to(*recommendation); - return true; } } @@ -911,8 +1244,8 @@ private: tiered = tiered_storage::from_vector_copy( std::get(storage_), tiered_config_); } else { - auto values = std::move(std::get(storage_)); - tiered = tiered_storage::from_vector(std::move(values), tiered_config_); + tiered = tiered_storage::from_vector( + std::move(std::get(storage_)), tiered_config_); } tiered_config_ = tiered.config(); storage_.template emplace(std::move(tiered)); @@ -924,6 +1257,9 @@ private: } else { values = std::get(storage_).to_vector_move(); } + if (logical_capacity_ > values.capacity()) { + values.reserve(logical_capacity_); + } storage_.template emplace(std::move(values)); refresh_vector_locations(0); } @@ -955,8 +1291,8 @@ private: rebuilt = tiered_storage::reconfigured_copy( std::get(storage_), target_config); } else { - auto values = std::get(storage_).to_vector_move(); - rebuilt = tiered_storage::from_vector(std::move(values), target_config); + rebuilt = tiered_storage::reconfigured_move( + std::get(storage_), target_config); } tiered_config_ = rebuilt.config(); storage_.template emplace(std::move(rebuilt)); @@ -1028,6 +1364,7 @@ private: [[no_unique_address]] Equal equal_{}; ResidencyMode residency_ = ResidencyMode::automatic; ReadAdaptationMode read_adaptation_ = ReadAdaptationMode::deferred; + size_type logical_capacity_ = 0; std::size_t read_sample_rate_ = 256; mutable std::size_t read_sample_countdown_ = 256; std::size_t edit_sample_rate_ = 1; diff --git a/include/universal_container/flat_hash_index.hpp b/include/universal_container/flat_hash_index.hpp index 2b180dd..95ded67 100644 --- a/include/universal_container/flat_hash_index.hpp +++ b/include/universal_container/flat_hash_index.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -38,14 +39,39 @@ public: } Entry& ensure(const Key& key) { - maybe_grow(); - const auto [position, found] = find_insert_position(key); + // A moved-from std::vector is allowed to be empty while the scalar + // counters retain their old values. Rebuild lazily so a moved-from + // index remains reusable and mask() is never evaluated for zero + // buckets. + if (buckets_.empty()) { + rehash(16); + } + + auto [position, found] = find_insert_position(key); + if (position == npos) { + // Normal growth is performed by reserve_for_elements() at a + // logical container-capacity change. Reaching a completely full + // table means that invariant was unavailable (most notably after + // reusing a moved-from standalone index), so grow only as a + // recovery path rather than as a load-factor policy. + if (buckets_.size() > std::numeric_limits::max() / 2) { + throw std::length_error("flat hash index capacity overflow"); + } + rehash(buckets_.size() * 2); + const auto retry = find_insert_position(key); + position = retry.first; + found = retry.second; + if (position == npos) { + throw std::length_error("flat hash index is full"); + } + } + auto& bucket = buckets_[position]; if (!found) { + bucket.key.emplace(key); if (bucket.state == State::tombstone) { --tombstones_; } - bucket.key.emplace(key); bucket.entry = {}; bucket.state = State::occupied; ++size_; @@ -58,27 +84,62 @@ public: if (position == npos) { return false; } - auto& bucket = buckets_[position]; - bucket.key.reset(); - bucket.entry = {}; - bucket.state = State::tombstone; - --size_; - ++tombstones_; - if (tombstones_ > buckets_.size() / 4) { - rehash(buckets_.size()); + if constexpr (std::is_nothrow_invocable_v + && std::is_nothrow_move_assignable_v) { + erase_and_compact(position); + } else { + // Computing hashes or moving a key can throw. Rebuild a complete + // replacement without the erased key, then commit with swap, so a + // failed erase never opens a hole in the live probe chain. + erase_transactional(position); } + --size_; return true; } void clear() { + // Allocate before committing so a failed reset leaves the live table + // and its duplicate metadata untouched. + std::vector replacement(16); + buckets_.swap(replacement); + size_ = 0; + tombstones_ = 0; + } + + // Used by the owning container after moving the live table elsewhere. + // Keeping zero buckets avoids an allocation in its noexcept move path; + // ensure()/reserve_for_elements() restore the minimum table lazily. + void reset_after_move() noexcept { buckets_.clear(); size_ = 0; tombstones_ = 0; - rehash(16); } [[nodiscard]] std::size_t distinct_values() const noexcept { return size_; } [[nodiscard]] std::size_t bucket_count() const noexcept { return buckets_.size(); } + [[nodiscard]] static std::size_t buckets_for_elements( + std::size_t element_capacity) { + // At most one distinct key per element. Keeping the worst-case load + // below 70% guarantees that ensure() cannot trigger an independent + // rehash between container capacity changes. + const auto required = element_capacity > (std::numeric_limits::max() - 6) / 10 + ? std::numeric_limits::max() + : (element_capacity * 10 + 6) / 7; + std::size_t buckets = 16; + while (buckets < required) { + if (buckets > std::numeric_limits::max() / 2) { + throw std::length_error("flat hash index capacity overflow"); + } + buckets *= 2; + } + return buckets; + } + void reserve_for_elements(std::size_t element_capacity) { + const auto requested = buckets_for_elements(element_capacity); + if (requested != buckets_.size() || tombstones_ != 0) { + rehash(requested); + } + } [[nodiscard]] std::size_t allocated_bytes() const noexcept { return buckets_.capacity() * sizeof(Bucket); } @@ -116,10 +177,20 @@ private: [[nodiscard]] std::pair find_insert_position(const Key& key) const { - auto position = hasher_(key) & mask(); + return find_insert_position_in(buckets_, key); + } + + [[nodiscard]] std::pair + find_insert_position_in(const std::vector& buckets, + const Key& key) const { + if (buckets.empty()) { + return {npos, false}; + } + const auto table_mask = buckets.size() - 1; + auto position = hasher_(key) & table_mask; auto first_tombstone = npos; - for (std::size_t probe = 0; probe < buckets_.size(); ++probe) { - const auto& bucket = buckets_[position]; + for (std::size_t probe = 0; probe < buckets.size(); ++probe) { + const auto& bucket = buckets[position]; if (bucket.state == State::empty) { return {first_tombstone == npos ? position : first_tombstone, false}; } @@ -130,41 +201,92 @@ private: } else if (equal_(*bucket.key, key)) { return {position, true}; } - position = (position + 1) & mask(); + position = (position + 1) & table_mask; } if (first_tombstone != npos) { return {first_tombstone, false}; } - throw std::length_error("flat hash index is full"); + return {npos, false}; } - void maybe_grow() { - if ((size_ + tombstones_ + 1) * 100 >= buckets_.size() * 82) { - rehash(buckets_.size() * 2); + void erase_and_compact(std::size_t position) { + auto hole = position; + buckets_[hole] = Bucket{}; + auto current = (hole + 1) & mask(); + while (buckets_[current].state != State::empty) { + auto& bucket = buckets_[current]; + if (bucket.state == State::occupied) { + const auto home = hasher_(*bucket.key) & mask(); + const auto current_distance = (current - home) & mask(); + const auto hole_distance = (hole - home) & mask(); + if (hole_distance < current_distance) { + buckets_[hole] = std::move(bucket); + bucket = Bucket{}; + hole = current; + } + } + current = (current + 1) & mask(); } } + void erase_transactional(std::size_t erased_position) { + std::vector replacement(buckets_.size()); + for (std::size_t source = 0; source < buckets_.size(); ++source) { + const auto& bucket = buckets_[source]; + if (source == erased_position || bucket.state != State::occupied) { + continue; + } + const auto [position, found] = + find_insert_position_in(replacement, *bucket.key); + if (position == npos || found) { + throw std::logic_error("flat hash index rebuild invariant violated"); + } + auto& target = replacement[position]; + target.key.emplace(*bucket.key); + target.entry = bucket.entry; + target.state = State::occupied; + } + buckets_.swap(replacement); + tombstones_ = 0; + } + void rehash(std::size_t requested_capacity) { std::size_t capacity = 16; while (capacity < requested_capacity) { + if (capacity > std::numeric_limits::max() / 2) { + throw std::length_error("flat hash index capacity overflow"); + } capacity *= 2; } - auto old = std::move(buckets_); - buckets_.assign(capacity, Bucket{}); - size_ = 0; - tombstones_ = 0; - for (auto& bucket : old) { + + // Build the complete replacement before touching the live table. An + // allocation, hash/equality call, or copy of a copyable Key may throw; + // in all of those cases the original buckets and counters remain + // unchanged. vector::swap is the no-throw commit for std::allocator. + std::vector replacement(capacity); + std::size_t replacement_size = 0; + for (const auto& bucket : buckets_) { if (bucket.state != State::occupied) { continue; } - const auto [position, found] = find_insert_position(*bucket.key); - (void)found; - auto& target = buckets_[position]; - target.key.emplace(std::move(*bucket.key)); + const auto [position, found] = + find_insert_position_in(replacement, *bucket.key); + if (position == npos) { + throw std::length_error("flat hash index is full"); + } + if (found) { + throw std::logic_error("flat hash index contains duplicate keys"); + } + auto& target = replacement[position]; + target.key.emplace(*bucket.key); target.entry = bucket.entry; target.state = State::occupied; - ++size_; + ++replacement_size; } + + buckets_.swap(replacement); + size_ = replacement_size; + tombstones_ = 0; } Hash hasher_{}; diff --git a/include/universal_container/tiered_storage.hpp b/include/universal_container/tiered_storage.hpp index 851dff8..f176751 100644 --- a/include/universal_container/tiered_storage.hpp +++ b/include/universal_container/tiered_storage.hpp @@ -231,6 +231,31 @@ public: } } + template + [[nodiscard]] std::size_t find_if(Predicate&& predicate) const { + std::size_t logical = 0; + for (const auto& leaf : leaves_) { + for (std::size_t i = 0; i < leaf->values.size(); ++i, ++logical) { + if (predicate(leaf->values[i])) { + return logical; + } + } + } + return npos; + } + + template + void for_each_match(Predicate&& predicate, Function&& function) const { + std::size_t logical = 0; + for (const auto& leaf : leaves_) { + for (std::size_t i = 0; i < leaf->values.size(); ++i, ++logical) { + if (predicate(leaf->values[i])) { + function(logical, leaf->values[i]); + } + } + } + } + template void for_each_with_location(Function&& function) const { for (const auto& leaf : leaves_) { @@ -262,21 +287,22 @@ public: if (source.empty()) { return result; } - const auto target_occupancy = bulk_target_occupancy( - result.config_.leaf_capacity); - const auto count = (source.size() + target_occupancy - 1) - / target_occupancy; - result.leaves_.reserve(count); + // Allocate the complete destination, including its directory, before + // moving the first value. For nothrow-move T the following loop and + // the final TieredStorage move cannot fail, so an allocation failure + // leaves source completely untouched. + const auto target_occupancy = result.prepare_bulk_destination(source.size()); + std::size_t leaf_index = 0; for (auto& value : source) { - if (result.leaves_.empty() - || result.leaves_.back()->values.size() == target_occupancy) { - result.leaves_.push_back(result.make_leaf()); - } - result.leaves_.back()->values.push_back(std::move(value)); + auto& leaf = *result.leaves_[leaf_index]; + leaf.values.push_back(std::move(value)); ++result.size_; + if (leaf.values.size() == target_occupancy + && leaf_index + 1 < result.leaves_.size()) { + ++leaf_index; + } } - result.rebuild_positions(); - result.rebuild_directory(); + assert(result.directory_.total() == result.size_); for (std::size_t leaf = 0; leaf < result.leaves_.size(); ++leaf) { result.refresh_leaf(leaf, relocate); } @@ -335,6 +361,31 @@ public: return result; } + // Rebuild a tiered layout without an intermediate vector. All leaves and + // directory arrays are allocated first; consequently this operation has a + // strong allocation-failure guarantee when T is nothrow-move-constructible. + static TieredStorage reconfigured_move(TieredStorage& source, + TieredConfig config) { + TieredStorage result(config); + if (source.empty()) { + return result; + } + + const auto target_occupancy = result.prepare_bulk_destination(source.size()); + std::size_t leaf_index = 0; + source.for_each([&](T& value) { + auto& leaf = *result.leaves_[leaf_index]; + leaf.values.push_back(std::move(value)); + ++result.size_; + if (leaf.values.size() == target_occupancy + && leaf_index + 1 < result.leaves_.size()) { + ++leaf_index; + } + }); + assert(result.directory_.total() == result.size_); + return result; + } + [[nodiscard]] std::size_t logical_index(leaf_id_type leaf_id, std::size_t local) const { if (leaf_id >= leaf_positions_.size()) { @@ -387,6 +438,31 @@ private: return std::max(1, capacity - capacity / 8); } + // Construct the entire shape needed by a bulk load while source elements + // are still untouched. RingBlock allocates all of its optional slots in + // its constructor, so inserting into these leaves does not allocate. + [[nodiscard]] std::size_t prepare_bulk_destination(std::size_t element_count) { + assert(element_count != 0); + const auto target_occupancy = bulk_target_occupancy(config_.leaf_capacity); + const auto count = 1 + (element_count - 1) / target_occupancy; + + leaves_.reserve(count); + leaf_positions_.reserve(count); + std::vector expected_sizes; + expected_sizes.reserve(count); + + auto remaining = element_count; + for (std::size_t i = 0; i < count; ++i) { + leaves_.push_back(make_leaf()); + const auto leaf_size = std::min(remaining, target_occupancy); + expected_sizes.push_back(leaf_size); + remaining -= leaf_size; + } + rebuild_positions(); + directory_.rebuild(expected_sizes); + return target_occupancy; + } + [[nodiscard]] std::unique_ptr make_leaf() { if (next_leaf_id_ == std::numeric_limits::max()) { throw std::length_error("tiered leaf id space exhausted"); diff --git a/results/benchmarks/20260813-focused-resize/README.md b/results/benchmarks/20260813-focused-resize/README.md new file mode 100644 index 0000000..5effd0f --- /dev/null +++ b/results/benchmarks/20260813-focused-resize/README.md @@ -0,0 +1,82 @@ +# Focused resize benchmark — 2026-08-13 + +This run replaces the retired benchmark matrix with one size sweep and one +fixed workload. Every candidate is `AdaptiveSequence` and uses +the same flat hash index; only storage residency differs. + +Workload per mixed trace: + +- 97,000 random indexed reads (97%); +- 1,000 `find_one` calls through the flat hash index (1%, 500 hit / 500 miss); +- 1,000 random inserts (1%); +- 1,000 random erases (1%). + +Each cell is the median of seven paired repeats. Candidate order is rotated per +repeat. Construction is outside the timer; automatic conversion on a resize +boundary is inside it. The per-operation columns are separate batched traces, +while `mixed` is the requested end-to-end workload. + +Environment: AMD Ryzen 9 5900X (12 cores / 24 threads), Windows 11 Pro build +26200, MSVC 19.51.36248, x64 Release `/O2 /MT`, baseline ISA profile, no IPO/LTO. + +Command: + +```powershell +.\out\bin\baseline\Release\uc_focused_bench.exe ` + --operations 100000 --repeats 7 ` + --output results\benchmarks\20260813-focused-resize\baseline.csv +``` + +## Median throughput (operations/second) + +| n | container | mixed mode | mixed leaf | mixed ops/s | indexed read ops/s | hash find ops/s | insert ops/s | erase ops/s | +|---:|:----------|:-----------|-----------:|------------:|-------------------:|----------------:|-------------:|------------:| +| 256 | forced vector + hash | vector | 16 | 164,500,740 | 654,450,262 | 145,369,967 | 1,739,433 | 8,205,128 | +| 256 | forced tiered + hash | tiered | 17 | 41,291,601 | 54,182,922 | 81,479,671 | 2,459,420 | 5,203,252 | +| 256 | adaptive + hash | vector | 16 | 151,676,020 | 571,755,289 | 146,134,736 | 1,740,644 | 8,421,053 | +| 512 | forced vector + hash | vector | 23 | 103,412,616 | 657,030,223 | 129,584,035 | 1,358,880 | 5,014,691 | +| 512 | forced tiered + hash | tiered | 23 | 38,755,184 | 50,032,521 | 78,963,992 | 2,698,327 | 5,344,468 | +| 512 | adaptive + hash | vector | 23 | 98,328,417 | 575,373,993 | 129,098,890 | 1,300,221 | 5,256,674 | +| 1,024 | forced vector + hash | vector | 32 | 58,816,610 | 662,251,656 | 110,411,836 | 931,619 | 2,801,120 | +| 1,024 | forced tiered + hash | tiered | 33 | 35,664,610 | 46,172,315 | 71,118,697 | 3,144,654 | 4,748,338 | +| 1,024 | adaptive + hash | vector | 32 | 56,631,555 | 584,112,150 | 109,673,174 | 911,743 | 2,893,519 | +| 2,048 | forced vector + hash | vector | 46 | 31,979,533 | 654,878,847 | 81,426,594 | 570,125 | 973,615 | +| 2,048 | forced tiered + hash | tiered | 46 | 32,177,103 | 43,327,556 | 59,157,596 | 2,581,311 | 3,880,481 | +| 2,048 | adaptive + hash | vector | 46 | 31,330,284 | 580,383,053 | 81,406,708 | 565,835 | 995,619 | +| 4,096 | forced vector + hash | vector | 64 | 17,199,862 | 661,813,369 | 77,297,673 | 313,342 | 421,248 | +| 4,096 | forced tiered + hash | tiered | 65 | 32,526,672 | 45,460,745 | 55,303,617 | 1,793,079 | 3,138,732 | +| 4,096 | adaptive + hash | vector → tiered | 65 | 32,639,206 | 581,395,349 | 76,710,647 | 1,822,822 | 422,048 | +| 8,192 | forced vector + hash | vector | 91 | 8,791,518 | 657,894,737 | 72,385,089 | 164,395 | 198,736 | +| 8,192 | forced tiered + hash | tiered | 91 | 28,479,481 | 42,929,510 | 51,116,904 | 1,317,003 | 2,276,867 | +| 8,192 | adaptive + hash | tiered | 91 | 28,603,301 | 40,546,568 | 50,296,751 | 1,348,618 | 2,704,896 | +| 16,384 | forced vector + hash | vector | 128 | 4,489,580 | 649,350,649 | 79,478,620 | 86,972 | 96,230 | +| 16,384 | forced tiered + hash | tiered | 129 | 23,255,814 | 40,364,899 | 51,284,681 | 935,629 | 1,709,694 | +| 16,384 | adaptive + hash | tiered | 129 | 22,955,260 | 38,213,153 | 50,658,561 | 836,400 | 2,076,843 | +| 32,768 | forced vector + hash | vector | 182 | 2,246,076 | 610,873,549 | 79,687,625 | 43,018 | 47,784 | +| 32,768 | forced tiered + hash | tiered | 182 | 18,402,989 | 37,572,797 | 50,266,412 | 571,004 | 1,245,640 | +| 32,768 | adaptive + hash | tiered | 182 | 17,943,013 | 35,944,071 | 49,058,085 | 514,165 | 1,548,227 | +| 65,536 | forced vector + hash | vector | 256 | 1,089,099 | 490,436,488 | 63,678,044 | 21,507 | 22,895 | +| 65,536 | forced tiered + hash | tiered | 257 | 12,614,158 | 32,799,790 | 44,620,945 | 315,139 | 910,167 | +| 65,536 | adaptive + hash | tiered | 257 | 12,777,430 | 30,628,809 | 42,513,392 | 295,255 | 1,116,819 | + +## Crossover + +At `n=2,048`, forced tiered had only a 0.618% median advantage and won four of +seven paired repeats (forced vector won three), so this boundary is treated as +noise rather than a stable crossover. At `n=4,096`, forced tiered won all seven +paired repeats and its median throughput was 1.8911x that of forced vector. +The adaptive row includes the one-time `vector → tiered` conversion and still +reached 32.64M mixed ops/s, essentially matching already-tiered storage. +Therefore the calibrated resize-boundary cutoff is **4,096 elements**, the +first decisive and stable size in this sweep. + +At exactly 4,096 elements an adaptive container is still vector. The next +insert changes logical capacity from 4,096 to 8,192 and performs the conversion; +the final leaf is `ceil(sqrt(4097)) = 65`. In the separate read/find/erase +batches no growth occurs, so those adaptive 4,096 measurements remain vector; +the insert batch and mixed trace include the conversion. + +All 189 raw rows have the required operation counts, and checksums match across +the three candidates for every `(n, repeat)` pair. The CSV also records the +mode and leaf used by each separate read/find/insert/erase batch. Raw data is in +[`baseline.csv`](baseline.csv). diff --git a/results/benchmarks/20260813-focused-resize/baseline.csv b/results/benchmarks/20260813-focused-resize/baseline.csv new file mode 100644 index 0000000..2d05dc2 --- /dev/null +++ b/results/benchmarks/20260813-focused-resize/baseline.csv @@ -0,0 +1,190 @@ +profile,n,container,repeat,seed,initial_mode,mixed_mode,initial_leaf,mixed_leaf,initial_logical_capacity,logical_capacity,initial_hash_buckets,hash_buckets,operations,read_count,find_count,find_hit_count,find_miss_count,insert_count,erase_count,read_batch_count,find_batch_count,insert_batch_count,erase_batch_count,read_mode,read_leaf,find_mode,find_leaf,insert_mode,insert_leaf,erase_mode,erase_leaf,mixed_ops_per_sec,read_ops_per_sec,find_ops_per_sec,insert_ops_per_sec,erase_ops_per_sec,bytes,checksum +"baseline",256,"forced_vector_hash",0,11643429248495452671,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,167168171.18020728,654450261.78010476,148367952.52225518,1816200.5085361423,8101265.8227848094,39456,2516218684603091152 +"baseline",256,"forced_tiered_hash",0,11643429248495452671,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,42591251.756889135,54531573.781219326,82209799.408089444,2222716.159146477,4620938.628158845,43752,2516218684603091152 +"baseline",256,"adaptive_hash",0,11643429248495452671,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,158579130.98636219,583430571.76196039,147167034.58425313,1857010.2135561744,8258064.5161290318,39456,2516218684603091152 +"baseline",256,"forced_vector_hash",1,15449987037075203961,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,150172698.60339391,637755102.04081643,143636885.95231256,1678697.330871244,8050314.4654088048,39456,2515907659441309718 +"baseline",256,"forced_tiered_hash",1,15449987037075203961,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,41291601.288297959,52706477.62610025,80392314.494734302,2383790.2264600713,5192697.7687626779,44132,2515907659441309718 +"baseline",256,"adaptive_hash",1,15449987037075203961,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,149320591.30954158,567536889.89784336,143533802.21042055,1709109.5539224066,8126984.1269841269,39456,2515907659441309718 +"baseline",256,"forced_tiered_hash",2,18085065223655675095,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,39816842.524387822,53231129.564569362,78876794.447073683,2263980.0769753228,5203252.0325203249,43752,2516672311691009245 +"baseline",256,"adaptive_hash",2,18085065223655675095,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,153186274.50980392,557103064.06685233,138715494.52073798,1654533.421575116,8619528.6195286196,39456,2516672311691009245 +"baseline",256,"forced_vector_hash",2,18085065223655675095,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,169262017.60324982,660066006.60066009,143595634.69270533,1761804.0873854826,9045936.3957597166,39456,2516672311691009245 +"baseline",256,"adaptive_hash",3,9610393720456262672,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,151676020.02123463,571755288.73642087,146584579.30225742,1740644.0382941687,9078014.1843971629,39456,2516336871995962384 +"baseline",256,"forced_vector_hash",3,9610393720456262672,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,166002656.04249668,660501981.50594449,147623265.42663124,1699235.344095157,8476821.1920529809,39456,2516336871995962384 +"baseline",256,"forced_tiered_hash",3,9610393720456262672,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,42925824.17582418,54182921.543129608,81479670.822129875,2552322.6135783563,5423728.8135593217,43920,2516336871995962384 +"baseline",256,"forced_vector_hash",4,13992777055088439294,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,150693188.66787222,639795265.51503515,145369966.5649077,1670564.650851988,8205128.205128205,39456,2516698023620607488 +"baseline",256,"forced_tiered_hash",4,13992777055088439294,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,39794659.556687489,53524594.551196277,81208380.704888746,2526528.5497726123,5192697.7687626779,44132,2516698023620607488 +"baseline",256,"adaptive_hash",4,13992777055088439294,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,160000000,576701268.74279118,141944641.58977997,1837897.4453225508,8504983.3887043186,39456,2516698023620607488 +"baseline",256,"forced_tiered_hash",5,5937878447685887746,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,42665756.463862106,55075177.617447816,83008217.813563541,2522068.0958385877,5481798.7152034268,43484,2516157113323081812 +"baseline",256,"adaptive_hash",5,5937878447685887746,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,133886731.82487616,569800569.80056977,146649068.77841324,1596424.0102171137,8421052.6315789465,39456,2516157113323081812 +"baseline",256,"forced_vector_hash",5,5937878447685887746,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,164500740.25333115,650618087.18282366,143430866.32243258,1739432.9448599755,8648648.6486486476,39456,2516157113323081812 +"baseline",256,"forced_tiered_hash",6,11237391578505003154,"tiered","tiered",16,17,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"tiered",16,"tiered",16,"tiered",33,"tiered",4,40705010.786827855,54262303.977426879,83654007.026936576,2459419.5769798327,5481798.7152034268,44400,2516652954639355697 +"baseline",256,"adaptive_hash",6,11237391578505003154,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,133440085.40165466,573723465.28973031,146134736.22680113,1777461.7845716316,7734138.9728096677,39456,2516652954639355697 +"baseline",256,"forced_vector_hash",6,11237391578505003154,"vector","vector",16,16,256,512,512,1024,100000,97000,1000,500,500,1000,1000,100000,100000,1000,256,"vector",16,"vector",16,"vector",16,"vector",16,141302811.92595732,660938532.71645737,147492625.36873156,1755926.2510974538,7619047.6190476194,39456,2516652954639355697 +"baseline",512,"forced_tiered_hash",0,14984970845100107740,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,39111389.236545682,50032521.138740182,78727759.407967255,2598077.4227071968,5289256.1983471075,84724,2516449983142523160 +"baseline",512,"adaptive_hash",0,14984970845100107740,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,94822681.585435241,576701268.74279118,130225289.7512697,1323801.9592268995,5378151.260504202,78368,2516449983142523160 +"baseline",512,"forced_vector_hash",0,14984970845100107740,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,98463962.189838529,651890482.39895701,130718954.24836603,1371930.3059404583,5251282.051282051,78368,2516449983142523160 +"baseline",512,"adaptive_hash",1,16304051715571137494,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,98328416.912487715,567859170.92561042,129065565.30717605,1272264.631043257,5517241.3793103443,78368,2516668772023801606 +"baseline",512,"forced_vector_hash",1,16304051715571137494,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,102301790.28132991,652741514.3603133,129584035.2468576,1286670.0977869274,4543034.6051464062,78368,2516668772023801606 +"baseline",512,"forced_tiered_hash",1,16304051715571137494,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,37228695.878783368,49842994.567113593,79170295.305201486,2661698.163428267,5366876.3102725362,85064,2516668772023801606 +"baseline",512,"forced_vector_hash",2,9563555999342315634,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,103412616.33919339,657894736.84210527,130907186.80455558,1358880.2826470986,5135406.2186559681,78368,2516540277992719419 +"baseline",512,"forced_tiered_hash",2,9563555999342315634,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,38556446.637877852,50152966.547971316,78963992.419456735,2784739.62684489,5344467.6409185808,85404,2516540277992719419 +"baseline",512,"adaptive_hash",2,9563555999342315634,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,94366330.093422666,575373993.09551215,130174433.74121322,1284026.7077555214,4361158.4327086881,78368,2516540277992719419 +"baseline",512,"adaptive_hash",3,14934861932824753028,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,93870271.285084024,575705238.91767418,131061598.95150721,1336183.8588989845,5140562.248995984,78368,2516478738380764097 +"baseline",512,"forced_vector_hash",3,14934861932824753028,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,101327388.79319081,650618087.18282366,127860887.35455824,1337613.6971642592,4530973.4513274338,78368,2516478738380764097 +"baseline",512,"forced_tiered_hash",3,14934861932824753028,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,38090884.851255096,49756194.646233454,78143314.839415491,2698327.0372369131,5262076.0534429606,86592,2516478738380764097 +"baseline",512,"adaptive_hash",4,7237444903719400313,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,105596620.90813093,577700751.01097631,126582278.48101266,1300221.0375763881,5256673.5112936348,78368,2516642161409729628 +"baseline",512,"forced_vector_hash",4,7237444903719400313,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,107054919.17353603,657030223.39027584,125203455.615375,1225940.9096481549,5283797.7296181628,78368,2516642161409729628 +"baseline",512,"forced_tiered_hash",4,7237444903719400313,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,38755183.505793899,50877639.277537525,78963992.419456735,2645502.6455026455,5355648.5355648529,86252,2516642161409729628 +"baseline",512,"adaptive_hash",5,14701316826153139949,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,106405618.21664184,566893424.03628111,123823675.08667657,1296176.2799740764,5120000,78368,2516651054704929232 +"baseline",512,"forced_vector_hash",5,14701316826153139949,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,115526802.2181146,664451827.24252486,121802679.65895249,1362397.8201634879,4172779.1361043192,78368,2516651054704929232 +"baseline",512,"forced_tiered_hash",5,14701316826153139949,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,39354584.809130266,50597045.132564254,79333597.778659269,2932551.319648094,5372507.8698845748,85064,2516651054704929232 +"baseline",512,"adaptive_hash",6,2747466510094964666,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,103616205.57455187,569151963.57427442,129098889.74954817,1428775.5393627661,5294725.9565667007,78368,2532477225510751123 +"baseline",512,"forced_vector_hash",6,2747466510094964666,"vector","vector",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"vector",23,"vector",23,"vector",23,"vector",23,104569695.70218551,662251655.62913907,133511348.46461949,1441337.5612568464,5014691.4789422136,78368,2532477225510751123 +"baseline",512,"forced_tiered_hash",6,2747466510094964666,"tiered","tiered",23,23,512,1024,1024,2048,100000,97000,1000,500,500,1000,1000,100000,100000,1000,512,"tiered",23,"tiered",23,"tiered",33,"tiered",4,39379380.956131369,49183553.019870147,79808459.696727857,2887669.6505919723,5084409.1360476669,85744,2532477225510751123 +"baseline",1024,"adaptive_hash",0,8263605205929953231,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,61117222.833394453,584112149.53271031,114573785.51787351,934666.79128890554,2944640.7538280329,156192,2533248489645560685 +"baseline",1024,"forced_vector_hash",0,8263605205929953231,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,59220656.164870307,666666666.66666675,113623451.88046813,946073.79375591292,2983293.5560859186,156192,2533248489645560685 +"baseline",1024,"forced_tiered_hash",0,8263605205929953231,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,35664610.007489569,45676700.31516923,70721357.85007073,3117206.9825436408,4653327.1288971612,164256,2533248489645560685 +"baseline",1024,"forced_vector_hash",1,7082387021252559084,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,58816609.810610518,662251655.62913907,112854079.67498025,931619.15408980811,2919708.0291970805,156192,2533011276197967348 +"baseline",1024,"forced_tiered_hash",1,7082387021252559084,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,35463508.050216332,46358536.924574666,70982396.365701303,3224766.2044501775,4657661.8537494177,165176,2533011276197967348 +"baseline",1024,"adaptive_hash",1,7082387021252559084,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,56631555.102503113,583771161.70461178,108695652.17391305,911743.2530999271,2893518.5185185187,156192,2533011276197967348 +"baseline",1024,"forced_tiered_hash",2,9982579891442255758,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,35683699.685983442,46957175.056348607,71352122.725651085,3155569.5803092457,4775549.1881566383,166772,2532625679719947189 +"baseline",1024,"adaptive_hash",2,9982579891442255758,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,58997050.147492632,589622641.50943398,109673173.94165386,888730.89228581579,2796420.5816554809,156192,2532625679719947189 +"baseline",1024,"forced_vector_hash",2,9982579891442255758,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,59648076.349537723,663129973.47480106,109349371.24111538,983284.1691248772,2395783.4211787255,156192,2532625679719947189 +"baseline",1024,"forced_tiered_hash",3,6223662145476107803,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,35713010.249633946,46853769.385747083,71397972.297586754,3168567.8073510774,4866180.0486618001,166096,2532988634027205437 +"baseline",1024,"adaptive_hash",3,6223662145476107803,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,61591525.006159149,590667454.22327232,109613065.8774526,936154.27822505147,2962085.3080568719,156192,2532988634027205437 +"baseline",1024,"forced_vector_hash",3,6223662145476107803,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,60081711.127132908,658327847.26793945,109051254.08942203,971439.67359626968,2790178.5714285714,156192,2532988634027205437 +"baseline",1024,"adaptive_hash",4,7077184263008060768,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,51958848.591915205,584453535.94389248,113083795.09216329,926268.98851426458,2952465.308532625,156192,2533026764089274159 +"baseline",1024,"forced_vector_hash",4,7077184263008060768,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,55922156.358349182,670241286.86327076,113430127.0417423,912741.87659729831,2801120.4481792715,156192,2533026764089274159 +"baseline",1024,"forced_tiered_hash",4,7077184263008060768,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,35472313.859033026,45400889.857441209,71751452.966922581,3144654.0880503147,4537205.081669692,164716,2533026764089274159 +"baseline",1024,"forced_tiered_hash",5,3198208010137807867,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,35835871.707579285,46172315.079878107,71118697.105469033,3102699.3484331369,4782400.7651841231,166096,2532977876410076447 +"baseline",1024,"adaptive_hash",5,3198208010137807867,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,52623270.009998426,307219662.05837172,81426593.925576091,751145.49688274611,2209944.7513812156,156192,2532977876410076447 +"baseline",1024,"forced_vector_hash",5,3198208010137807867,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,45793836.149654262,351988736.36043644,82994439.372562036,754147.81297134247,2220248.6678507994,156192,2532977876410076447 +"baseline",1024,"adaptive_hash",6,878090231870985884,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,47361939.945060156,540248514.31658566,110168557.89357717,882145.37755822157,2691790.0403768509,156192,2533145598879769717 +"baseline",1024,"forced_vector_hash",6,878090231870985884,"vector","vector",32,32,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",32,"vector",32,"vector",32,"vector",32,57286892.758936755,656167979.00262475,110411836.14883517,918020.74726888817,2845759.8178713717,156192,2533145598879769717 +"baseline",1024,"forced_tiered_hash",6,878090231870985884,"tiered","tiered",32,33,1024,2048,2048,4096,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",32,"tiered",32,"tiered",33,"tiered",6,34379619.761405438,44738725.841088042,69895855.175788075,2955956.251847473,4748338.0816714149,166772,2533145598879769717 +"baseline",2048,"forced_tiered_hash",0,17604282621400283443,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,31505986.137366097,41753653.444676407,59154096.421177171,2295684.113865932,2778549.5971103082,321736,2555495986187765739 +"baseline",2048,"adaptive_hash",0,17604282621400283443,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,28851702.250432778,572409845.44934177,78758761.912262738,567086.31053646363,960891.7075045642,311840,2555495986187765739 +"baseline",2048,"forced_vector_hash",0,17604282621400283443,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,32079042.761364002,645161290.3225807,79107665.532790124,566829.15769187163,867077.0831526923,311840,2555495986187765739 +"baseline",2048,"forced_tiered_hash",1,16081151116256482927,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,32177102.773666259,43327556.325823218,58768218.147625767,2600104.0041601663,3813882.5324180019,322968,2556698868577027992 +"baseline",2048,"adaptive_hash",1,16081151116256482927,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,31330283.852371704,580383052.81485784,81433224.755700335,568795.85916614521,995619.2751891677,311840,2556698868577027992 +"baseline",2048,"forced_vector_hash",1,16081151116256482927,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,31755104.633069765,661813368.63004637,81333875.559170395,575175.42850569426,978377.84952548682,311840,2556698868577027992 +"baseline",2048,"forced_vector_hash",2,18436629033319334241,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,31345014.575431775,651465798.04560268,82155767.334866896,570125.42759407067,973520.24922118383,311840,2556276914809095413 +"baseline",2048,"forced_tiered_hash",2,18436629033319334241,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,32539372.640895486,43639537.420903333,59559261.465157837,2566735.1129363449,3907776.4751856197,323584,2556276914809095413 +"baseline",2048,"adaptive_hash",2,18436629033319334241,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,33111486.374623358,585823081.42940831,82290980.908492431,562651.21251336299,935016.36278634879,311840,2556276914809095413 +"baseline",2048,"forced_vector_hash",3,5149219854132784233,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,29173230.643561468,654878847.41322851,80619155.111254439,546716.96462741238,956022.94455066917,311840,2556998120563425210 +"baseline",2048,"forced_tiered_hash",3,5149219854132784233,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,32399157.621901833,43691017.126878709,58210605.972408168,2511931.6754584275,3869969.0402476783,321736,2556998120563425210 +"baseline",2048,"adaptive_hash",3,5149219854132784233,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,28376039.272438351,578703703.70370376,81406707.91273202,542652.48534838296,1007759.7500755819,311840,2556998120563425210 +"baseline",2048,"adaptive_hash",4,11228077710321852230,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,30816640.98613251,569800569.80056977,80070462.006565779,579710.14492753625,1005732.6762546515,311840,2556450339210586434 +"baseline",2048,"forced_vector_hash",4,11228077710321852230,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,33212660.666245975,663570006.63570011,81426593.925576091,596338.48172222555,973615.03261610365,311840,2556450339210586434 +"baseline",2048,"forced_tiered_hash",4,11228077710321852230,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,32150205.761316869,43252595.155709349,59157595.835305251,2615062.7615062762,3880481.1796662789,320608,2556450339210586434 +"baseline",2048,"adaptive_hash",5,9566722756865290973,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,31993857.179421552,587199060.48150325,82088327.039894924,558628.0096084018,965344.14518775942,311840,2555871518819976418 +"baseline",2048,"forced_vector_hash",5,9566722756865290973,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,32189531.964205239,669792364.36704624,83104795.146679968,547225.5663784612,975039.0015600624,311840,2555871518819976418 +"baseline",2048,"forced_tiered_hash",5,9566722756865290973,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,31830914.183855359,42920297.008455291,59783583.427990675,2710027.1002710024,3894080.9968847353,320608,2555871518819976418 +"baseline",2048,"adaptive_hash",6,11775567059054511224,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,32073898.261594713,581395348.83720922,81162243.324405476,565834.88937927911,1044059.302568386,311840,2556823170109389014 +"baseline",2048,"forced_vector_hash",6,11775567059054511224,"vector","vector",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",46,"vector",46,"vector",46,"vector",46,31979533.098816756,653167864.14108419,82304526.748971194,576202.82339383464,1034768.2119205298,311840,2556823170109389014 +"baseline",2048,"forced_tiered_hash",6,11775567059054511224,"tiered","tiered",46,46,2048,4096,4096,8192,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",46,"tiered",46,"tiered",46,"tiered",46,33871896.487484336,43940592.31918446,59220656.164870307,2581311.3061435209,3938558.4875935409,321736,2556823170109389014 +"baseline",4096,"forced_tiered_hash",0,7600465057368479647,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,32578595.862518322,46262028.1273131,54824561.403508767,1773678.60943597,3109452.736318408,628464,2606749384601431537 +"baseline",4096,"adaptive_hash",0,7600465057368479647,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,32639206.21450486,581395348.83720922,76710647.437864378,1886080.7242549981,434839.32686872198,628464,2606749384601431537 +"baseline",4096,"forced_vector_hash",0,7600465057368479647,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,17404016.847088307,661813368.63004637,77297673.340032458,289435.60057887121,427880.70685892773,623136,2606749384601431537 +"baseline",4096,"forced_vector_hash",1,7524986396680695453,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,17641040.115725223,638569604.0868454,76341705.473700285,315537.04404897132,429239.81628535863,623136,2601740960506217425 +"baseline",4096,"forced_tiered_hash",1,7524986396680695453,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,32450674.974039461,45363817.818907641,54030689.431597143,1796622.3499820339,3147623.5442241104,624244,2601740960506217425 +"baseline",4096,"adaptive_hash",1,7524986396680695453,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,32852590.426755149,581057524.69494474,76359193.646915093,1822821.7280349983,426712.18263281416,624244,2601740960506217425 +"baseline",4096,"forced_vector_hash",2,7460972931250716399,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,17199862.401100792,666222518.32111931,78864353.312302843,299697.30572122155,380531.98371323111,623136,2608275452360560399 +"baseline",4096,"forced_tiered_hash",2,7460972931250716399,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,31092593.74417014,44316419.233325951,55303616.856542416,1814882.0326678767,3157562.3618566468,622556,2608275452360560399 +"baseline",4096,"adaptive_hash",2,7460972931250716399,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,32599837.000814997,578368999.42163098,76359193.646915093,1870207.5930428277,434782.60869565216,622556,2608275452360560399 +"baseline",4096,"forced_tiered_hash",3,15361207867526911576,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,32899065.666535072,45599635.202918373,56201877.142696567,1756543.1231336729,3152585.1197982342,625088,2608821061764209734 +"baseline",4096,"adaptive_hash",3,15361207867526911576,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,33017466.239640769,583771161.70461178,76787222.606158346,1815870.7100054477,413018.33801420784,625088,2608821061764209734 +"baseline",4096,"forced_vector_hash",3,15361207867526911576,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,16115516.018822923,646412411.11829352,74744001.79385604,313952.02813010174,418988.56161226804,623136,2608821061764209734 +"baseline",4096,"adaptive_hash",4,11903448878383901159,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,32381322.453208987,589970501.47492623,76740081.344486222,1803101.3342949871,393638.79703983624,627620,2609006760121496943 +"baseline",4096,"forced_vector_hash",4,11903448878383901159,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,16684463.427656166,668449197.86096251,76446754.835257247,313342.10691232688,418095.15845806507,623136,2609006760121496943 +"baseline",4096,"forced_tiered_hash",4,11903448878383901159,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,31838008.214206122,44480028.46721822,55318913.536538146,1772421.1272598368,3093102.3816888337,627620,2609006760121496943 +"baseline",4096,"forced_tiered_hash",5,14750161124092376284,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,33208248.929033969,45722646.42677518,54761513.608236134,1883239.1713747645,3080714.7258163895,622556,2606581900541539285 +"baseline",4096,"adaptive_hash",5,14750161124092376284,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,33283408.221001834,574712643.67816091,76698880.196349144,1798561.1510791369,422047.77580822154,622556,2606581900541539285 +"baseline",4096,"forced_vector_hash",5,14750161124092376284,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,17349966.167565972,660938532.71645737,78027465.667915106,297026.76211126626,421247.73579342011,623136,2606581900541539285 +"baseline",4096,"adaptive_hash",6,5292099999092531180,"vector","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector->tiered",65,"vector",64,32252862.441541687,590318772.13695395,78740157.480314955,1836884.6436443792,420062.16920104175,626776,2608914160727435722 +"baseline",4096,"forced_vector_hash",6,5292099999092531180,"vector","vector",64,64,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",64,"vector",64,"vector",64,"vector",64,17002176.278563656,665778961.38482022,78486774.97841613,317783.14478200075,432656.94630727294,623136,2608914160727435722 +"baseline",4096,"forced_tiered_hash",6,5292099999092531180,"tiered","tiered",64,65,4096,8192,8192,16384,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",64,"tiered",64,"tiered",65,"tiered",64,32526671.870934166,45460744.646997318,55694792.536897801,1793078.7161556394,3138731.9522912744,626776,2608914160727435722 +"baseline",8192,"forced_tiered_hash",0,1533354404713909492,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,27054081.108135164,42929509.744998708,51714330.04085432,1309757.6948264572,2308402.5854108958,1237280,2600442685266303989 +"baseline",8192,"adaptive_hash",0,1533354404713909492,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,29148570.262628619,40794680.373679273,49875311.72069826,1308729.2239235702,2704895.861509332,1237280,2600442685266303989 +"baseline",8192,"forced_vector_hash",0,1533354404713909492,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,8709511.6576813553,664893617.02127659,71813285.457809687,166747.26117623519,198736.03879327475,1245728,2600442685266303989 +"baseline",8192,"adaptive_hash",1,8851810353577849580,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,28164253.928913422,40543279.951348066,50296750.82989639,1333688.9837289944,2703433.3603676669,1238436,2610692047236465301 +"baseline",8192,"forced_vector_hash",1,8851810353577849580,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,8783255.601521261,659630606.86015832,72385088.671733633,161540.44972861203,193757.14479471432,1245728,2610692047236465301 +"baseline",8192,"forced_tiered_hash",1,8851810353577849580,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,27512587.008556414,43410314.290675469,51245259.813467249,1348435.8144552319,2285714.2857142854,1238436,2610692047236465301 +"baseline",8192,"forced_vector_hash",2,6074101987220463306,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,9024863.4989395794,654022236.75604963,71428571.428571433,163076.27077184001,199203.18725099601,1245728,2604400936210790827 +"baseline",8192,"forced_tiered_hash",2,6074101987220463306,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,28787747.934479084,42892682.508364074,51116904.360271946,1256597.1349585322,2222222.2222222225,1238436,2604400936210790827 +"baseline",8192,"adaptive_hash",2,6074101987220463306,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,28477047.499715228,40498947.027377293,50263885.398341291,1406074.24071991,2700513.097488523,1238436,2604400936210790827 +"baseline",8192,"forced_vector_hash",3,2489097639920090271,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,8906543.6376105528,637348629.70044613,71813285.457809687,172161.48747525178,192267.02043798426,1245728,2603956742105246981 +"baseline",8192,"forced_tiered_hash",3,2489097639920090271,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,28417971.525192529,42078687.144961074,49578582.052553296,1286504.5670912133,2255808.7074216106,1237280,2603956742105246981 +"baseline",8192,"adaptive_hash",3,2489097639920090271,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,28468129.928544994,36736343.264391467,47705371.624844961,1366493.5774801858,2704895.861509332,1237280,2603956742105246981 +"baseline",8192,"adaptive_hash",4,1885911782505072956,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,28603300.820914734,40546567.733041398,50459178.524573617,1348617.6668914363,2722570.1061802343,1237280,2604212537617782366 +"baseline",8192,"forced_vector_hash",4,1885911782505072956,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,8824956.9783347305,657894736.84210527,72663856.997529432,164395.27199197753,200952.51492072421,1245728,2604212537617782366 +"baseline",8192,"forced_tiered_hash",4,1885911782505072956,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,28479480.534275055,43012602.692588933,50306871.918704093,1317002.5023047545,2276867.0309653915,1237280,2604212537617782366 +"baseline",8192,"forced_tiered_hash",5,2737113097762816153,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,28498147.620404676,42098172.939294435,50671395.996959716,1381597.1262779774,2319109.4619666049,1235716,2608173366147803176 +"baseline",8192,"adaptive_hash",5,2737113097762816153,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,28656579.550664835,41177681.696520485,50357538.523516975,1332267.5193178789,2717391.3043478262,1235716,2608173366147803176 +"baseline",8192,"forced_vector_hash",5,2737113097762816153,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,8791517.9434881229,650618087.18282366,75063804.23359856,167504.18760469012,200835.47557840619,1245728,2608173366147803176 +"baseline",8192,"forced_vector_hash",6,16149500638224588516,"vector","vector",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",91,"vector",91,"vector",91,"vector",91,8634907.4769663848,666222518.32111931,75035641.92991671,161269.51361114695,190901.62839089017,1245728,2604522381130608740 +"baseline",8192,"forced_tiered_hash",6,16149500638224588516,"tiered","tiered",91,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",91,"tiered",91,29339279.427297268,42990413.137870252,52446635.548329569,1346982.7586206896,2207018.3182520415,1235716,2604522381130608740 +"baseline",8192,"adaptive_hash",6,16149500638224588516,"tiered","tiered",65,91,8192,16384,16384,32768,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",65,"tiered",65,"tiered",91,"tiered",65,28920959.019001067,41523066.063198105,51002193.094303049,1426533.5235378032,2758620.6896551726,1235716,2604522381130608740 +"baseline",16384,"forced_vector_hash",0,16519628863117178580,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4489579.6855498385,653594771.24182999,78889239.50773114,83519.163472058674,96723.959492005772,2490912,2811432233702823938 +"baseline",16384,"forced_tiered_hash",0,16519628863117178580,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,23466466.419486552,40734856.816978291,44122837.980938934,923361.03416435828,1396453.0093562352,2465412,2811432233702823938 +"baseline",16384,"adaptive_hash",0,16519628863117178580,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,22854008.593107231,38134462.113411888,50867287.247571088,868357.06842653698,2057189.8786257971,2465412,2811432233702823938 +"baseline",16384,"adaptive_hash",1,12150548782170805316,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,23044660.552150067,35303254.960107327,51464155.215892136,836400.13382402144,2046245.1401677921,2465412,2809601574756132900 +"baseline",16384,"forced_vector_hash",1,12150548782170805316,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4505356.8693176182,649350649.35064936,79157761.418507084,87278.313084764697,96229.71958659713,2490912,2809601574756132900 +"baseline",16384,"forced_tiered_hash",1,12150548782170805316,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,22649543.611696225,41621576.625322565,50446451.092165664,837871.80561374116,1707941.9299743809,2465412,2809601574756132900 +"baseline",16384,"adaptive_hash",2,14057633980897130801,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,23025028.205659553,38423115.346192271,50658561.296859168,866776.45835139125,2076843.1983385254,2465412,2849474804600164814 +"baseline",16384,"forced_vector_hash",2,14057633980897130801,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4469873.0556052206,655307994.75753605,79955225.073958591,86878.708634874856,95758.840934989319,2490912,2849474804600164814 +"baseline",16384,"forced_tiered_hash",2,14057633980897130801,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,23206163.557040751,40086587.027980439,51802735.184417732,975704.94682408043,1707358.7160662457,2465412,2849474804600164814 +"baseline",16384,"forced_tiered_hash",3,9496396460044610260,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,23691629.747210313,40364898.684104308,51284681.265705928,956205.77548288403,1713796.0582690658,2465412,2809604592862375281 +"baseline",16384,"adaptive_hash",3,9496396460044610260,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,23141719.892622419,38822889.975929804,44738725.841088042,835073.06889352819,2125850.3401360544,2465412,2809604592862375281 +"baseline",16384,"forced_vector_hash",3,9496396460044610260,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4597257.2763089538,633312222.92590249,79674926.300693169,87185.4783867199,96414.350311900431,2490912,2809604592862375281 +"baseline",16384,"adaptive_hash",4,14635596384326246855,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,22955260.197874345,38213152.967251323,37296732.806206174,810964.2364771713,2092925.910422771,2465412,2802782570264501827 +"baseline",16384,"forced_vector_hash",4,14635596384326246855,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4496180.4946697783,651465798.04560268,79993600.511959031,84569.456894947827,94758.886014536023,2490912,2802782570264501827 +"baseline",16384,"forced_tiered_hash",4,14635596384326246855,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,23009134.626446698,41142104.830083102,51551706.361480564,936241.92491339764,1738828.0299078419,2465412,2802782570264501827 +"baseline",16384,"adaptive_hash",5,2683868946381181637,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,22778005.557833355,38630920.188518889,49967521.11127767,810832.72520878934,2072968.4908789385,2465412,2805449818878573377 +"baseline",16384,"forced_vector_hash",5,2683868946381181637,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4443634.7154518515,649350649.35064936,79478620.251152441,87065.542940725776,98948.180837695298,2490912,2805449818878573377 +"baseline",16384,"forced_tiered_hash",5,2683868946381181637,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,23255813.953488372,36956280.719908349,50045040.536482841,935628.74251497013,1709693.9647803043,2465412,2805449818878573377 +"baseline",16384,"forced_tiered_hash",6,7266316803874473535,"tiered","tiered",128,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",128,"tiered",128,"tiered",129,"tiered",128,23616654.464728527,40329085.33634457,51602249.858093813,929108.98448387987,1710863.9863130883,2465412,2802923093430382200 +"baseline",16384,"adaptive_hash",6,7266316803874473535,"tiered","tiered",91,129,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",91,"tiered",91,"tiered",129,"tiered",91,22878059.940517046,38170852.736850142,51559680.329981953,837100.28461409674,2079434.3938448743,2465412,2802923093430382200 +"baseline",16384,"forced_vector_hash",6,7266316803874473535,"vector","vector",128,128,16384,32768,32768,65536,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",128,"vector",128,"vector",128,"vector",128,4471492.0027365535,641436818.47338045,77754451.442345068,86972.403656319846,94894.666919719108,2490912,2802923093430382200 +"baseline",32768,"forced_tiered_hash",0,3799899914257613626,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18081547.780490011,37212071.59602575,50994390.617032126,573000.22920009168,1245640.2590931738,4921232,5921195651255336034 +"baseline",32768,"adaptive_hash",0,3799899914257613626,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,18079586.339064565,35783296.357260428,48744820.862783328,509658.01946893631,1541544.6277169725,4921232,5921195651255336034 +"baseline",32768,"forced_vector_hash",0,3799899914257613626,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2246075.5445048637,637348629.70044613,80638658.172728002,43353.102348004024,48926.55146094683,4981280,5921195651255336034 +"baseline",32768,"forced_vector_hash",1,14870762844571170300,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2274650.3862356353,614250614.25061429,68898994.074686512,43827.547366621817,47624.263609823931,4981280,5903990191892135316 +"baseline",32768,"forced_tiered_hash",1,14870762844571170300,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18031338.46625435,37676135.935498461,50994390.617032126,557631.18273573858,1253918.4952978056,4921232,5903990191892135316 +"baseline",32768,"adaptive_hash",1,14870762844571170300,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,16707321.148127109,35651894.898213841,49058084.772370487,519076.04464053985,1548227.2797646695,4921232,5903990191892135316 +"baseline",32768,"adaptive_hash",2,3668356801526459156,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,17825629.690368813,35951824.555096172,49748768.717974231,514165.2527122217,1523693.4328813043,4921232,5867651332793912201 +"baseline",32768,"forced_vector_hash",2,3668356801526459156,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2285824.0052665384,610873549.17532074,81254570.569594547,43017.968605486516,47917.734832838978,4981280,5867651332793912201 +"baseline",32768,"forced_tiered_hash",2,3668356801526459156,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18491124.260355029,38431975.403535739,50266411.983512618,575373.99309551215,1244400.1991040318,4921232,5867651332793912201 +"baseline",32768,"forced_vector_hash",3,5597019136430372439,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2267244.6629060637,600240096.03841531,79687624.5119133,44299.339496848108,47770.548501437894,4981280,5952659854051489931 +"baseline",32768,"forced_tiered_hash",3,5597019136430372439,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18790281.666322179,37717346.207520835,49287791.414066732,571004.3967338549,1260080.6451612904,4921232,5952659854051489931 +"baseline",32768,"adaptive_hash",3,5597019136430372439,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,18053800.324968409,33225902.913911685,48612123.863691606,489620.05483744608,1558360.6046439146,4921232,5952659854051489931 +"baseline",32768,"adaptive_hash",4,6810547043850850325,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,18033289.452328999,36264732.54759746,50125313.283208027,463306.15270570794,1557632.398753894,4921232,5886124142016643594 +"baseline",32768,"forced_vector_hash",4,6810547043850850325,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2236391.0016571656,609756097.56097555,68376068.376068369,42827.286120533114,48879.199945255292,4981280,5886124142016643594 +"baseline",32768,"forced_tiered_hash",4,6810547043850850325,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18414849.734826162,36848699.240916796,50733093.196692206,578435.90930124943,1232134.056185313,4921232,5886124142016643594 +"baseline",32768,"adaptive_hash",5,11482174959549226004,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,17838667.094794676,36209581.055147186,49838026.414154001,525762.35541535227,1533036.9461904033,4921232,5854156252755476422 +"baseline",32768,"forced_vector_hash",5,11482174959549226004,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2227990.2414027425,589275191.5144372,77567483.710828424,42526.770602094017,47344.673960902765,4981280,5854156252755476422 +"baseline",32768,"forced_tiered_hash",5,11482174959549226004,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18402988.645356007,33594248.664628617,49394912.32403063,519750.51975051977,1255492.7809165097,4921232,5854156252755476422 +"baseline",32768,"adaptive_hash",6,4266179595843056063,"tiered","tiered",129,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",129,"tiered",129,"tiered",182,"tiered",129,17943012.990741406,35944071.025484346,48683121.56175454,534702.17089081381,1548227.2797646695,4921232,5945944892063186673 +"baseline",32768,"forced_vector_hash",6,4266179595843056063,"vector","vector",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",182,"vector",182,"vector",182,"vector",182,2226794.573746983,617283950.61728394,81806282.722513095,42699.459851832878,47783.787916435707,4981280,5945944892063186673 +"baseline",32768,"forced_tiered_hash",6,4266179595843056063,"tiered","tiered",182,182,32768,65536,65536,131072,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",182,"tiered",182,18222238.419767484,37572797.294758596,49202912.812438495,552852.72003538255,1242081.7289777668,4921232,5945944892063186673 +"baseline",65536,"forced_tiered_hash",0,10661120783772612247,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,13402849.445792176,34753596.997289218,45568466.621098205,329261.46653057192,899523.25267608173,9837616,7078339330582418198 +"baseline",65536,"adaptive_hash",0,10661120783772612247,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,13422458.457491074,32863386.900653984,44482007.028157115,300237.18737802864,1125365.7438667566,9837616,7078339330582418198 +"baseline",65536,"forced_vector_hash",0,10661120783772612247,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1119194.1801902631,498256103.63726956,62523446.292359635,21548.705461519399,23725.130132338778,9962016,7078339330582418198 +"baseline",65536,"forced_vector_hash",1,3645920298421955121,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1040478.7867184964,374391613.62785476,44754744.002864301,21149.518636955821,22512.027050451703,9962016,7058462025128772180 +"baseline",65536,"forced_tiered_hash",1,3645920298421955121,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,10046818.17268471,27078989.412115142,41592147.402570397,285103.34996436205,820209.97375328094,9837616,7058462025128772180 +"baseline",65536,"adaptive_hash",1,3645920298421955121,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,11310937.676733401,31331265.469812326,43438599.539550841,283157.77551251557,1129177.9584462512,9837616,7058462025128772180 +"baseline",65536,"forced_vector_hash",2,13440076570755554700,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1047528.4613482949,359582883.85472852,65638332.786347233,21566.969754481619,23456.668496286809,9962016,7031035215144323638 +"baseline",65536,"forced_tiered_hash",2,13440076570755554700,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,13332800.02133248,34330049.091970205,45493835.585278191,315139.29156687256,912908.52656563802,9837616,7031035215144323638 +"baseline",65536,"adaptive_hash",2,13440076570755554700,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,12777429.947740313,25755273.392227057,22394911.876021765,282757.45065882488,1079447.322970639,9837616,7031035215144323638 +"baseline",65536,"forced_tiered_hash",3,265097094473494960,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,11666841.669291705,27756960.057734478,32832096.657692559,287811.19585551874,881057.26872246701,9837616,7048178356920180496 +"baseline",65536,"adaptive_hash",3,265097094473494960,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,9837387.9767444152,30628809.458176363,43387712.599791735,299976.0019198464,1097815.3474585575,9837616,7048178356920180496 +"baseline",65536,"forced_vector_hash",3,265097094473494960,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1099008.5843560523,490436488.47474253,71916576.770945698,22126.341409447949,22096.761719569997,9962016,7048178356920180496 +"baseline",65536,"forced_tiered_hash",4,10511978180227547512,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,12827748.986607831,33056758.454265974,44620945.071616612,324002.07361327112,912492.0156948627,9837616,7047216827085719657 +"baseline",65536,"adaptive_hash",4,10511978180227547512,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,11165449.632656708,24729826.643915229,42163848.716110803,281761.57335662562,1116819.2986374805,9837616,7047216827085719657 +"baseline",65536,"forced_vector_hash",4,10511978180227547512,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1096373.4160145072,491159135.55992144,60727515.637335271,21258.096677572074,22895.240537397087,9962016,7047216827085719657 +"baseline",65536,"adaptive_hash",5,18171218181464882810,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,13292216.078264568,31373533.287318818,38030043.734550297,295255.24816203606,986193.29388560168,9837616,6972502824690047116 +"baseline",65536,"forced_vector_hash",5,18171218181464882810,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1075244.5374889383,475285171.10266161,63678043.81049414,21506.810131428116,22458.535928042853,9962016,6972502824690047116 +"baseline",65536,"forced_tiered_hash",5,18171218181464882810,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,12614158.131086331,32799790.081343479,45798030.684680559,312460.94238220225,910166.56048056798,9837616,6972502824690047116 +"baseline",65536,"forced_vector_hash",6,13709039232440699940,"vector","vector",256,256,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"vector",256,"vector",256,"vector",256,"vector",256,1089099.2060466788,501253132.83208019,71169311.792754963,20818.283449256476,22991.835599178728,9962016,7010486178119326697 +"baseline",65536,"forced_tiered_hash",6,13709039232440699940,"tiered","tiered",256,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",256,"tiered",256,"tiered",257,"tiered",256,10754075.794726202,31665611.146295123,44218439.089100152,315666.52987783705,919793.96615158208,9837616,7010486178119326697 +"baseline",65536,"adaptive_hash",6,13709039232440699940,"tiered","tiered",182,257,65536,131072,131072,262144,100000,97000,1000,500,500,1000,1000,100000,100000,1000,1000,"tiered",182,"tiered",182,"tiered",257,"tiered",182,13104614.134636804,29824038.174768861,42513391.718391292,311342.19620785204,1135331.5168029063,9837616,7010486178119326697 diff --git a/src/benchmark.cpp b/src/benchmark.cpp index 6dfe1a2..1411ccc 100644 --- a/src/benchmark.cpp +++ b/src/benchmark.cpp @@ -730,7 +730,7 @@ private: } return uc::CostModelPolicy(adaptation, config); } - uc::AdaptiveSequence values_; + uc::AdaptiveSequence values_; }; template diff --git a/src/focused_benchmark.cpp b/src/focused_benchmark.cpp new file mode 100644 index 0000000..03ef9ab --- /dev/null +++ b/src/focused_benchmark.cpp @@ -0,0 +1,896 @@ +#include "universal_container/adaptive_sequence.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef UC_BUILD_PROFILE +#define UC_BUILD_PROFILE "unknown" +#endif + +namespace { + +using Value = std::uint32_t; +using Clock = std::chrono::steady_clock; +// Every compared layout uses the same flat value index. Value lookup is thus +// comparable, and insert/erase timings include hash-index maintenance. +using Sequence = uc::AdaptiveSequence; + +volatile std::uint64_t benchmark_sink = 0; + +enum class OperationKind : std::uint8_t { read, find, insert, erase }; + +struct Operation { + OperationKind kind = OperationKind::read; + std::size_t index = 0; + Value value = 0; +}; + +struct FindQuery { + Value value = 0; + std::size_t expected_index = std::numeric_limits::max(); +}; + +struct Trace { + std::vector initial; + std::vector mixed; + std::vector reads; + std::vector finds; + std::vector inserts; + std::vector erases; + std::size_t read_count = 0; + std::size_t find_count = 0; + std::size_t find_hit_count = 0; + std::size_t find_miss_count = 0; + std::size_t insert_count = 0; + std::size_t erase_count = 0; + std::uint64_t expected_mixed_guard = 0; + std::uint64_t expected_read_guard = 0; + std::uint64_t expected_find_guard = 0; + std::uint64_t expected_insert_guard = 0; + std::uint64_t expected_erase_guard = 0; +}; + +struct Options { + std::size_t min_n = 256; + std::size_t max_n = 65'536; + std::size_t operations = 100'000; + std::size_t repeats = 7; + std::filesystem::path output = "results/benchmarks/focused.csv"; + std::uint64_t seed = 0x6a09'e667'f3bc'c909ULL; +}; + +struct Result { + std::size_t n = 0; + std::string container; + std::size_t repeat = 0; + std::uint64_t seed = 0; + std::string initial_mode; + std::string mode; + std::string read_mode; + std::string find_mode; + std::string insert_mode; + std::string erase_mode; + std::size_t read_leaf = 0; + std::size_t find_leaf = 0; + std::size_t insert_leaf = 0; + std::size_t erase_leaf = 0; + std::size_t initial_leaf = 0; + std::size_t leaf = 0; + std::optional initial_logical_capacity; + std::optional logical_capacity; + std::optional initial_hash_buckets; + std::optional hash_buckets; + std::size_t operations = 0; + std::size_t read_count = 0; + std::size_t find_count = 0; + std::size_t find_hit_count = 0; + std::size_t find_miss_count = 0; + std::size_t insert_count = 0; + std::size_t erase_count = 0; + std::size_t read_batch_count = 0; + std::size_t find_batch_count = 0; + std::size_t insert_batch_count = 0; + std::size_t erase_batch_count = 0; + double mixed_ops_per_sec = 0.0; + double read_ops_per_sec = 0.0; + double find_ops_per_sec = 0.0; + double insert_ops_per_sec = 0.0; + double erase_ops_per_sec = 0.0; + std::size_t bytes = 0; + std::uint64_t checksum = 0; +}; + +constexpr std::size_t missing_index = std::numeric_limits::max(); +constexpr std::size_t minimum_fast_batch_operations = 100'000; + +[[noreturn]] void usage_error(std::string_view message) { + throw std::invalid_argument(std::string(message) + + "\nusage: uc_focused_bench [--min-n N] [--max-n N]" + " [--operations N] [--repeats N] [--output FILE] [--seed N]"); +} + +template +Integer parse_integer(std::string_view text, std::string_view option) { + Integer value{}; + const auto* first = text.data(); + const auto* last = first + text.size(); + const auto [end, error] = std::from_chars(first, last, value); + if (error != std::errc{} || end != last) { + usage_error("invalid integer for " + std::string(option)); + } + return value; +} + +Options parse_options(int argc, char** argv) { + Options options; + for (int i = 1; i < argc; ++i) { + const std::string_view option(argv[i]); + if (option == "--help" || option == "-h") { + std::cout + << "usage: uc_focused_bench [--min-n N] [--max-n N]" + " [--operations N] [--repeats N] [--output FILE] [--seed N]\n"; + std::exit(0); + } + if (i + 1 == argc) { + usage_error("missing value for " + std::string(option)); + } + const std::string_view value(argv[++i]); + if (option == "--min-n") { + options.min_n = parse_integer(value, option); + } else if (option == "--max-n") { + options.max_n = parse_integer(value, option); + } else if (option == "--operations") { + options.operations = parse_integer(value, option); + } else if (option == "--repeats") { + options.repeats = parse_integer(value, option); + } else if (option == "--output") { + options.output = std::filesystem::path(value); + } else if (option == "--seed") { + options.seed = parse_integer(value, option); + } else { + usage_error("unknown option: " + std::string(option)); + } + } + + if (options.min_n == 0 || options.max_n < options.min_n) { + usage_error("the size range must satisfy 1 <= min-n <= max-n"); + } + if (options.operations == 0 || options.operations % 100 != 0) { + usage_error("--operations must be a positive multiple of 100"); + } + if (options.repeats == 0) { + usage_error("--repeats must be positive"); + } + if (options.max_n >= (std::uint64_t{1} << 31) + || options.operations / 100 >= (std::uint64_t{1} << 31) - options.max_n) { + usage_error("size plus inserted values must stay below 2^31"); + } + return options; +} + +std::vector power_of_two_sizes(const Options& options) { + std::vector sizes; + std::size_t n = std::bit_ceil(options.min_n); + while (n <= options.max_n) { + sizes.push_back(n); + if (n > options.max_n / 2) { + break; + } + n *= 2; + } + if (sizes.empty()) { + usage_error("the requested range contains no power-of-two size"); + } + return sizes; +} + +std::size_t ceil_sqrt(std::size_t value) { + auto root = static_cast(std::sqrt(static_cast(value))); + while (root * root < value) { + ++root; + } + while (root > 1 && (root - 1) * (root - 1) >= value) { + --root; + } + return root; +} + +std::size_t random_index(std::mt19937_64& random, std::size_t upper_exclusive) { + if (upper_exclusive == 0) { + throw std::logic_error("cannot select an index from an empty sequence"); + } + return static_cast(random() % upper_exclusive); +} + +std::uint64_t guard_start(std::uint64_t tag) noexcept { + return 0xcbf2'9ce4'8422'2325ULL ^ (tag * 0x9e37'79b9'7f4a'7c15ULL); +} + +void guard_event(std::uint64_t& guard, + std::uint64_t tag, + std::size_t index, + Value value) noexcept { + // This executes in timed batches, so keep the guard much cheaper than the + // operation being measured. The full ordered sequence digest below gives + // the structural checks a stronger second line of defence. + const auto word = static_cast(index) + ^ (static_cast(value) << 17) + ^ (tag << 3); + guard += std::rotl(word, static_cast(tag & 7U)); + guard ^= guard >> 29; +} + +template +std::uint64_t sequence_digest(const Container& container) { + auto digest = guard_start(0x51); + std::size_t index = 0; + if constexpr (requires(const Container& value) { + value.for_each([](const Value&) {}); + }) { + container.for_each([&](const Value& value) { + guard_event(digest, 0x52, index++, value); + }); + } else { + for (const auto value : container) { + guard_event(digest, 0x52, index++, value); + } + } + guard_event(digest, 0x53, container.size(), static_cast(container.size())); + return digest; +} + +void finish_guard(std::uint64_t& guard, std::uint64_t state_digest) noexcept { + guard_event(guard, 0x7f, static_cast(state_digest), + static_cast(state_digest >> 32)); +} + +Trace make_trace(std::size_t n, std::size_t operation_count, std::uint64_t seed) { + Trace trace; + trace.initial.resize(n); + for (std::size_t i = 0; i < n; ++i) { + trace.initial[i] = static_cast(i); + } + + trace.find_count = operation_count / 100; + trace.insert_count = operation_count / 100; + trace.erase_count = operation_count / 100; + trace.read_count = operation_count - trace.find_count + - trace.insert_count - trace.erase_count; + trace.find_hit_count = (trace.find_count + 1) / 2; + trace.find_miss_count = trace.find_count - trace.find_hit_count; + + std::mt19937_64 random(seed); + std::vector kinds; + kinds.reserve(operation_count); + kinds.insert(kinds.end(), trace.read_count, OperationKind::read); + kinds.insert(kinds.end(), trace.find_count, OperationKind::find); + kinds.insert(kinds.end(), trace.insert_count, OperationKind::insert); + kinds.insert(kinds.end(), trace.erase_count, OperationKind::erase); + std::shuffle(kinds.begin(), kinds.end(), random); + + trace.expected_mixed_guard = guard_start(1); + auto mixed_shadow = trace.initial; + trace.mixed.reserve(operation_count); + std::size_t find_ordinal = 0; + std::size_t insert_ordinal = 0; + for (std::size_t operation_index = 0; operation_index < kinds.size(); ++operation_index) { + if (kinds[operation_index] == OperationKind::erase && mixed_shadow.size() == 1) { + const auto next_insert = std::find(kinds.begin() + + static_cast(operation_index + 1), + kinds.end(), OperationKind::insert); + if (next_insert == kinds.end()) { + throw std::logic_error("trace scheduling could not keep the sequence non-empty"); + } + std::iter_swap(kinds.begin() + static_cast(operation_index), + next_insert); + } + + Operation operation; + operation.kind = kinds[operation_index]; + switch (operation.kind) { + case OperationKind::read: + operation.index = random_index(random, mixed_shadow.size()); + operation.value = mixed_shadow[operation.index]; + guard_event(trace.expected_mixed_guard, 1, operation.index, operation.value); + break; + case OperationKind::find: + if ((find_ordinal++ & 1U) == 0) { + operation.index = random_index(random, mixed_shadow.size()); + operation.value = mixed_shadow[operation.index]; + } else { + operation.index = missing_index; + operation.value = static_cast( + std::numeric_limits::max() - find_ordinal); + } + guard_event(trace.expected_mixed_guard, 2, operation.index, operation.value); + break; + case OperationKind::insert: + operation.index = static_cast(random() % (mixed_shadow.size() + 1)); + operation.value = static_cast(n + insert_ordinal++); + mixed_shadow.insert(mixed_shadow.begin() + + static_cast(operation.index), operation.value); + guard_event(trace.expected_mixed_guard, 3, operation.index, operation.value); + break; + case OperationKind::erase: + operation.index = random_index(random, mixed_shadow.size()); + operation.value = mixed_shadow[operation.index]; + mixed_shadow.erase(mixed_shadow.begin() + + static_cast(operation.index)); + guard_event(trace.expected_mixed_guard, 4, operation.index, operation.value); + break; + } + trace.mixed.push_back(operation); + } + auto mixed_digest = guard_start(0x51); + for (std::size_t i = 0; i < mixed_shadow.size(); ++i) { + guard_event(mixed_digest, 0x52, i, mixed_shadow[i]); + } + guard_event(mixed_digest, 0x53, mixed_shadow.size(), + static_cast(mixed_shadow.size())); + finish_guard(trace.expected_mixed_guard, mixed_digest); + + const auto read_batch_count = std::max( + trace.read_count, minimum_fast_batch_operations); + trace.expected_read_guard = guard_start(2); + trace.reads.reserve(read_batch_count); + for (std::size_t i = 0; i < read_batch_count; ++i) { + const auto index = random_index(random, trace.initial.size()); + trace.reads.push_back(index); + guard_event(trace.expected_read_guard, 1, index, trace.initial[index]); + } + auto initial_digest = guard_start(0x51); + for (std::size_t i = 0; i < trace.initial.size(); ++i) { + guard_event(initial_digest, 0x52, i, trace.initial[i]); + } + guard_event(initial_digest, 0x53, trace.initial.size(), + static_cast(trace.initial.size())); + finish_guard(trace.expected_read_guard, initial_digest); + + const auto find_batch_count = std::max( + trace.find_count, minimum_fast_batch_operations); + trace.expected_find_guard = guard_start(3); + trace.finds.reserve(find_batch_count); + for (std::size_t i = 0; i < find_batch_count; ++i) { + FindQuery query; + if ((i & 1U) == 0) { + query.expected_index = random_index(random, trace.initial.size()); + query.value = trace.initial[query.expected_index]; + } else { + query.value = static_cast(std::numeric_limits::max() - i); + } + trace.finds.push_back(query); + guard_event(trace.expected_find_guard, 2, query.expected_index, query.value); + } + finish_guard(trace.expected_find_guard, initial_digest); + + trace.expected_insert_guard = guard_start(4); + auto insert_shadow = trace.initial; + trace.inserts.reserve(trace.insert_count); + for (std::size_t i = 0; i < trace.insert_count; ++i) { + Operation operation; + operation.kind = OperationKind::insert; + operation.index = static_cast(random() % (insert_shadow.size() + 1)); + operation.value = static_cast(n + i); + trace.inserts.push_back(operation); + insert_shadow.insert(insert_shadow.begin() + + static_cast(operation.index), operation.value); + guard_event(trace.expected_insert_guard, 3, operation.index, operation.value); + } + auto insert_digest = guard_start(0x51); + for (std::size_t i = 0; i < insert_shadow.size(); ++i) { + guard_event(insert_digest, 0x52, i, insert_shadow[i]); + } + guard_event(insert_digest, 0x53, insert_shadow.size(), + static_cast(insert_shadow.size())); + finish_guard(trace.expected_insert_guard, insert_digest); + + trace.expected_erase_guard = guard_start(5); + auto erase_shadow = trace.initial; + const auto erase_batch_count = std::min(trace.erase_count, erase_shadow.size()); + trace.erases.reserve(erase_batch_count); + for (std::size_t i = 0; i < erase_batch_count; ++i) { + Operation operation; + operation.kind = OperationKind::erase; + operation.index = random_index(random, erase_shadow.size()); + operation.value = erase_shadow[operation.index]; + trace.erases.push_back(operation); + erase_shadow.erase(erase_shadow.begin() + + static_cast(operation.index)); + guard_event(trace.expected_erase_guard, 4, operation.index, operation.value); + } + auto erase_digest = guard_start(0x51); + for (std::size_t i = 0; i < erase_shadow.size(); ++i) { + guard_event(erase_digest, 0x52, i, erase_shadow[i]); + } + guard_event(erase_digest, 0x53, erase_shadow.size(), + static_cast(erase_shadow.size())); + finish_guard(trace.expected_erase_guard, erase_digest); + + return trace; +} + +template +Value indexed_read(Container& container, std::size_t index) { + return static_cast(container[index]); +} + +template +void insert_at(Container& container, std::size_t index, Value value) { + if constexpr (std::same_as>) { + container.insert(container.begin() + static_cast(index), value); + } else { + container.insert(index, value); + } +} + +template +void erase_at(Container& container, std::size_t index) { + if constexpr (std::same_as>) { + container.erase(container.begin() + static_cast(index)); + } else { + container.erase(index); + } +} + +template +std::size_t find_value(const Container& container, Value value) { + if constexpr (std::same_as>) { + const auto found = std::find(container.begin(), container.end(), value); + return found == container.end() + ? missing_index + : static_cast(found - container.begin()); + } else { + return container.find_one(value).value_or(missing_index); + } +} + +template +std::optional logical_capacity(const Container& container) { + if constexpr (requires(const Container& value) { + { value.capacity() } -> std::convertible_to; + }) { + return static_cast(container.capacity()); + } else { + return std::nullopt; + } +} + +template +std::optional hash_bucket_count(const Container& container) { + if constexpr (requires(const Container& value) { + { value.hash_bucket_count() } -> std::convertible_to; + }) { + return static_cast(container.hash_bucket_count()); + } else { + return std::nullopt; + } +} + +template +std::size_t allocated_bytes(const Container& container) { + if constexpr (requires(const Container& value) { + { value.allocated_bytes() } -> std::convertible_to; + }) { + return static_cast(container.allocated_bytes()); + } else { + return sizeof(container) + container.capacity() * sizeof(Value); + } +} + +template +std::string storage_mode(const Container& container) { + if constexpr (requires(const Container& value) { value.mode(); }) { + return container.mode() == uc::StorageMode::vector ? "vector" : "tiered"; + } else { + return "vector"; + } +} + +template +std::size_t active_leaf(const Container& container) { + if constexpr (requires(const Container& value) { value.tiered_config(); }) { + return container.tiered_config().leaf_capacity; + } else { + return 0; + } +} + +template +std::string mode_transition(const Container& container, + std::string_view initial_mode) { + const auto final_mode = storage_mode(container); + return final_mode == initial_mode + ? final_mode : std::string(initial_mode) + "->" + final_mode; +} + +double operations_per_second(std::size_t count, Clock::duration duration) { + const auto seconds = std::chrono::duration(duration).count(); + return seconds > 0.0 ? static_cast(count) / seconds : 0.0; +} + +template +Result run_candidate(const Trace& trace, + std::size_t n, + std::size_t repeat, + std::uint64_t seed, + std::string container_name, + Builder&& builder) { + Result result; + result.n = n; + result.container = std::move(container_name); + result.repeat = repeat; + result.seed = seed; + result.operations = trace.mixed.size(); + result.read_count = trace.read_count; + result.find_count = trace.find_count; + result.find_hit_count = trace.find_hit_count; + result.find_miss_count = trace.find_miss_count; + result.insert_count = trace.insert_count; + result.erase_count = trace.erase_count; + result.read_batch_count = trace.reads.size(); + result.find_batch_count = trace.finds.size(); + result.insert_batch_count = trace.inserts.size(); + result.erase_batch_count = trace.erases.size(); + + auto mixed_container = builder(); // Construction is deliberately outside the timer. + result.initial_mode = storage_mode(mixed_container); + result.initial_leaf = active_leaf(mixed_container); + result.initial_logical_capacity = logical_capacity(mixed_container); + result.initial_hash_buckets = hash_bucket_count(mixed_container); + auto mixed_guard = guard_start(1); + const auto mixed_begin = Clock::now(); + for (const auto& operation : trace.mixed) { + switch (operation.kind) { + case OperationKind::read: { + const auto value = indexed_read(mixed_container, operation.index); + guard_event(mixed_guard, 1, operation.index, value); + break; + } + case OperationKind::find: { + const auto index = find_value(mixed_container, operation.value); + guard_event(mixed_guard, 2, index, operation.value); + break; + } + case OperationKind::insert: + insert_at(mixed_container, operation.index, operation.value); + guard_event(mixed_guard, 3, operation.index, operation.value); + break; + case OperationKind::erase: + erase_at(mixed_container, operation.index); + guard_event(mixed_guard, 4, operation.index, operation.value); + break; + } + } + const auto mixed_end = Clock::now(); + result.mixed_ops_per_sec = operations_per_second(trace.mixed.size(), mixed_end - mixed_begin); + result.mode = storage_mode(mixed_container); + result.leaf = active_leaf(mixed_container); + result.logical_capacity = logical_capacity(mixed_container); + result.hash_buckets = hash_bucket_count(mixed_container); + result.bytes = allocated_bytes(mixed_container); + finish_guard(mixed_guard, sequence_digest(mixed_container)); + if (mixed_guard != trace.expected_mixed_guard) { + throw std::runtime_error(result.container + ": mixed workload checksum mismatch"); + } + + auto read_container = builder(); + const auto read_initial_mode = storage_mode(read_container); + auto read_guard = guard_start(2); + const auto read_begin = Clock::now(); + for (const auto index : trace.reads) { + guard_event(read_guard, 1, index, indexed_read(read_container, index)); + } + const auto read_end = Clock::now(); + result.read_ops_per_sec = operations_per_second(trace.reads.size(), read_end - read_begin); + result.read_mode = mode_transition(read_container, read_initial_mode); + result.read_leaf = active_leaf(read_container); + finish_guard(read_guard, sequence_digest(read_container)); + if (read_guard != trace.expected_read_guard) { + throw std::runtime_error(result.container + ": read batch checksum mismatch"); + } + + auto find_container = builder(); + const auto find_initial_mode = storage_mode(find_container); + auto find_guard = guard_start(3); + const auto find_begin = Clock::now(); + for (const auto& query : trace.finds) { + guard_event(find_guard, 2, find_value(find_container, query.value), query.value); + } + const auto find_end = Clock::now(); + result.find_ops_per_sec = operations_per_second(trace.finds.size(), find_end - find_begin); + result.find_mode = mode_transition(find_container, find_initial_mode); + result.find_leaf = active_leaf(find_container); + finish_guard(find_guard, sequence_digest(find_container)); + if (find_guard != trace.expected_find_guard) { + throw std::runtime_error(result.container + ": find batch checksum mismatch"); + } + + auto insert_container = builder(); + const auto insert_initial_mode = storage_mode(insert_container); + auto insert_guard = guard_start(4); + const auto insert_begin = Clock::now(); + for (const auto& operation : trace.inserts) { + insert_at(insert_container, operation.index, operation.value); + guard_event(insert_guard, 3, operation.index, operation.value); + } + const auto insert_end = Clock::now(); + result.insert_ops_per_sec = operations_per_second(trace.inserts.size(), insert_end - insert_begin); + result.insert_mode = mode_transition(insert_container, insert_initial_mode); + result.insert_leaf = active_leaf(insert_container); + finish_guard(insert_guard, sequence_digest(insert_container)); + if (insert_guard != trace.expected_insert_guard) { + throw std::runtime_error(result.container + ": insert batch checksum mismatch"); + } + + auto erase_container = builder(); + const auto erase_initial_mode = storage_mode(erase_container); + auto erase_guard = guard_start(5); + const auto erase_begin = Clock::now(); + for (const auto& operation : trace.erases) { + erase_at(erase_container, operation.index); + guard_event(erase_guard, 4, operation.index, operation.value); + } + const auto erase_end = Clock::now(); + result.erase_ops_per_sec = operations_per_second(trace.erases.size(), erase_end - erase_begin); + result.erase_mode = mode_transition(erase_container, erase_initial_mode); + result.erase_leaf = active_leaf(erase_container); + finish_guard(erase_guard, sequence_digest(erase_container)); + if (erase_guard != trace.expected_erase_guard) { + throw std::runtime_error(result.container + ": erase batch checksum mismatch"); + } + + result.checksum = mixed_guard; + guard_event(result.checksum, 0x61, static_cast(read_guard), + static_cast(read_guard >> 32)); + guard_event(result.checksum, 0x62, static_cast(find_guard), + static_cast(find_guard >> 32)); + guard_event(result.checksum, 0x63, static_cast(insert_guard), + static_cast(insert_guard >> 32)); + guard_event(result.checksum, 0x64, static_cast(erase_guard), + static_cast(erase_guard >> 32)); + benchmark_sink ^= result.checksum; + return result; +} + +Sequence build_forced_vector(const std::vector& initial, uc::TieredConfig config) { + Sequence sequence(config); + sequence.force_vector_mode(); + for (const auto value : initial) { + sequence.push_back(value); + } + return sequence; +} + +Sequence build_forced_tiered(const std::vector& initial, uc::TieredConfig config) { + auto sequence = build_forced_vector(initial, config); + sequence.force_tiered_mode(config); + return sequence; +} + +Sequence build_adaptive(const std::vector& initial, uc::TieredConfig config) { + Sequence sequence(config); + for (const auto value : initial) { + sequence.push_back(value); + } + sequence.enable_auto_mode(); + (void)sequence.adapt_now(); + return sequence; +} + +std::string csv_string(std::string_view text) { + std::string result; + result.reserve(text.size() + 2); + result.push_back('"'); + for (const auto character : text) { + if (character == '"') { + result.push_back('"'); + } + result.push_back(character); + } + result.push_back('"'); + return result; +} + +void write_csv(const std::filesystem::path& path, const std::vector& results) { + if (!path.parent_path().empty()) { + std::filesystem::create_directories(path.parent_path()); + } + std::ofstream output(path, std::ios::binary); + if (!output) { + throw std::runtime_error("cannot open output file: " + path.string()); + } + output + << "profile,n,container,repeat,seed,initial_mode,mixed_mode,initial_leaf,mixed_leaf," + "initial_logical_capacity,logical_capacity,initial_hash_buckets,hash_buckets,operations," + "read_count,find_count,find_hit_count,find_miss_count,insert_count,erase_count," + "read_batch_count,find_batch_count,insert_batch_count,erase_batch_count," + "read_mode,read_leaf,find_mode,find_leaf,insert_mode,insert_leaf,erase_mode,erase_leaf," + "mixed_ops_per_sec,read_ops_per_sec,find_ops_per_sec,insert_ops_per_sec," + "erase_ops_per_sec,bytes,checksum\n"; + output << std::setprecision(17); + for (const auto& result : results) { + output << csv_string(UC_BUILD_PROFILE) << ',' << result.n << ',' + << csv_string(result.container) << ',' << result.repeat << ',' + << result.seed << ',' << csv_string(result.initial_mode) << ',' + << csv_string(result.mode) << ',' << result.initial_leaf << ',' + << result.leaf << ','; + if (result.initial_logical_capacity) { + output << *result.initial_logical_capacity; + } + output << ','; + if (result.logical_capacity) { + output << *result.logical_capacity; + } + output << ','; + if (result.initial_hash_buckets) { + output << *result.initial_hash_buckets; + } + output << ','; + if (result.hash_buckets) { + output << *result.hash_buckets; + } + output << ',' << result.operations << ',' << result.read_count << ',' + << result.find_count << ',' << result.find_hit_count << ',' + << result.find_miss_count << ',' << result.insert_count << ',' + << result.erase_count << ',' << result.read_batch_count << ',' + << result.find_batch_count << ',' << result.insert_batch_count << ',' + << result.erase_batch_count << ',' << csv_string(result.read_mode) << ',' + << result.read_leaf << ',' << csv_string(result.find_mode) << ',' + << result.find_leaf << ',' << csv_string(result.insert_mode) << ',' + << result.insert_leaf << ',' << csv_string(result.erase_mode) << ',' + << result.erase_leaf << ',' << result.mixed_ops_per_sec << ',' + << result.read_ops_per_sec << ',' << result.find_ops_per_sec << ',' + << result.insert_ops_per_sec << ',' << result.erase_ops_per_sec << ',' + << result.bytes << ',' << result.checksum << '\n'; + } +} + +double median(std::vector values) { + std::sort(values.begin(), values.end()); + const auto middle = values.size() / 2; + if ((values.size() & 1U) != 0) { + return values[middle]; + } + return (values[middle - 1] + values[middle]) / 2.0; +} + +void print_summary(const std::vector& results, + const std::vector& sizes) { + constexpr std::string_view containers[] = { + "forced_vector_hash", "forced_tiered_hash", "adaptive_hash" + }; + if (!results.empty()) { + const auto& sample = results.front(); + std::cout << "\nMixed trace: " << sample.read_count << " indexed reads, " + << sample.find_count << " flat-hash find_one calls (" + << sample.find_hit_count << " hit / " << sample.find_miss_count + << " miss), " << sample.insert_count << " inserts, " + << sample.erase_count << " erases.\n"; + } + std::cout + << "\nMedian throughput (operations/second)\n" + "| n | container | mixed mode | mixed leaf | mixed ops/s | read ops/s | find ops/s | insert ops/s | erase ops/s |\n" + "|---:|:----------|:-----|-----:|------------:|-----------:|-----------:|-------------:|------------:|\n"; + for (const auto n : sizes) { + for (const auto name : containers) { + std::vector mixed; + std::vector reads; + std::vector finds; + std::vector inserts; + std::vector erases; + const Result* representative = nullptr; + for (const auto& result : results) { + if (result.n != n || result.container != name) { + continue; + } + representative = &result; + mixed.push_back(result.mixed_ops_per_sec); + reads.push_back(result.read_ops_per_sec); + finds.push_back(result.find_ops_per_sec); + inserts.push_back(result.insert_ops_per_sec); + erases.push_back(result.erase_ops_per_sec); + } + if (!representative) { + continue; + } + const auto displayed_mode = representative->initial_mode == representative->mode + ? representative->mode + : representative->initial_mode + "->" + representative->mode; + std::cout << "| " << n << " | " << name << " | " + << displayed_mode << " | " << representative->leaf << " | " + << std::fixed << std::setprecision(0) + << median(std::move(mixed)) << " | " << median(std::move(reads)) << " | " + << median(std::move(finds)) << " | " << median(std::move(inserts)) << " | " + << median(std::move(erases)) << " |\n"; + } + } +} + +std::uint64_t repeat_seed(std::uint64_t base, std::size_t n, std::size_t repeat) noexcept { + auto value = base ^ (static_cast(n) * 0x9e37'79b9'7f4a'7c15ULL); + value ^= static_cast(repeat + 1) * 0xbf58'476d'1ce4'e5b9ULL; + value ^= value >> 30; + value *= 0xbf58'476d'1ce4'e5b9ULL; + value ^= value >> 27; + value *= 0x94d0'49bb'1331'11ebULL; + return value ^ (value >> 31); +} + +} // namespace + +int main(int argc, char** argv) { + try { + const auto options = parse_options(argc, argv); + const auto sizes = power_of_two_sizes(options); + std::vector results; + results.reserve(sizes.size() * options.repeats * 3); + + const auto total_cells = sizes.size() * options.repeats; + std::size_t completed_cells = 0; + for (const auto n : sizes) { + const uc::TieredConfig config{ceil_sqrt(n), 64, 4}; + for (std::size_t repeat = 0; repeat < options.repeats; ++repeat) { + const auto seed = repeat_seed(options.seed, n, repeat); + const auto trace = make_trace(n, options.operations, seed); + + // Rotate the candidate order per paired repeat so cache state, + // thermal drift, and a fixed first/last position do not always + // favour the same representation. + const auto first_candidate = static_cast(seed % 3); + for (std::size_t offset = 0; offset < 3; ++offset) { + switch ((first_candidate + offset) % 3) { + case 0: + results.push_back(run_candidate( + trace, n, repeat, seed, "forced_vector_hash", + [&] { return build_forced_vector(trace.initial, config); })); + break; + case 1: + results.push_back(run_candidate( + trace, n, repeat, seed, "forced_tiered_hash", + [&] { return build_forced_tiered(trace.initial, config); })); + break; + default: + results.push_back(run_candidate( + trace, n, repeat, seed, "adaptive_hash", + [&] { return build_adaptive(trace.initial, config); })); + break; + } + } + + ++completed_cells; + const auto percent = completed_cells * 100 / total_cells; + std::cerr << "\r[" << std::setw(3) << percent << "%] n=" << n + << " repeat=" << (repeat + 1) << '/' << options.repeats << std::flush; + } + } + std::cerr << '\n'; + + write_csv(options.output, results); + print_summary(results, sizes); + std::cout << "\nRaw repeat data: " << options.output.string() << '\n'; + return benchmark_sink == 0x0123'4567'89ab'cdefULL ? 2 : 0; + } catch (const std::exception& error) { + std::cerr << "uc_focused_bench: " << error.what() << '\n'; + return 1; + } +} diff --git a/tests/adaptive_sequence_tests.cpp b/tests/adaptive_sequence_tests.cpp index 961d4e7..dc0fe2b 100644 --- a/tests/adaptive_sequence_tests.cpp +++ b/tests/adaptive_sequence_tests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,12 @@ struct DeferredTransitionPolicy { } }; +struct WraparoundHash { + [[nodiscard]] std::size_t operator()(std::uint32_t) const noexcept { + return std::numeric_limits::max(); + } +}; + template void expect_equal(const Sequence& sequence, const Reference& reference) { EXPECT(sequence.size() == reference.size()); @@ -172,6 +179,157 @@ void indexed_duplicates_and_ids() { EXPECT(values.size() == 2); } +void indexed_ids_reuse_slots_without_reviving_stale_handles() { + uc::AdaptiveSequence values; + for (std::uint32_t value = 0; value < 64; ++value) { + values.push_back(value); + } + + const auto first_id = values.id_at(0); + const auto fixed_allocation = values.allocated_bytes(); + for (std::uint32_t step = 0; step < 2'048; ++step) { + const auto retired = values.id_at(0); + values.erase_by_id(retired); + EXPECT(!values.id_alive(retired)); + + values.push_back(10'000 + step); + const auto replacement = values.id_at(values.size() - 1); + EXPECT(replacement != retired); + EXPECT(values.id_alive(replacement)); + EXPECT(!values.id_alive(retired)); + EXPECT(!values.id_alive(first_id)); + EXPECT(values.find_one(10'000 + step) == values.size() - 1); + EXPECT(values.find_all_ids(10'000 + step) + == std::vector({replacement})); + + bool stale_rejected = false; + try { + values.erase_by_id(retired); + } catch (const std::out_of_range&) { + stale_rejected = true; + } + EXPECT(stale_rejected); + } + + // Churn at a fixed live size must recycle the 64 metadata slots instead + // of growing the ID vector outside a logical-capacity boundary. + EXPECT(values.size() == 64); + EXPECT(values.capacity() == 64); + EXPECT(values.allocated_bytes() == fixed_allocation); + + values.set(0, 0xf00d); + values.set(1, 0xf00d); + const auto duplicate_ids = values.find_all_ids(0xf00d); + EXPECT(duplicate_ids.size() == 2); + EXPECT(values.erase_all(0xf00d) == 2); + for (const auto id : duplicate_ids) { + EXPECT(!values.id_alive(id)); + } + + std::vector ids_before_clear; + ids_before_clear.reserve(values.size()); + for (std::size_t index = 0; index < values.size(); ++index) { + ids_before_clear.push_back(values.id_at(index)); + } + values.clear(); + for (const auto id : ids_before_clear) { + EXPECT(!values.id_alive(id)); + } + EXPECT(!values.id_alive(decltype(values)::invalid_id)); + + values.push_back(77); + const auto after_clear = values.id_at(0); + EXPECT(values.id_alive(after_clear)); + EXPECT(std::find(ids_before_clear.begin(), ids_before_clear.end(), after_clear) + == ids_before_clear.end()); +} + +void moved_from_sequences_are_reusable_and_assignment_invalidates_proxies() { + using Sequence = uc::AdaptiveSequence; + Sequence source({8, 4, 3}); + for (std::uint32_t value = 0; value < 64; ++value) { + source.push_back(value); + } + source.force_tiered_mode(); + const auto transferred_id = source.id_at(17); + + Sequence moved(std::move(source)); + EXPECT(moved.size() == 64); + EXPECT(moved.id_alive(transferred_id)); + EXPECT(source.empty()); + EXPECT(source.capacity() == 0); + EXPECT(source.mode() == uc::StorageMode::vector); + EXPECT(!source.id_alive(transferred_id)); + + for (std::uint32_t value = 0; value < 40; ++value) { + source.push_back(1'000 + value); + } + EXPECT(source.size() == 40); + EXPECT(source.contains(1'039)); + + Sequence destination; + destination.push_back(7); + auto stale_after_move_assignment = destination[0]; + destination = std::move(moved); + bool move_assignment_invalidated = false; + try { + stale_after_move_assignment = 8; + } catch (const std::logic_error&) { + move_assignment_invalidated = true; + } + EXPECT(move_assignment_invalidated); + EXPECT(destination.id_alive(transferred_id)); + EXPECT(moved.empty()); + moved.push_back(2'000); + EXPECT(moved.contains(2'000)); + + Sequence copy_source; + copy_source.push_back(99); + auto stale_after_copy_assignment = destination[0]; + destination = copy_source; + bool copy_assignment_invalidated = false; + try { + stale_after_copy_assignment = 9; + } catch (const std::logic_error&) { + copy_assignment_invalidated = true; + } + EXPECT(copy_assignment_invalidated); + EXPECT(destination.size() == 1); + EXPECT(destination.contains(99)); +} + +void hash_backward_shift_handles_wraparound_and_duplicates() { + using Sequence = uc::AdaptiveSequence; + Sequence values; + std::vector ids; + for (std::uint32_t value = 0; value < 12; ++value) { + values.push_back(value); + ids.push_back(values.id_at(values.size() - 1)); + } + values.push_back(5); + const auto duplicate_id = values.id_at(values.size() - 1); + const auto buckets = values.hash_bucket_count(); + + // Every distinct key starts in the final bucket, so the probe chain wraps + // through bucket zero. Removing a middle key must compact that chain + // without losing later keys or the duplicate list. + values.erase_by_id(ids[3]); + EXPECT(!values.id_alive(ids[3])); + for (std::uint32_t value = 0; value < 12; ++value) { + EXPECT(values.contains(value) == (value != 3)); + } + EXPECT(values.find_all(5).size() == 2); + EXPECT(values.id_alive(ids[5])); + EXPECT(values.id_alive(duplicate_id)); + + values.erase_by_id(ids[5]); + EXPECT(values.contains(5)); + EXPECT(values.find_all_ids(5) + == std::vector({duplicate_id})); + EXPECT(values.hash_bucket_count() == buckets); +} + void indexed_proxy_detects_structural_invalidation() { uc::AdaptiveSequence values({8, 4, 2}); values.push_back(10); @@ -278,47 +436,66 @@ void tiered_shape_rebuild_preserves_order() { EXPECT(values.policy().telemetry().tiered_directory_rebuilds == 2); } -void adaptation_is_deferred_until_a_safe_boundary() { - using sequence_type = uc::AdaptiveSequence< - std::uint32_t, false, DeferredTransitionPolicy>; +void capacity_boundaries_control_mode_and_geometry() { + uc::ResizePolicyConfig policy_config; + policy_config.minimum_tiered_size = 8; + uc::ResizePolicy policy(policy_config, {4, 4, 3}); + uc::AdaptiveSequence values({4, 4, 3}, policy); - sequence_type explicit_maintenance({8, 4, 2}, DeferredTransitionPolicy{}); - for (std::uint32_t value = 0; value < 4; ++value) { - explicit_maintenance.push_back(value); + for (std::uint32_t value = 0; value < 8; ++value) { + values.push_back(value); } - explicit_maintenance.insert(2, 99); - EXPECT(explicit_maintenance.mode() == uc::StorageMode::vector); - EXPECT(explicit_maintenance.policy().decision_ready()); - EXPECT(explicit_maintenance.policy().transitions == 0); - EXPECT(explicit_maintenance.adapt_now()); - EXPECT(explicit_maintenance.mode() == uc::StorageMode::tiered); - EXPECT(explicit_maintenance.policy().decisions == 1); - EXPECT(explicit_maintenance.policy().transitions == 1); - EXPECT(explicit_maintenance.policy().last_from == uc::StorageMode::vector); - EXPECT(explicit_maintenance.policy().last_to == uc::StorageMode::tiered); - EXPECT(!explicit_maintenance.adapt_now()); - EXPECT(!explicit_maintenance.adapt_now()); - EXPECT(explicit_maintenance.policy().decisions == 1); - EXPECT(explicit_maintenance.policy().transitions == 1); - expect_equal(explicit_maintenance, - std::vector({0, 1, 99, 2, 3})); + EXPECT(values.capacity() == 8); + EXPECT(values.mode() == uc::StorageMode::vector); - sequence_type next_mutation({8, 4, 2}, DeferredTransitionPolicy{}); - for (std::uint32_t value = 0; value < 4; ++value) { - next_mutation.push_back(value); + // The ninth element is the next x2 capacity boundary. The representation + // and sqrt(N) leaf are selected as part of that single rebuild. + values.push_back(8); + EXPECT(values.capacity() == 16); + EXPECT(values.mode() == uc::StorageMode::tiered); + EXPECT(values.tiered_config().leaf_capacity == 4); + const auto leaf_at_16 = values.tiered_config().leaf_capacity; + for (std::uint32_t value = 9; value < 16; ++value) { + values.push_back(value); + EXPECT(values.tiered_config().leaf_capacity == leaf_at_16); } - next_mutation.insert(1, 77); - EXPECT(next_mutation.mode() == uc::StorageMode::vector); - EXPECT(next_mutation.policy().transitions == 0); - // The pending recommendation is applied before this append; it is not - // applied after the preceding, already committed insertion. - next_mutation.push_back(4); - EXPECT(next_mutation.mode() == uc::StorageMode::tiered); - EXPECT(next_mutation.policy().decisions == 1); - EXPECT(next_mutation.policy().transitions == 1); - expect_equal(next_mutation, - std::vector({0, 77, 1, 2, 3, 4})); + values.push_back(16); + EXPECT(values.capacity() == 32); + EXPECT(values.mode() == uc::StorageMode::tiered); + EXPECT(values.tiered_config().leaf_capacity == 5); + + // Shrink is exactly one /2 step at 12.5% occupancy. At N=4 the same + // rebuild crosses the cutoff in the opposite direction. + while (values.size() > 4) { + values.erase(values.size() - 1); + } + EXPECT(values.capacity() == 16); + EXPECT(values.mode() == uc::StorageMode::vector); + expect_equal(values, std::vector({0, 1, 2, 3})); + + EXPECT(!values.adapt_now()); + EXPECT(values.mode() == uc::StorageMode::vector); +} + +void calibrated_default_cutoff_is_applied_at_resize() { + uc::AdaptiveSequence values; + for (std::uint32_t value = 0; value < 4'096; ++value) { + values.push_back(value); + } + EXPECT(values.capacity() == 4'096); + EXPECT(values.mode() == uc::StorageMode::vector); + + values.push_back(4'096); + EXPECT(values.capacity() == 8'192); + EXPECT(values.mode() == uc::StorageMode::tiered); + EXPECT(values.tiered_config().leaf_capacity == 65); + + while (values.size() > 1'024) { + values.erase(values.size() - 1); + } + EXPECT(values.capacity() == 4'096); + EXPECT(values.mode() == uc::StorageMode::vector); } std::optional shape_decision( @@ -540,69 +717,42 @@ void policy_changes_shape_with_scale_and_locality() { EXPECT(depth->tiered_config.directory_levels == 3); } -void container_applies_same_mode_shape_decision() { - uc::AdaptationConfig config; - config.evaluation_interval = 64; - config.minimum_observations = 64; - config.minimum_residency_operations = 0; - config.minimum_shape_residency_operations = 0; - config.minimum_shape_edit_fraction = 0.0; - config.required_confirmation_windows = 1; - config.minimum_tiered_size = 1; - config.forecast_operations = 1'000'000; - config.tiered_rebuild_safety = 0.0; - config.minimum_shape_improvement = 0.0; - config.vector_move_unit = 1.0; - const uc::TieredConfig initial_shape{1024, 64, 4}; - uc::AdaptiveSequence values( - initial_shape, uc::CostModelPolicy(config, initial_shape)); - for (std::uint32_t i = 0; i < 10'000; ++i) { - values.push_back(i); - } - values.force_tiered_mode(initial_shape); - values.enable_auto_mode(); +void indexed_hash_capacity_tracks_container_capacity() { + uc::ResizePolicyConfig policy_config; + policy_config.minimum_tiered_size = 32; + uc::AdaptiveSequence values( + {8, 8, 3}, uc::ResizePolicy(policy_config, {8, 8, 3})); - for (std::size_t i = 0; - i < 256 && values.policy().telemetry().tiered_rebuilds == 0; ++i) { - const auto position = (i * 104'729 + 17) % values.size(); - values.insert(position, 0xabcdefu); - values.erase(position); + auto previous_capacity = values.capacity(); + auto previous_buckets = values.hash_bucket_count(); + for (std::uint32_t value = 0; value < 257; ++value) { + values.push_back(value * 16); + EXPECT(values.contains(value * 16)); + if (values.capacity() == previous_capacity) { + EXPECT(values.hash_bucket_count() == previous_buckets); + } else { + EXPECT(values.capacity() == 1 + || values.capacity() == previous_capacity * 2); + EXPECT(values.hash_bucket_count() >= previous_buckets); + previous_capacity = values.capacity(); + previous_buckets = values.hash_bucket_count(); + } } - EXPECT(values.mode() == uc::StorageMode::tiered); - EXPECT(values.policy().telemetry().tiered_rebuilds == 1); - EXPECT(values.tiered_config().leaf_capacity <= 128); - for (std::size_t i = 0; i < values.size(); ++i) { - EXPECT(values[i] == i); + // These keys share low hash bits and exercise backward-shift deletion in + // the linear-probing table without an intermediate rehash. + for (std::uint32_t value = 0; value < 257; value += 2) { + EXPECT(values.erase_one(value * 16)); + } + for (std::uint32_t value = 0; value < 257; ++value) { + EXPECT(values.contains(value * 16) == (value % 2 != 0)); } -} -void policy_switches_for_sustained_middle_edits() { - uc::TieredConfig tiered{32, 16, 3}; - uc::AdaptationConfig adaptation; - adaptation.evaluation_interval = 64; - adaptation.minimum_observations = 64; - adaptation.minimum_residency_operations = 0; - adaptation.minimum_tiered_size = 128; - adaptation.forecast_operations = 16'384; - adaptation.vector_to_tiered_safety = 1.0; - // Make the decision boundary deliberately small; this test checks policy - // plumbing, while machine-calibrated constants are validated by benchmarks. - adaptation.vector_move_unit = 0.20; - adaptation.tiered_move_unit = 0.20; - adaptation.tiered_read_base = 2.0; - uc::AdaptiveSequence values( - tiered, uc::CostModelPolicy(adaptation, tiered)); - - for (std::uint32_t i = 0; i < 512; ++i) { - values.push_back(i); + const auto buckets_before_shrink = values.hash_bucket_count(); + while (values.size() > values.capacity() / 8) { + values.erase(values.size() - 1); } - values.enable_auto_mode(); - for (std::size_t i = 0; i < 256 && values.mode() != uc::StorageMode::tiered; ++i) { - values.insert(values.size() / 2, 7); - values.erase(values.size() / 2); - } - EXPECT(values.mode() == uc::StorageMode::tiered); + EXPECT(values.hash_bucket_count() <= buckets_before_shrink); } } // namespace @@ -611,17 +761,20 @@ int main() { ring_block_offsets(); randomized_differential(); indexed_duplicates_and_ids(); + indexed_ids_reuse_slots_without_reviving_stale_handles(); + moved_from_sequences_are_reusable_and_assignment_invalidates_proxies(); + hash_backward_shift_handles_wraparound_and_duplicates(); indexed_proxy_detects_structural_invalidation(); iterator_and_contiguous_contract(); non_trivial_values(); tiered_shape_rebuild_preserves_order(); - adaptation_is_deferred_until_a_safe_boundary(); + capacity_boundaries_control_mode_and_geometry(); + calibrated_default_cutoff_is_applied_at_resize(); forecast_horizon_is_bounded_by_observed_evidence(); confirmation_requires_consecutive_supporting_windows(); stationary_phases_do_not_thrash(); policy_changes_shape_with_scale_and_locality(); - container_applies_same_mode_shape_decision(); - policy_switches_for_sustained_middle_edits(); + indexed_hash_capacity_tracks_container_capacity(); if (failures != 0) { std::cerr << failures << " test assertion(s) failed\n"; diff --git a/tools/audit_build_flags.ps1 b/tools/audit_build_flags.ps1 index f2da590..a11532f 100644 --- a/tools/audit_build_flags.ps1 +++ b/tools/audit_build_flags.ps1 @@ -6,7 +6,7 @@ param( $ErrorActionPreference = 'Stop' $build = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\out\build\msvc-$Profile")) -$targets = @('uc_demo', 'uc_tests', 'uc_bench') +$targets = @('uc_demo', 'uc_tests', 'uc_bench', 'uc_focused_bench') function Require-Flag( [string]$Command,