New adaptation policy
This commit is contained in:
@@ -89,4 +89,8 @@ if(UC_BUILD_BENCHMARKS)
|
|||||||
add_executable(uc_bench src/benchmark.cpp)
|
add_executable(uc_bench src/benchmark.cpp)
|
||||||
target_link_libraries(uc_bench PRIVATE universal_container)
|
target_link_libraries(uc_bench PRIVATE universal_container)
|
||||||
set_property(TARGET uc_bench PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE)
|
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()
|
endif()
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
# Universal Container
|
# Universal Container
|
||||||
|
|
||||||
Экспериментальный C++20-контейнер индексируемой последовательности, который во
|
Экспериментальный C++20-контейнер индексируемой последовательности с двумя
|
||||||
время работы выбирает представление и его геометрию:
|
представлениями. Текущая default-policy привязывает все автоматические
|
||||||
|
перестройки к изменению логической вместимости:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
contiguous vector <-> segmented tiered storage
|
capacity boundary
|
||||||
|
|-- grow: capacity *= 2 when the current capacity is exhausted
|
||||||
|
`-- shrink: capacity /= 2 at 12.5% occupancy
|
||||||
|
|
|
|
||||||
+-> leaf 64 / 128 / 256 / 512 / 1024
|
+-- small N: contiguous vector
|
||||||
+-> минимально достаточная глубина каталога
|
`-- large N: tiered storage, leaf = ceil(sqrt(N))
|
||||||
```
|
```
|
||||||
|
|
||||||
Это не контейнер с одним «лучшим» размером блока. `CostModelPolicy` собирает
|
Между изменениями вместимости контейнер не меняет ни представление, ни размер
|
||||||
выборочную статистику чтений и полную статистику структурных изменений,
|
leaf. На границе вместимости auto-mode заново выбирает `vector` или tiered,
|
||||||
оценивает стоимость кандидатов и выдаёт `AdaptationDecision`: целевой режим и
|
поэтому обратный переход `tiered -> vector` также возможен. Focused cutoff
|
||||||
`TieredConfig`. Переход выполняется, только если прогнозируемая экономия на
|
равен 4096 элементов; он выбран по baseline focused-run 7 x 100000 для
|
||||||
будущем горизонте превышает стоимость O(N)-перестройки с запасом.
|
текущего `uint32_t` workload и остаётся калибруемой, а не универсальной
|
||||||
|
константой.
|
||||||
|
|
||||||
Проект исследовательский. Сохранённые измерения показывают, что оптимальный
|
Поиск по значению не удалён: `AdaptiveSequence<T, true>` использует flat hash.
|
||||||
leaf действительно меняется с `N`, размером элемента и локальностью правок, но
|
Число hash buckets рассчитывается из логической вместимости с запасом под
|
||||||
пока не доказывают превосходство adaptive-варианта во всех или в среднем по
|
worst-case load не выше 70% и пересматривается только на той же границе
|
||||||
финальной holdout-матрице. Ограничения честно перечислены в
|
вместимости. Это исключает независимый rehash посреди обычной серии операций.
|
||||||
|
|
||||||
|
Старые многомерные benchmark-матрицы и сделанные по ним выводы сохранены только
|
||||||
|
как **historical / retired**. Активная постановка и ограничения описаны в
|
||||||
|
[docs/benchmarking.md](docs/benchmarking.md) и
|
||||||
[docs/findings.md](docs/findings.md).
|
[docs/findings.md](docs/findings.md).
|
||||||
|
|
||||||
`std::deque` и `std::list` присутствуют только как внешние benchmark-baselines.
|
|
||||||
Они не являются режимами контейнера: `AdaptiveSequence` переключается только
|
|
||||||
между contiguous vector и tiered storage.
|
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
Из обычной командной строки Windows:
|
Из обычной командной строки Windows:
|
||||||
@@ -50,7 +54,8 @@ C++». `build.bat` сам находит комплектный CMake из Visua
|
|||||||
|
|
||||||
- `uc_demo.exe` — минимальный пример;
|
- `uc_demo.exe` — минимальный пример;
|
||||||
- `uc_tests.exe` — differential/property-тесты;
|
- `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`). Профиль
|
Все Release-цели собираются с `/O2` и статическим MSVC runtime (`/MT`). Профиль
|
||||||
`scalar` — проверенный для MSVC 19.51 режим `project-novec`: внутренний ключ
|
`scalar` — проверенный для MSVC 19.51 режим `project-novec`: внутренний ключ
|
||||||
@@ -61,16 +66,27 @@ AVX2-тестов и benchmark выполняется проверка CPU/OS;
|
|||||||
безопасно пропускается. Подробности приведены в
|
безопасно пропускается. Подробности приведены в
|
||||||
[docs/benchmarking.md](docs/benchmarking.md).
|
[docs/benchmarking.md](docs/benchmarking.md).
|
||||||
|
|
||||||
Полный воспроизводимый запуск:
|
Текущий воспроизводимый запуск:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
benchmark.bat smoke
|
benchmark.bat
|
||||||
benchmark.bat quick
|
|
||||||
benchmark.bat full
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Для multi-core сравнения `std::vector`, пяти fixed-tiered geometry и adaptive
|
Он собирает и тестирует только профиль `baseline`, создаёт timestamp-папку и
|
||||||
вплоть до 100 миллионов `uint32_t`:
|
один раз запускает `uc_focused_bench` с 100000 операций и 7 повторами. В
|
||||||
|
benchmark сравниваются три варианта одного типа
|
||||||
|
`AdaptiveSequence<uint32_t, true>`: 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\<timestamp>-focused\`. Подтверждённая таблица публикуется
|
||||||
|
вместе с результатами запуска, а не вшивается в документацию до завершения
|
||||||
|
серии.
|
||||||
|
|
||||||
|
Следующие команды относятся к **historical / retired benchmark matrix** и не
|
||||||
|
являются текущим способом калибровки:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
large_benchmark.bat smoke 12
|
large_benchmark.bat smoke 12
|
||||||
@@ -78,13 +94,9 @@ large_benchmark.bat full 12 3
|
|||||||
large_benchmark.bat full 12 3 huge
|
large_benchmark.bat full 12 3 huge
|
||||||
```
|
```
|
||||||
|
|
||||||
Третий аргумент ограничивает число одновременно работающих N=100M-ячеек;
|
Старые CSV оставлены для аудита истории, но напрямую не сравнимы с focused
|
||||||
значение 3 — эвристически более безопасный вариант для машины с 32 GiB RAM.
|
постановкой: в ней другой workload и hash обязателен у каждого кандидата.
|
||||||
Четвёртый аргумент оставляет в матрице только N=100M.
|
`out\` полностью игнорируется Git, результаты исследования — намеренно нет.
|
||||||
|
|
||||||
Каждый запуск создаёт отдельную timestamp-папку в `results\benchmarks\`, куда
|
|
||||||
пишутся raw CSV, сводки и описание окружения. `out\` полностью игнорируется
|
|
||||||
Git, результаты исследования — намеренно нет.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -100,7 +112,7 @@ auto x = values[0];
|
|||||||
values.force_vector_mode();
|
values.force_vector_mode();
|
||||||
values.force_tiered_mode({256, 64, 4});
|
values.force_tiered_mode({256, 64, 4});
|
||||||
values.enable_auto_mode();
|
values.enable_auto_mode();
|
||||||
values.adapt_now(); // явная безопасная maintenance point
|
values.adapt_now(); // default-policy не меняет mode между resize
|
||||||
```
|
```
|
||||||
|
|
||||||
Опциональный flat hash index со stable IDs:
|
Опциональный flat hash index со stable IDs:
|
||||||
|
|||||||
+12
-40
@@ -2,56 +2,28 @@
|
|||||||
setlocal EnableExtensions
|
setlocal EnableExtensions
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
set "UC_SCALE=%~1"
|
if not "%~1"=="" (
|
||||||
if not defined UC_SCALE set "UC_SCALE=smoke"
|
echo Usage: benchmark.bat
|
||||||
if /I not "%UC_SCALE%"=="smoke" if /I not "%UC_SCALE%"=="quick" if /I not "%UC_SCALE%"=="full" (
|
echo.
|
||||||
echo Usage: benchmark.bat [smoke^|quick^|full]
|
echo The retired smoke/quick/full matrix is no longer run by this script.
|
||||||
exit /b 2
|
exit /b 2
|
||||||
)
|
)
|
||||||
|
|
||||||
set "UC_PROFILES=scalar baseline"
|
call build.bat baseline test
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "tools\test_avx2.ps1"
|
if errorlevel 1 exit /b 1
|
||||||
if not errorlevel 1 set "UC_PROFILES=scalar baseline avx2"
|
|
||||||
|
|
||||||
for %%P in (%UC_PROFILES%) do (
|
for /f "delims=" %%I in ('powershell.exe -NoProfile -Command "Get-Date -Format yyyyMMdd-HHmmss"') do set "UC_RUN_ID=%%I-focused"
|
||||||
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%"
|
|
||||||
set "UC_RESULT_DIR=results\benchmarks\%UC_RUN_ID%"
|
set "UC_RESULT_DIR=results\benchmarks\%UC_RUN_ID%"
|
||||||
mkdir "%UC_RESULT_DIR%" 2>nul
|
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
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
for %%P in (%UC_PROFILES%) do (
|
echo.
|
||||||
echo.
|
echo === Focused resize benchmark: baseline /O2, 100000 operations, 7 repeats ===
|
||||||
echo === Tuning %%P /O2 profile ===
|
"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"
|
||||||
"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"
|
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
type "%UC_RESULT_DIR%\summary.md"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Benchmark run completed: %UC_RESULT_DIR%
|
echo Benchmark run completed: %UC_RESULT_DIR%
|
||||||
|
|||||||
+118
-82
@@ -2,101 +2,137 @@
|
|||||||
|
|
||||||
## Представления
|
## Представления
|
||||||
|
|
||||||
`AdaptiveSequence<T>` хранит `variant<vector<T>, TieredStorage<T>>`. Векторный
|
`AdaptiveSequence<T>` хранит `variant<vector<T>, TieredStorage<T>>`.
|
||||||
режим предназначен для random/sequential read и append. Tiered-режим состоит из
|
Contiguous vector выгоден для малых последовательностей и индексного чтения.
|
||||||
независимо выделенных циклических leaf-блоков (`RingBlock`) и многоуровневого
|
Tiered storage состоит из независимо выделенных циклических leaf-блоков
|
||||||
каталога весов. Каталог строит только необходимое число уровней до заданного
|
(`RingBlock`) и многоуровневого каталога весов.
|
||||||
максимума; поиск спускается по уровням, а отдельный Fenwick index восстанавливает
|
|
||||||
логическую позицию stable ID.
|
|
||||||
|
|
||||||
Текущий `TieredStorage` — сегментированный исследовательский вариант, а не
|
У обоих backend есть единая **логическая вместимость** `capacity()`. Это не
|
||||||
полная реализация implicit tiered vector с offsets на каждом внутреннем узле.
|
сумма физического slack в leaf-блоках, а управляющая граница, на которой
|
||||||
Offsets реально используются внутри leaf, но split/merge меняет массив
|
разрешена автоматическая O(N)-перестройка.
|
||||||
дескрипторов leaf и перестраивает каталог. Это важное ограничение: некоторые
|
|
||||||
uniform-edit результаты отражают O(number_of_leaves) обслуживание split, а не
|
|
||||||
теоретическую границу полноценного tiered vector.
|
|
||||||
|
|
||||||
## Адаптивное решение
|
Текущий `TieredStorage` остаётся сегментированным исследовательским вариантом, а
|
||||||
|
не полной реализацией implicit tiered vector с offsets/carry на каждом
|
||||||
|
внутреннем узле. Offsets используются внутри leaf, но split/merge может менять
|
||||||
|
массив leaf-дескрипторов и перестраивать каталог за O(number_of_leaves). Это
|
||||||
|
ограничение нужно учитывать при интерпретации результатов.
|
||||||
|
|
||||||
Storage и policy разделены. Контейнер передаёт `OperationSample`, policy
|
## Текущая default-policy: перестройка только с capacity
|
||||||
возвращает:
|
|
||||||
|
|
||||||
```cpp
|
`ResizePolicy` не пытается распознать workload после каждой операции. В
|
||||||
struct AdaptationDecision {
|
auto-mode представление и геометрия пересматриваются только вместе с изменением
|
||||||
StorageMode target;
|
логической вместимости:
|
||||||
TieredConfig tiered_config;
|
|
||||||
double expected_saving;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Для каждого окна оцениваются vector и все tiered-кандидаты. Главная форма
|
1. Перед вставкой, которая не помещается, capacity удваивается до достаточного
|
||||||
модели edit-cost:
|
значения. Для обычного роста это одна граница `C -> 2C`.
|
||||||
|
2. После удаления при `size <= capacity / 8`, то есть при заполнении не более
|
||||||
|
12.5%, capacity уменьшается на один шаг `C -> C/2` (но не ниже `size`).
|
||||||
|
3. Внутри той же перестройки выбираются mode, leaf geometry и размер hash
|
||||||
|
table. Между такими границами они остаются неизменными.
|
||||||
|
|
||||||
|
В auto-mode целевое представление определяется размером на момент перестройки:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
vector: fixed + move_unit * sizeof(T) * (N - position)
|
target_size < 4096 -> vector
|
||||||
|
target_size >= 4096 -> tiered
|
||||||
tiered: fixed
|
|
||||||
+ move_unit * sizeof(T) * distance_inside_leaf
|
|
||||||
+ directory_unit * locality_multiplier * N / leaf
|
|
||||||
+ lookup(levels)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Первая переменная часть tiered растёт с leaf, вторая — с `N/leaf`. Поэтому
|
`4096` — cutoff, подтверждённый текущим focused-run 7 x 100000 для
|
||||||
минимум сдвигается к большим блокам при росте N. Для близких последовательных
|
`AdaptiveSequence<uint32_t, true>` и заданного mix; это не универсальный
|
||||||
edit-позиций directory multiplier уменьшается: горячая область не создаёт
|
результат для всех типов и workload. Само пересечение `size=4096` немедленного перехода не
|
||||||
split во множестве разных leaf, и policy выбирает меньший блок.
|
вызывает: mode меняется только на следующей capacity-boundary. Например, при
|
||||||
|
обычном последовательном росте контейнер остаётся vector при capacity 4096 и
|
||||||
|
переходит в tiered, когда следующая вставка меняет capacity на 8192. На shrink
|
||||||
|
boundary правило применяется заново, поэтому возможен обратный переход
|
||||||
|
`tiered -> vector`.
|
||||||
|
|
||||||
Для каждого leaf вычисляется минимально достаточная глубина при текущих `N` и
|
Явные `force_vector_mode()`, `force_tiered_mode()` и `reserve()` остаются
|
||||||
fanout. Сравниваются три альтернативы: остаться, перейти в другое
|
пользовательскими управляющими операциями и не являются автоматической
|
||||||
представление, либо перестроить tiered с другой геометрией. Решение требует:
|
адаптацией.
|
||||||
|
|
||||||
|
## Геометрия 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<T, true>` использует 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<T, false>`
|
||||||
|
не несёт его память; тот же 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
|
```text
|
||||||
(current_cost - candidate_cost) * forecast
|
(current_cost - candidate_cost) * forecast
|
||||||
> rebuild_cost * safety_factor
|
> rebuild_cost * safety_factor
|
||||||
```
|
```
|
||||||
|
|
||||||
Для `vector→tiered`, `tiered→vector` и `tiered→tiered` используются разные
|
Также исследовались deferred/eager read adaptation, minimum residency и
|
||||||
safety factor. Дополнительно действуют minimum residency, минимальное улучшение
|
отдельные `vector -> tiered`, `tiered -> vector`, `tiered -> tiered` решения.
|
||||||
shape, EWMA и два подтверждающих окна. После перехода статистическое окно
|
Эта схема и соответствующая benchmark-матрица **retired**: они сохранены для
|
||||||
начинается заново. Все коэффициенты находятся в `AdaptationConfig` и могут
|
воспроизводимости старых CSV и для экспериментов с явно выбранной
|
||||||
заменяться другой policy.
|
`CostModelPolicy`, но не описывают текущую default-policy и не используются для
|
||||||
|
нового cutoff.
|
||||||
## Почему чтение по умолчанию не перестраивает 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<T, true>` использует 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).
|
|
||||||
|
|||||||
+113
-75
@@ -1,99 +1,137 @@
|
|||||||
# Сборка и benchmark-методика
|
# Сборка и benchmark-методика
|
||||||
|
|
||||||
## Профили кода
|
## Активная focused-постановка
|
||||||
|
|
||||||
Все сравниваемые бинарники — Release x64 `/O2`, `/MT`, без IPO/LTO. Отличается
|
Текущий benchmark отвечает на один практический вопрос: при каком `N` для
|
||||||
только ось SIMD:
|
фиксированной малой доли правок становится выгодно переходить от contiguous
|
||||||
|
vector к tiered storage. Старую широкую матрицу типов, SIMD-профилей, leaf и
|
||||||
|
workload для этой калибровки не используют.
|
||||||
|
|
||||||
| Профиль | Ключи проекта | Назначение |
|
Все три кандидата имеют один и тот же тип данных и один и тот же flat hash:
|
||||||
|
|
||||||
|
| Имя в CSV | Реализация | Режим |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `scalar` | `/O2 /d2Qvec- /Qvec-report:1 /Oi-` | MSVC-specific `project-novec` |
|
| `forced_vector_hash` | `AdaptiveSequence<uint32_t, true>` | принудительно vector |
|
||||||
| `baseline` | `/O2`, без `/arch:AVX*` | обычный MSVC x64 |
|
| `forced_tiered_hash` | `AdaptiveSequence<uint32_t, true>` | принудительно tiered |
|
||||||
| `avx2` | `/O2 /arch:AVX2` | разрешён AVX2 |
|
| `adaptive_hash` | `AdaptiveSequence<uint32_t, true>` | текущая resize-bound auto-policy |
|
||||||
|
|
||||||
`/d2Qvec-` — внутренний, недокументированный ключ MSVC, а не переносимая
|
Таким образом, поиск по значению не даёт одному кандидату скрытого преимущества:
|
||||||
гарантия. Командная строка проверена автоматическим аудитом для MSVC 19.51;
|
стоимость хранения и обновления hash index присутствует у vector, tiered и
|
||||||
вывод `/Qvec-report` пока не разбирается как отдельное доказательство. `scalar`
|
adaptive. Для forced tiered начальный leaf равен `ceil(sqrt(N))`. Auto-mode
|
||||||
не означает «ни одной SIMD-инструкции в процессе»: x64 ABI имеет SSE2-baseline,
|
выбирает mode и leaf только при capacity-boundary.
|
||||||
а CRT `memmove` может runtime-dispatch-ить SIMD. Дополнительно задаётся
|
|
||||||
`_USE_STD_VECTOR_ALGORITHMS=0`, чтобы STL не включала свои явно
|
|
||||||
векторизованные алгоритмы. Это честнее называть `project-novec`.
|
|
||||||
|
|
||||||
## Матрица
|
## Трасса
|
||||||
|
|
||||||
Benchmark сравнивает:
|
Mixed trace содержит ровно 100000 операций в default-запуске:
|
||||||
|
|
||||||
- reserved `std::vector`;
|
| Операция | Доля | Default count | Детали |
|
||||||
- `std::deque`;
|
|---|---:|---:|---|
|
||||||
- `std::list` с index semantics;
|
| индексное чтение | 97% | 97000 | случайный существующий индекс |
|
||||||
- forced tiered для каждого leaf 64/128/256/512/1024;
|
| hash find по значению | 1% | 1000 | `find_one`, 50% hit / 50% miss |
|
||||||
- adaptive deferred;
|
| вставка | 1% | 1000 | случайная позиция |
|
||||||
- experimental adaptive eager-read.
|
| удаление | 1% | 1000 | случайная существующая позиция |
|
||||||
|
|
||||||
`std::deque` и `std::list` — только внешние фиксированные baselines;
|
Виды операций перемешиваются детерминированным seed. Вставки и удаления
|
||||||
`AdaptiveSequence` никогда в них не переключается. Для `std::list` runner
|
сбалансированы, поэтому benchmark измеряет steady-size workload, но любая
|
||||||
эмулирует index semantics линейным проходом, поэтому доступ по индексу имеет
|
реально достигнутая capacity-boundary и стоимость перехода остаются внутри
|
||||||
O(N).
|
mixed timer. Построение исходного контейнера находится вне timer.
|
||||||
|
|
||||||
Типы: `uint32_t`, `uint64_t`, trivially-copyable 16/32/64-byte значения и
|
Кроме общей `mixed ops/s`, runner измеряет отдельные homogeneous batches и
|
||||||
non-trivial movable string wrapper. Workloads включают random access, traversal,
|
выводит `read ops/s`, `find ops/s`, `insert ops/s`, `erase ops/s`. Быстрые read
|
||||||
append, 99.99/99/80/50% reads, uniform/localized edits, bursty edits и отдельный
|
и find batches содержат не менее 100000 вызовов для устойчивости таймера;
|
||||||
phase trace:
|
фактические batch counts записываются в CSV. Поэтому отдельные колонки — это
|
||||||
|
пропускная способность соответствующего типа операции, а не время его 1%-доли
|
||||||
|
в mixed trace.
|
||||||
|
|
||||||
```text
|
Один и тот же материализованный trace и seed применяются к трём кандидатам.
|
||||||
uniform edits -> maintenance
|
Порядок кандидатов циклически меняется между paired repeats, чтобы фиксированная
|
||||||
localized edits -> maintenance
|
первая или последняя позиция не принадлежала всегда одному представлению.
|
||||||
reads -> maintenance
|
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, а не только
|
Скрипт выполняет ровно один focused-run:
|
||||||
`vector↔tiered`. Время rebuild остаётся внутри измеряемого end-to-end интервала.
|
|
||||||
|
|
||||||
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\<timestamp>-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
|
## CSV
|
||||||
|
|
||||||
Raw CSV содержит profile/type/workload/N/seed, initial и final tier config,
|
Каждая raw-строка содержит:
|
||||||
total/ns-per-op, p50/p95/p99/max edit latency, checksum, allocated bytes,
|
|
||||||
storage switches, shape rebuilds и final mode. Checksum должен совпасть у всех
|
|
||||||
контейнеров одной трассы.
|
|
||||||
|
|
||||||
`tools/analyze_results.ps1` сначала берёт median повторов, затем сравнивает с
|
- `profile`, `n`, `container`, `repeat`, `seed`;
|
||||||
лучшим фиксированным baseline в каждой ячейке. Итоговый geometric mean считается
|
- initial/final mixed `mode`, `leaf`, `logical_capacity` и отдельные mode/leaf
|
||||||
иерархически по sizes → workloads → types → profiles, чтобы семейство с большим
|
поля 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
|
## Historical / retired benchmark matrix
|
||||||
benchmark.bat smoke rem проверка всего конвейера
|
|
||||||
benchmark.bat quick rem рабочее исследование
|
|
||||||
benchmark.bat full rem финальные повторы, длительный запуск
|
|
||||||
```
|
|
||||||
|
|
||||||
Отдельная large-матрица с обязательным reserved vector, fixed leaf 64…1024 и
|
Старый `uc_bench`, вызовы `benchmark.bat smoke|quick|full`, multi-profile
|
||||||
adaptive запускается внешним process pool. Второй аргумент — число workers:
|
сравнение `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
|
Их CSV сохранены для аудита истории проекта, но не участвуют в выборе текущего
|
||||||
large_benchmark.bat smoke 12
|
cutoff: набор кандидатов, mix операций и наличие hash index отличаются от
|
||||||
large_benchmark.bat full 12 3
|
focused постановки. Старые aggregate scores и speedup нельзя смешивать с новой
|
||||||
large_benchmark.bat full 12 3 huge
|
таблицей operations/second.
|
||||||
```
|
|
||||||
|
|
||||||
Каждый worker закрепляется за отдельным logical CPU. Третий аргумент ограничивает
|
Профили `scalar` и `avx2` по-прежнему можно собирать для отдельных инженерных
|
||||||
число одновременно работающих N=100M-ячеек и лишь снижает риск pagefile; это
|
экспериментов. `scalar` использует внутренний MSVC `/d2Qvec-` и не является
|
||||||
эвристика, а не измеритель доступной памяти. Четвёртый аргумент `huge` оставляет
|
универсальной гарантией отсутствия SIMD в CRT/ABI; это ещё одна причина не
|
||||||
только N=100M. Large raw,
|
расширять текущий baseline-run до старой матрицы без отдельной задачи.
|
||||||
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, а не
|
|
||||||
финальным доказательством.
|
|
||||||
|
|||||||
+48
-5
@@ -1,6 +1,49 @@
|
|||||||
# Наблюдения и ограничения
|
# Наблюдения и ограничения
|
||||||
|
|
||||||
## Что уже установлено
|
## Текущая focused-постановка
|
||||||
|
|
||||||
|
Активное исследование больше не использует старую benchmark-матрицу. Оно
|
||||||
|
сравнивает только три варианта `AdaptiveSequence<uint32_t, true>` с одинаковым
|
||||||
|
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 дал разные оптимумы, поэтому
|
На Ryzen 9 5900X / MSVC 19.51 quick-tuning дал разные оптимумы, поэтому
|
||||||
фиксированный `leaf=512` отвергнут как итоговая стратегия. Для baseline
|
фиксированный `leaf=512` отвергнут как итоговая стратегия. Для baseline
|
||||||
@@ -26,7 +69,7 @@
|
|||||||
доказательство, а сигнал, что старую таблицу leaf нужно заново проверить на
|
доказательство, а сигнал, что старую таблицу leaf нужно заново проверить на
|
||||||
одинаковом quick holdout с тремя профилями.
|
одинаковом quick holdout с тремя профилями.
|
||||||
|
|
||||||
## Hysteresis и итоговый quick-checkpoint
|
### Hysteresis и итоговый quick-checkpoint
|
||||||
|
|
||||||
После v13 выполнены три контрольные серии:
|
После v13 выполнены три контрольные серии:
|
||||||
|
|
||||||
@@ -138,7 +181,7 @@ adaptive-deferred sequential `uint32` найден устойчивый AVX2 cod
|
|||||||
1.85–1.98x относительно baseline во всех пяти повторах при верных checksums.
|
1.85–1.98x относительно baseline во всех пяти повторах при верных checksums.
|
||||||
Он требует отдельного анализа generated code, а не постфактум объяснения SIMD.
|
Он требует отдельного анализа generated code, а не постфактум объяснения SIMD.
|
||||||
|
|
||||||
## Large-scale sweep до 100 миллионов элементов
|
### Large-scale sweep до 100 миллионов элементов
|
||||||
|
|
||||||
Серии [`large v1`](../results/benchmarks/20260811-large-full-12c-v1/README.md)
|
Серии [`large v1`](../results/benchmarks/20260811-large-full-12c-v1/README.md)
|
||||||
и [`seed-jittered v2`](../results/benchmarks/20260811-large-n100m-512edit-12c-v2/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-профили сохранены
|
делят L3/DRAM, а N>1M имеет один repeat на профиль. Поэтому SIMD-профили сохранены
|
||||||
отдельно, но точный AVX2 speedup по этой серии не утверждается.
|
отдельно, но точный AVX2 speedup по этой серии не утверждается.
|
||||||
|
|
||||||
## Чего пока нельзя утверждать
|
### Чего нельзя было утверждать по retired-сериям
|
||||||
|
|
||||||
- Не доказано, что adaptive implementation уже имеет geometric mean > 1 против
|
- Не доказано, что adaptive implementation уже имеет geometric mean > 1 против
|
||||||
лучшего фиксированного контейнера на независимом full holdout.
|
лучшего фиксированного контейнера на независимом full holdout.
|
||||||
@@ -183,7 +226,7 @@ fixed oracle: до первого перехода он успевал выпо
|
|||||||
зафиксированной версии компилятора project-novec, но не публичный контракт
|
зафиксированной версии компилятора project-novec, но не публичный контракт
|
||||||
MSVC и не доказательство отсутствия SIMD внутри CRT или x64 runtime.
|
MSVC и не доказательство отсутствия SIMD внутри CRT или x64 runtime.
|
||||||
|
|
||||||
## Следующая исследовательская граница
|
### Бывшая исследовательская граница
|
||||||
|
|
||||||
Phase-aware hysteresis завершён и прошёл трёхпрофильный quick-validator.
|
Phase-aware hysteresis завершён и прошёл трёхпрофильный quick-validator.
|
||||||
Следующий этап — независимый full holdout с несколькими seeds, affinity/pinning
|
Следующий этап — независимый full holdout с несколькими seeds, affinity/pinning
|
||||||
|
|||||||
@@ -151,6 +151,89 @@ struct AdaptationTelemetry {
|
|||||||
std::size_t last_evidence_windows = 0;
|
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<AdaptationDecision>
|
||||||
|
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<std::size_t>(1,
|
||||||
|
config.minimum_tiered_size);
|
||||||
|
config.shrink_denominator = std::max<std::size_t>(2,
|
||||||
|
config.shrink_denominator);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TieredConfig normalize_tiered(TieredConfig config) noexcept {
|
||||||
|
config.leaf_capacity = std::clamp<std::size_t>(
|
||||||
|
config.leaf_capacity, 4, 1U << 20U);
|
||||||
|
config.directory_fanout = std::clamp<std::size_t>(
|
||||||
|
config.directory_fanout, 2, 1U << 16U);
|
||||||
|
config.directory_levels = std::clamp<std::size_t>(
|
||||||
|
config.directory_levels, 1, 8);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResizePolicyConfig config_{};
|
||||||
|
TieredConfig active_tiered_{};
|
||||||
|
AdaptationTelemetry telemetry_{};
|
||||||
|
};
|
||||||
|
|
||||||
class CostModelPolicy {
|
class CostModelPolicy {
|
||||||
public:
|
public:
|
||||||
static constexpr std::size_t candidate_count = 5;
|
static constexpr std::size_t candidate_count = 5;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <concepts>
|
#include <concepts>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -25,7 +26,7 @@ namespace uc {
|
|||||||
|
|
||||||
template <class T,
|
template <class T,
|
||||||
bool HashIndexEnabled = false,
|
bool HashIndexEnabled = false,
|
||||||
class AdaptationPolicy = CostModelPolicy,
|
class AdaptationPolicy = ResizePolicy,
|
||||||
class Hash = std::hash<T>,
|
class Hash = std::hash<T>,
|
||||||
class Equal = std::equal_to<T>>
|
class Equal = std::equal_to<T>>
|
||||||
class AdaptiveSequence {
|
class AdaptiveSequence {
|
||||||
@@ -54,8 +55,10 @@ private:
|
|||||||
struct IdMetadata {
|
struct IdMetadata {
|
||||||
internal_id previous = invalid_internal_id;
|
internal_id previous = invalid_internal_id;
|
||||||
internal_id next = 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 primary = 0; // vector index or stable leaf id
|
||||||
std::uint32_t secondary = 0; // local leaf index
|
std::uint32_t secondary = 0; // local leaf index
|
||||||
|
std::uint32_t generation = 0;
|
||||||
std::uint8_t flags = 0;
|
std::uint8_t flags = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,6 +68,7 @@ private:
|
|||||||
struct IndexedState {
|
struct IndexedState {
|
||||||
detail::FlatDuplicateIndex<T, Hash, Equal> values;
|
detail::FlatDuplicateIndex<T, Hash, Equal> values;
|
||||||
std::vector<IdMetadata> ids;
|
std::vector<IdMetadata> ids;
|
||||||
|
internal_id free_head = invalid_internal_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NoIndexState {};
|
struct NoIndexState {};
|
||||||
@@ -237,9 +241,53 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdaptiveSequence(const AdaptiveSequence&) = default;
|
AdaptiveSequence(const AdaptiveSequence&) = default;
|
||||||
AdaptiveSequence(AdaptiveSequence&&) noexcept = default;
|
|
||||||
AdaptiveSequence& operator=(const AdaptiveSequence&) = default;
|
AdaptiveSequence(AdaptiveSequence&& other) noexcept
|
||||||
AdaptiveSequence& operator=(AdaptiveSequence&&) noexcept = default;
|
: 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;
|
~AdaptiveSequence() = default;
|
||||||
|
|
||||||
[[nodiscard]] size_type size() const noexcept {
|
[[nodiscard]] size_type size() const noexcept {
|
||||||
@@ -249,6 +297,10 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool empty() const noexcept { return size() == 0; }
|
[[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 {
|
[[nodiscard]] StorageMode mode() const noexcept {
|
||||||
return std::holds_alternative<vector_storage>(storage_)
|
return std::holds_alternative<vector_storage>(storage_)
|
||||||
? StorageMode::vector
|
? StorageMode::vector
|
||||||
@@ -258,13 +310,7 @@ public:
|
|||||||
[[nodiscard]] const TieredConfig& tiered_config() const noexcept { return tiered_config_; }
|
[[nodiscard]] const TieredConfig& tiered_config() const noexcept { return tiered_config_; }
|
||||||
|
|
||||||
reference operator[](size_type index) {
|
reference operator[](size_type index) {
|
||||||
if (read_adaptation_ == ReadAdaptationMode::eager_nonconst) {
|
|
||||||
apply_pending_adaptation();
|
|
||||||
}
|
|
||||||
observe_random_read();
|
observe_random_read();
|
||||||
if (read_adaptation_ == ReadAdaptationMode::eager_nonconst) {
|
|
||||||
apply_pending_adaptation();
|
|
||||||
}
|
|
||||||
if constexpr (HashIndexEnabled) {
|
if constexpr (HashIndexEnabled) {
|
||||||
return reference_proxy(*this, index);
|
return reference_proxy(*this, index);
|
||||||
} else {
|
} else {
|
||||||
@@ -295,7 +341,6 @@ public:
|
|||||||
|
|
||||||
void erase(size_type index) {
|
void erase(size_type index) {
|
||||||
check_index(index);
|
check_index(index);
|
||||||
apply_pending_adaptation();
|
|
||||||
const auto old_size = size();
|
const auto old_size = size();
|
||||||
const auto id = record_id(record_at_unchecked(index));
|
const auto id = record_id(record_at_unchecked(index));
|
||||||
if constexpr (HashIndexEnabled) {
|
if constexpr (HashIndexEnabled) {
|
||||||
@@ -311,13 +356,11 @@ public:
|
|||||||
(void)std::get<tiered_storage>(storage_).erase(index, relocate);
|
(void)std::get<tiered_storage>(storage_).erase(index, relocate);
|
||||||
}
|
}
|
||||||
if constexpr (HashIndexEnabled) {
|
if constexpr (HashIndexEnabled) {
|
||||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
release_id_slot(id);
|
||||||
metadata.flags &= static_cast<std::uint8_t>(~alive_flag);
|
|
||||||
metadata.previous = invalid_internal_id;
|
|
||||||
metadata.next = invalid_internal_id;
|
|
||||||
}
|
}
|
||||||
++generation_;
|
++generation_;
|
||||||
observe_structural_edit(OperationKind::erase, old_size, index);
|
observe_structural_edit(OperationKind::erase, old_size, index);
|
||||||
|
shrink_after_erase();
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(size_type index, const T& value) { set_impl(index, T(value)); }
|
void set(size_type index, const T& value) { set_impl(index, T(value)); }
|
||||||
@@ -325,14 +368,11 @@ public:
|
|||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
if constexpr (HashIndexEnabled) {
|
if constexpr (HashIndexEnabled) {
|
||||||
for (auto& metadata : index_state_.ids) {
|
|
||||||
metadata.flags &= static_cast<std::uint8_t>(~alive_flag);
|
|
||||||
metadata.previous = invalid_internal_id;
|
|
||||||
metadata.next = invalid_internal_id;
|
|
||||||
}
|
|
||||||
index_state_.values.clear();
|
index_state_.values.clear();
|
||||||
|
rebuild_free_id_list();
|
||||||
}
|
}
|
||||||
storage_.template emplace<vector_storage>();
|
storage_.template emplace<vector_storage>();
|
||||||
|
logical_capacity_ = 0;
|
||||||
residency_ = ResidencyMode::automatic;
|
residency_ = ResidencyMode::automatic;
|
||||||
policy_.reset();
|
policy_.reset();
|
||||||
read_sample_countdown_ = read_sample_rate_;
|
read_sample_countdown_ = read_sample_rate_;
|
||||||
@@ -342,9 +382,12 @@ public:
|
|||||||
|
|
||||||
void reserve(size_type capacity) {
|
void reserve(size_type capacity) {
|
||||||
force_vector_mode();
|
force_vector_mode();
|
||||||
std::get<vector_storage>(storage_).reserve(capacity);
|
const auto requested = std::max(capacity, size());
|
||||||
|
std::get<vector_storage>(storage_).reserve(requested);
|
||||||
|
logical_capacity_ = std::max(logical_capacity_, requested);
|
||||||
if constexpr (HashIndexEnabled) {
|
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);
|
index_state_.ids.reserve(index_state_.ids.size() + additional);
|
||||||
}
|
}
|
||||||
++generation_;
|
++generation_;
|
||||||
@@ -364,15 +407,24 @@ public:
|
|||||||
if (!entry || entry->head == invalid_internal_id) {
|
if (!entry || entry->head == invalid_internal_id) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return resolve_id(entry->head);
|
return resolve_internal_id(entry->head);
|
||||||
} else {
|
} else {
|
||||||
for (size_type i = 0; i < size(); ++i) {
|
if (mode() == StorageMode::vector) {
|
||||||
if (equal_(value_at_unchecked(i), value)) {
|
const auto& values = std::get<vector_storage>(storage_);
|
||||||
|
for (size_type i = 0; i < values.size(); ++i) {
|
||||||
|
if (equal_(record_value(values[i]), value)) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
const auto found = std::get<tiered_storage>(storage_).find_if(
|
||||||
|
[&](const record_type& record) {
|
||||||
|
return equal_(record_value(record), value);
|
||||||
|
});
|
||||||
|
return found == tiered_storage::npos
|
||||||
|
? std::nullopt : std::optional<size_type>(found);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::vector<size_type> find_all(const T& value) const {
|
[[nodiscard]] std::vector<size_type> find_all(const T& value) const {
|
||||||
@@ -385,16 +437,27 @@ public:
|
|||||||
result.reserve(entry->count);
|
result.reserve(entry->count);
|
||||||
auto id = entry->head;
|
auto id = entry->head;
|
||||||
while (id != invalid_internal_id) {
|
while (id != invalid_internal_id) {
|
||||||
result.push_back(resolve_id(id));
|
result.push_back(resolve_internal_id(id));
|
||||||
id = index_state_.ids[static_cast<size_type>(id)].next;
|
id = index_state_.ids[static_cast<size_type>(id)].next;
|
||||||
}
|
}
|
||||||
std::sort(result.begin(), result.end());
|
std::sort(result.begin(), result.end());
|
||||||
} else {
|
} else {
|
||||||
for (size_type i = 0; i < size(); ++i) {
|
if (mode() == StorageMode::vector) {
|
||||||
if (equal_(value_at_unchecked(i), value)) {
|
const auto& values = std::get<vector_storage>(storage_);
|
||||||
|
for (size_type i = 0; i < values.size(); ++i) {
|
||||||
|
if (equal_(record_value(values[i]), value)) {
|
||||||
result.push_back(i);
|
result.push_back(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
std::get<tiered_storage>(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;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -411,7 +474,7 @@ public:
|
|||||||
result.reserve(entry->count);
|
result.reserve(entry->count);
|
||||||
auto id = entry->head;
|
auto id = entry->head;
|
||||||
while (id != invalid_internal_id) {
|
while (id != invalid_internal_id) {
|
||||||
result.push_back(static_cast<stable_id>(id));
|
result.push_back(stable_id_for_slot(id));
|
||||||
id = index_state_.ids[static_cast<size_type>(id)].next;
|
id = index_state_.ids[static_cast<size_type>(id)].next;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -432,11 +495,11 @@ public:
|
|||||||
if (!entry) {
|
if (!entry) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
std::vector<internal_id> ids;
|
std::vector<stable_id> ids;
|
||||||
ids.reserve(entry->count);
|
ids.reserve(entry->count);
|
||||||
auto id = entry->head;
|
auto id = entry->head;
|
||||||
while (id != invalid_internal_id) {
|
while (id != invalid_internal_id) {
|
||||||
ids.push_back(id);
|
ids.push_back(stable_id_for_slot(id));
|
||||||
id = index_state_.ids[static_cast<size_type>(id)].next;
|
id = index_state_.ids[static_cast<size_type>(id)].next;
|
||||||
}
|
}
|
||||||
for (const auto current : ids) {
|
for (const auto current : ids) {
|
||||||
@@ -460,14 +523,22 @@ public:
|
|||||||
[[nodiscard]] stable_id id_at(size_type index) const
|
[[nodiscard]] stable_id id_at(size_type index) const
|
||||||
requires(HashIndexEnabled) {
|
requires(HashIndexEnabled) {
|
||||||
check_index(index);
|
check_index(index);
|
||||||
return static_cast<stable_id>(record_at_unchecked(index).id);
|
return stable_id_for_slot(record_at_unchecked(index).id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool id_alive(stable_id id) const noexcept
|
[[nodiscard]] bool id_alive(stable_id id) const noexcept
|
||||||
requires(HashIndexEnabled) {
|
requires(HashIndexEnabled) {
|
||||||
return id <= static_cast<stable_id>(std::numeric_limits<internal_id>::max())
|
if (id == invalid_id) {
|
||||||
&& id < index_state_.ids.size()
|
return false;
|
||||||
&& (index_state_.ids[static_cast<size_type>(id)].flags & alive_flag) != 0;
|
}
|
||||||
|
const auto slot = slot_from_stable_id(id);
|
||||||
|
if (slot == invalid_internal_id
|
||||||
|
|| static_cast<size_type>(slot) >= index_state_.ids.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& metadata = index_state_.ids[static_cast<size_type>(slot)];
|
||||||
|
return metadata.generation == generation_from_stable_id(id)
|
||||||
|
&& (metadata.flags & alive_flag) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void erase_by_id(stable_id id)
|
void erase_by_id(stable_id id)
|
||||||
@@ -532,12 +603,11 @@ public:
|
|||||||
return read_adaptation_;
|
return read_adaptation_;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool adapt_now() {
|
// Automatic rebuilds are capacity-boundary operations. This compatibility
|
||||||
if (residency_ != ResidencyMode::automatic) {
|
// hook therefore never changes the representation between two resize
|
||||||
return false;
|
// events; explicit force_* calls remain available for callers that need an
|
||||||
}
|
// immediate manual conversion.
|
||||||
return apply_pending_adaptation();
|
bool adapt_now() noexcept { return false; }
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] T* data() noexcept
|
[[nodiscard]] T* data() noexcept
|
||||||
requires(!HashIndexEnabled) {
|
requires(!HashIndexEnabled) {
|
||||||
@@ -625,10 +695,29 @@ public:
|
|||||||
return bytes;
|
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]] AdaptationPolicy& policy() noexcept { return policy_; }
|
||||||
[[nodiscard]] const AdaptationPolicy& policy() const noexcept { return policy_; }
|
[[nodiscard]] const AdaptationPolicy& policy() const noexcept { return policy_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void reset_after_move() noexcept {
|
||||||
|
storage_.template emplace<vector_storage>();
|
||||||
|
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,
|
static void set_location(IdMetadata& metadata,
|
||||||
bool tiered,
|
bool tiered,
|
||||||
std::uint32_t primary,
|
std::uint32_t primary,
|
||||||
@@ -667,14 +756,88 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record_type make_record(T value) {
|
static constexpr unsigned stable_id_slot_bits =
|
||||||
if constexpr (HashIndexEnabled) {
|
std::numeric_limits<internal_id>::digits;
|
||||||
|
static_assert(stable_id_slot_bits == 32);
|
||||||
|
|
||||||
|
[[nodiscard]] static internal_id slot_from_stable_id(stable_id id) noexcept {
|
||||||
|
return static_cast<internal_id>(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static std::uint32_t generation_from_stable_id(
|
||||||
|
stable_id id) noexcept {
|
||||||
|
return static_cast<std::uint32_t>(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<size_type>(slot)].generation;
|
||||||
|
return (static_cast<stable_id>(generation) << stable_id_slot_bits)
|
||||||
|
| static_cast<stable_id>(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<size_type>(id)];
|
||||||
|
index_state_.free_head = metadata.free_next;
|
||||||
|
assert(metadata.generation
|
||||||
|
< std::numeric_limits<std::uint32_t>::max());
|
||||||
|
const auto next_generation = metadata.generation + 1;
|
||||||
|
metadata = {};
|
||||||
|
metadata.generation = next_generation;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
if (index_state_.ids.size() >= static_cast<size_type>(invalid_internal_id)) {
|
if (index_state_.ids.size() >= static_cast<size_type>(invalid_internal_id)) {
|
||||||
throw std::length_error("stable id space exhausted");
|
throw std::length_error("stable id slot space exhausted");
|
||||||
}
|
}
|
||||||
const auto id = static_cast<internal_id>(index_state_.ids.size());
|
const auto id = static_cast<internal_id>(index_state_.ids.size());
|
||||||
index_state_.ids.emplace_back();
|
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<size_type>(id) < index_state_.ids.size());
|
||||||
|
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||||
|
const auto generation = metadata.generation;
|
||||||
|
metadata = {};
|
||||||
|
metadata.generation = generation;
|
||||||
|
if (generation != std::numeric_limits<std::uint32_t>::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<internal_id>(index - 1);
|
||||||
|
auto& metadata = index_state_.ids[index - 1];
|
||||||
|
const auto generation = metadata.generation;
|
||||||
|
metadata = {};
|
||||||
|
metadata.generation = generation;
|
||||||
|
if (generation != std::numeric_limits<std::uint32_t>::max()) {
|
||||||
|
metadata.free_next = index_state_.free_head;
|
||||||
|
index_state_.free_head = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record_type make_record(T value) {
|
||||||
|
if constexpr (HashIndexEnabled) {
|
||||||
|
const auto id = acquire_id_slot();
|
||||||
|
try {
|
||||||
return IndexedRecord{std::move(value), id};
|
return IndexedRecord{std::move(value), id};
|
||||||
|
} catch (...) {
|
||||||
|
release_id_slot(id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -707,22 +870,39 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void push_back_impl(T value) {
|
void push_back_impl(T value) {
|
||||||
apply_pending_adaptation();
|
|
||||||
const auto old_size = size();
|
const auto old_size = size();
|
||||||
|
grow_before_insert(old_size + 1);
|
||||||
auto record = make_record(std::move(value));
|
auto record = make_record(std::move(value));
|
||||||
const auto id = record_id(record);
|
const auto id = record_id(record);
|
||||||
auto relocate = relocation_callback();
|
auto relocate = relocation_callback();
|
||||||
|
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) {
|
if (mode() == StorageMode::vector) {
|
||||||
auto& values = std::get<vector_storage>(storage_);
|
auto& values = std::get<vector_storage>(storage_);
|
||||||
values.push_back(std::move(record));
|
values.push_back(std::move(record));
|
||||||
refresh_vector_locations(old_size);
|
refresh_vector_locations(old_size);
|
||||||
} else {
|
} else {
|
||||||
std::get<tiered_storage>(storage_).push_back(std::move(record), relocate);
|
std::get<tiered_storage>(storage_).push_back(
|
||||||
|
std::move(record), relocate);
|
||||||
}
|
}
|
||||||
if constexpr (HashIndexEnabled) {
|
} catch (...) {
|
||||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
unlink_value(rollback_key, id);
|
||||||
metadata.flags |= alive_flag;
|
throw;
|
||||||
link_value(value_at_unchecked(old_size), id);
|
}
|
||||||
|
index_state_.ids[static_cast<size_type>(id)].flags |= alive_flag;
|
||||||
|
} catch (...) {
|
||||||
|
release_id_slot(id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
} else if (mode() == StorageMode::vector) {
|
||||||
|
std::get<vector_storage>(storage_).push_back(std::move(record));
|
||||||
|
} else {
|
||||||
|
std::get<tiered_storage>(storage_).push_back(std::move(record), relocate);
|
||||||
}
|
}
|
||||||
++generation_;
|
++generation_;
|
||||||
policy_.observe({OperationKind::append, old_size, old_size, 1, sizeof(T)});
|
policy_.observe({OperationKind::append, old_size, old_size, 1, sizeof(T)});
|
||||||
@@ -732,22 +912,40 @@ private:
|
|||||||
if (index > size()) {
|
if (index > size()) {
|
||||||
throw std::out_of_range("AdaptiveSequence insertion index out of range");
|
throw std::out_of_range("AdaptiveSequence insertion index out of range");
|
||||||
}
|
}
|
||||||
apply_pending_adaptation();
|
|
||||||
const auto old_size = size();
|
const auto old_size = size();
|
||||||
|
grow_before_insert(old_size + 1);
|
||||||
auto record = make_record(std::move(value));
|
auto record = make_record(std::move(value));
|
||||||
const auto id = record_id(record);
|
const auto id = record_id(record);
|
||||||
auto relocate = relocation_callback();
|
auto relocate = relocation_callback();
|
||||||
|
if constexpr (HashIndexEnabled) {
|
||||||
|
try {
|
||||||
|
T rollback_key(record.value);
|
||||||
|
link_value(record.value, id);
|
||||||
|
try {
|
||||||
if (mode() == StorageMode::vector) {
|
if (mode() == StorageMode::vector) {
|
||||||
auto& values = std::get<vector_storage>(storage_);
|
auto& values = std::get<vector_storage>(storage_);
|
||||||
values.insert(values.begin() + static_cast<difference_type>(index), std::move(record));
|
values.insert(values.begin() + static_cast<difference_type>(index),
|
||||||
|
std::move(record));
|
||||||
refresh_vector_locations(index);
|
refresh_vector_locations(index);
|
||||||
} else {
|
} else {
|
||||||
std::get<tiered_storage>(storage_).insert(index, std::move(record), relocate);
|
std::get<tiered_storage>(storage_).insert(
|
||||||
|
index, std::move(record), relocate);
|
||||||
}
|
}
|
||||||
if constexpr (HashIndexEnabled) {
|
} catch (...) {
|
||||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
unlink_value(rollback_key, id);
|
||||||
metadata.flags |= alive_flag;
|
throw;
|
||||||
link_value(value_at_unchecked(index), id);
|
}
|
||||||
|
index_state_.ids[static_cast<size_type>(id)].flags |= alive_flag;
|
||||||
|
} catch (...) {
|
||||||
|
release_id_slot(id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
} else if (mode() == StorageMode::vector) {
|
||||||
|
auto& values = std::get<vector_storage>(storage_);
|
||||||
|
values.insert(values.begin() + static_cast<difference_type>(index),
|
||||||
|
std::move(record));
|
||||||
|
} else {
|
||||||
|
std::get<tiered_storage>(storage_).insert(index, std::move(record), relocate);
|
||||||
}
|
}
|
||||||
++generation_;
|
++generation_;
|
||||||
observe_structural_edit(OperationKind::insert, old_size, index);
|
observe_structural_edit(OperationKind::insert, old_size, index);
|
||||||
@@ -837,13 +1035,28 @@ private:
|
|||||||
|
|
||||||
[[nodiscard]] size_type resolve_id(stable_id id) const
|
[[nodiscard]] size_type resolve_id(stable_id id) const
|
||||||
requires(HashIndexEnabled) {
|
requires(HashIndexEnabled) {
|
||||||
if (id >= index_state_.ids.size()) {
|
if (id == invalid_id) {
|
||||||
throw std::out_of_range("unknown stable id");
|
throw std::out_of_range("unknown stable id");
|
||||||
}
|
}
|
||||||
const auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
const auto slot = slot_from_stable_id(id);
|
||||||
if ((metadata.flags & alive_flag) == 0) {
|
if (slot == invalid_internal_id
|
||||||
|
|| static_cast<size_type>(slot) >= index_state_.ids.size()) {
|
||||||
|
throw std::out_of_range("unknown stable id");
|
||||||
|
}
|
||||||
|
const auto& metadata = index_state_.ids[static_cast<size_type>(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");
|
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<size_type>(id) < index_state_.ids.size());
|
||||||
|
const auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||||
|
assert((metadata.flags & alive_flag) != 0);
|
||||||
if ((metadata.flags & tiered_flag) == 0) {
|
if ((metadata.flags & tiered_flag) == 0) {
|
||||||
return metadata.primary;
|
return metadata.primary;
|
||||||
}
|
}
|
||||||
@@ -852,51 +1065,171 @@ private:
|
|||||||
metadata.secondary);
|
metadata.secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apply_pending_adaptation() {
|
[[nodiscard]] static size_type ceil_sqrt(size_type value) noexcept {
|
||||||
if (residency_ != ResidencyMode::automatic || !policy_.decision_ready()) {
|
if (value <= 1) {
|
||||||
return false;
|
return value;
|
||||||
|
}
|
||||||
|
auto root = static_cast<size_type>(std::sqrt(static_cast<long double>(value)));
|
||||||
|
while (root > value / root) {
|
||||||
|
--root;
|
||||||
|
}
|
||||||
|
while (root < value / root
|
||||||
|
|| (root <= value / root && root * root < value)) {
|
||||||
|
++root;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (requires(AdaptationPolicy& p, StorageMode current,
|
[[nodiscard]] size_type minimum_tiered_size() const noexcept {
|
||||||
size_type count, TieredConfig config) {
|
if constexpr (requires(const AdaptationPolicy& p) {
|
||||||
p.recommended_decision(current, count, config);
|
p.config().minimum_tiered_size;
|
||||||
}) {
|
}) {
|
||||||
const auto recommendation =
|
return std::max<size_type>(1, policy_.config().minimum_tiered_size);
|
||||||
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;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
convert_to(recommendation->target);
|
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
const auto recommendation = policy_.recommended_mode(mode(), size());
|
return 4 * 1024;
|
||||||
if (!recommendation || *recommendation == mode()) {
|
}
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] size_type shrink_denominator() const noexcept {
|
||||||
|
if constexpr (requires(const AdaptationPolicy& p) {
|
||||||
|
p.config().shrink_denominator;
|
||||||
|
}) {
|
||||||
|
return std::max<size_type>(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<size_type>(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<size_type>::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<record_type>
|
||||||
|
|| std::is_nothrow_move_constructible_v<record_type>) {
|
||||||
|
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 (target_mode == StorageMode::vector) {
|
||||||
|
vector_storage rebuilt;
|
||||||
|
rebuilt.reserve(target_capacity);
|
||||||
|
if (mode() == StorageMode::vector) {
|
||||||
|
auto& source = std::get<vector_storage>(storage_);
|
||||||
|
if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
} else if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||||
|
const auto& source = std::get<tiered_storage>(storage_);
|
||||||
|
source.for_each([&](const record_type& record) {
|
||||||
|
rebuilt.push_back(record);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
auto& source = std::get<tiered_storage>(storage_);
|
||||||
|
source.for_each([&](record_type& record) {
|
||||||
|
rebuilt.push_back(std::move(record));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto source_mode = mode();
|
||||||
|
storage_.template emplace<vector_storage>(std::move(rebuilt));
|
||||||
|
logical_capacity_ = target_capacity;
|
||||||
|
refresh_vector_locations(0);
|
||||||
|
++generation_;
|
||||||
|
if (source_mode != StorageMode::vector) {
|
||||||
|
notify_policy_transition(source_mode, StorageMode::vector);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const auto target_config = geometry_for(target_size);
|
||||||
|
tiered_storage rebuilt;
|
||||||
|
if (mode() == StorageMode::vector) {
|
||||||
|
if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||||
|
rebuilt = tiered_storage::from_vector_copy(
|
||||||
|
std::get<vector_storage>(storage_), target_config);
|
||||||
|
} else {
|
||||||
|
rebuilt = tiered_storage::from_vector(
|
||||||
|
std::move(std::get<vector_storage>(storage_)), target_config);
|
||||||
|
}
|
||||||
|
} else if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||||
|
rebuilt = tiered_storage::reconfigured_copy(
|
||||||
|
std::get<tiered_storage>(storage_), target_config);
|
||||||
|
} else {
|
||||||
|
rebuilt = tiered_storage::reconfigured_move(
|
||||||
|
std::get<tiered_storage>(storage_), target_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto source_mode = mode();
|
||||||
|
tiered_config_ = rebuilt.config();
|
||||||
|
storage_.template emplace<tiered_storage>(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(
|
tiered = tiered_storage::from_vector_copy(
|
||||||
std::get<vector_storage>(storage_), tiered_config_);
|
std::get<vector_storage>(storage_), tiered_config_);
|
||||||
} else {
|
} else {
|
||||||
auto values = std::move(std::get<vector_storage>(storage_));
|
tiered = tiered_storage::from_vector(
|
||||||
tiered = tiered_storage::from_vector(std::move(values), tiered_config_);
|
std::move(std::get<vector_storage>(storage_)), tiered_config_);
|
||||||
}
|
}
|
||||||
tiered_config_ = tiered.config();
|
tiered_config_ = tiered.config();
|
||||||
storage_.template emplace<tiered_storage>(std::move(tiered));
|
storage_.template emplace<tiered_storage>(std::move(tiered));
|
||||||
@@ -924,6 +1257,9 @@ private:
|
|||||||
} else {
|
} else {
|
||||||
values = std::get<tiered_storage>(storage_).to_vector_move();
|
values = std::get<tiered_storage>(storage_).to_vector_move();
|
||||||
}
|
}
|
||||||
|
if (logical_capacity_ > values.capacity()) {
|
||||||
|
values.reserve(logical_capacity_);
|
||||||
|
}
|
||||||
storage_.template emplace<vector_storage>(std::move(values));
|
storage_.template emplace<vector_storage>(std::move(values));
|
||||||
refresh_vector_locations(0);
|
refresh_vector_locations(0);
|
||||||
}
|
}
|
||||||
@@ -955,8 +1291,8 @@ private:
|
|||||||
rebuilt = tiered_storage::reconfigured_copy(
|
rebuilt = tiered_storage::reconfigured_copy(
|
||||||
std::get<tiered_storage>(storage_), target_config);
|
std::get<tiered_storage>(storage_), target_config);
|
||||||
} else {
|
} else {
|
||||||
auto values = std::get<tiered_storage>(storage_).to_vector_move();
|
rebuilt = tiered_storage::reconfigured_move(
|
||||||
rebuilt = tiered_storage::from_vector(std::move(values), target_config);
|
std::get<tiered_storage>(storage_), target_config);
|
||||||
}
|
}
|
||||||
tiered_config_ = rebuilt.config();
|
tiered_config_ = rebuilt.config();
|
||||||
storage_.template emplace<tiered_storage>(std::move(rebuilt));
|
storage_.template emplace<tiered_storage>(std::move(rebuilt));
|
||||||
@@ -1028,6 +1364,7 @@ private:
|
|||||||
[[no_unique_address]] Equal equal_{};
|
[[no_unique_address]] Equal equal_{};
|
||||||
ResidencyMode residency_ = ResidencyMode::automatic;
|
ResidencyMode residency_ = ResidencyMode::automatic;
|
||||||
ReadAdaptationMode read_adaptation_ = ReadAdaptationMode::deferred;
|
ReadAdaptationMode read_adaptation_ = ReadAdaptationMode::deferred;
|
||||||
|
size_type logical_capacity_ = 0;
|
||||||
std::size_t read_sample_rate_ = 256;
|
std::size_t read_sample_rate_ = 256;
|
||||||
mutable std::size_t read_sample_countdown_ = 256;
|
mutable std::size_t read_sample_countdown_ = 256;
|
||||||
std::size_t edit_sample_rate_ = 1;
|
std::size_t edit_sample_rate_ = 1;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -38,14 +39,39 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Entry& ensure(const Key& key) {
|
Entry& ensure(const Key& key) {
|
||||||
maybe_grow();
|
// A moved-from std::vector is allowed to be empty while the scalar
|
||||||
const auto [position, found] = find_insert_position(key);
|
// 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<std::size_t>::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];
|
auto& bucket = buckets_[position];
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
bucket.key.emplace(key);
|
||||||
if (bucket.state == State::tombstone) {
|
if (bucket.state == State::tombstone) {
|
||||||
--tombstones_;
|
--tombstones_;
|
||||||
}
|
}
|
||||||
bucket.key.emplace(key);
|
|
||||||
bucket.entry = {};
|
bucket.entry = {};
|
||||||
bucket.state = State::occupied;
|
bucket.state = State::occupied;
|
||||||
++size_;
|
++size_;
|
||||||
@@ -58,27 +84,62 @@ public:
|
|||||||
if (position == npos) {
|
if (position == npos) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto& bucket = buckets_[position];
|
if constexpr (std::is_nothrow_invocable_v<const Hash&, const Key&>
|
||||||
bucket.key.reset();
|
&& std::is_nothrow_move_assignable_v<Bucket>) {
|
||||||
bucket.entry = {};
|
erase_and_compact(position);
|
||||||
bucket.state = State::tombstone;
|
} else {
|
||||||
--size_;
|
// Computing hashes or moving a key can throw. Rebuild a complete
|
||||||
++tombstones_;
|
// replacement without the erased key, then commit with swap, so a
|
||||||
if (tombstones_ > buckets_.size() / 4) {
|
// failed erase never opens a hole in the live probe chain.
|
||||||
rehash(buckets_.size());
|
erase_transactional(position);
|
||||||
}
|
}
|
||||||
|
--size_;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
// Allocate before committing so a failed reset leaves the live table
|
||||||
|
// and its duplicate metadata untouched.
|
||||||
|
std::vector<Bucket> 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();
|
buckets_.clear();
|
||||||
size_ = 0;
|
size_ = 0;
|
||||||
tombstones_ = 0;
|
tombstones_ = 0;
|
||||||
rehash(16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::size_t distinct_values() const noexcept { return size_; }
|
[[nodiscard]] std::size_t distinct_values() const noexcept { return size_; }
|
||||||
[[nodiscard]] std::size_t bucket_count() const noexcept { return buckets_.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<std::size_t>::max() - 6) / 10
|
||||||
|
? std::numeric_limits<std::size_t>::max()
|
||||||
|
: (element_capacity * 10 + 6) / 7;
|
||||||
|
std::size_t buckets = 16;
|
||||||
|
while (buckets < required) {
|
||||||
|
if (buckets > std::numeric_limits<std::size_t>::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 {
|
[[nodiscard]] std::size_t allocated_bytes() const noexcept {
|
||||||
return buckets_.capacity() * sizeof(Bucket);
|
return buckets_.capacity() * sizeof(Bucket);
|
||||||
}
|
}
|
||||||
@@ -116,10 +177,20 @@ private:
|
|||||||
|
|
||||||
[[nodiscard]] std::pair<std::size_t, bool>
|
[[nodiscard]] std::pair<std::size_t, bool>
|
||||||
find_insert_position(const Key& key) const {
|
find_insert_position(const Key& key) const {
|
||||||
auto position = hasher_(key) & mask();
|
return find_insert_position_in(buckets_, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::pair<std::size_t, bool>
|
||||||
|
find_insert_position_in(const std::vector<Bucket>& 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;
|
auto first_tombstone = npos;
|
||||||
for (std::size_t probe = 0; probe < buckets_.size(); ++probe) {
|
for (std::size_t probe = 0; probe < buckets.size(); ++probe) {
|
||||||
const auto& bucket = buckets_[position];
|
const auto& bucket = buckets[position];
|
||||||
if (bucket.state == State::empty) {
|
if (bucket.state == State::empty) {
|
||||||
return {first_tombstone == npos ? position : first_tombstone, false};
|
return {first_tombstone == npos ? position : first_tombstone, false};
|
||||||
}
|
}
|
||||||
@@ -130,41 +201,92 @@ private:
|
|||||||
} else if (equal_(*bucket.key, key)) {
|
} else if (equal_(*bucket.key, key)) {
|
||||||
return {position, true};
|
return {position, true};
|
||||||
}
|
}
|
||||||
position = (position + 1) & mask();
|
position = (position + 1) & table_mask;
|
||||||
}
|
}
|
||||||
if (first_tombstone != npos) {
|
if (first_tombstone != npos) {
|
||||||
return {first_tombstone, false};
|
return {first_tombstone, false};
|
||||||
}
|
}
|
||||||
throw std::length_error("flat hash index is full");
|
return {npos, false};
|
||||||
}
|
}
|
||||||
|
|
||||||
void maybe_grow() {
|
void erase_and_compact(std::size_t position) {
|
||||||
if ((size_ + tombstones_ + 1) * 100 >= buckets_.size() * 82) {
|
auto hole = position;
|
||||||
rehash(buckets_.size() * 2);
|
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<Bucket> 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) {
|
void rehash(std::size_t requested_capacity) {
|
||||||
std::size_t capacity = 16;
|
std::size_t capacity = 16;
|
||||||
while (capacity < requested_capacity) {
|
while (capacity < requested_capacity) {
|
||||||
|
if (capacity > std::numeric_limits<std::size_t>::max() / 2) {
|
||||||
|
throw std::length_error("flat hash index capacity overflow");
|
||||||
|
}
|
||||||
capacity *= 2;
|
capacity *= 2;
|
||||||
}
|
}
|
||||||
auto old = std::move(buckets_);
|
|
||||||
buckets_.assign(capacity, Bucket{});
|
// Build the complete replacement before touching the live table. An
|
||||||
size_ = 0;
|
// allocation, hash/equality call, or copy of a copyable Key may throw;
|
||||||
tombstones_ = 0;
|
// in all of those cases the original buckets and counters remain
|
||||||
for (auto& bucket : old) {
|
// unchanged. vector::swap is the no-throw commit for std::allocator.
|
||||||
|
std::vector<Bucket> replacement(capacity);
|
||||||
|
std::size_t replacement_size = 0;
|
||||||
|
for (const auto& bucket : buckets_) {
|
||||||
if (bucket.state != State::occupied) {
|
if (bucket.state != State::occupied) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto [position, found] = find_insert_position(*bucket.key);
|
const auto [position, found] =
|
||||||
(void)found;
|
find_insert_position_in(replacement, *bucket.key);
|
||||||
auto& target = buckets_[position];
|
if (position == npos) {
|
||||||
target.key.emplace(std::move(*bucket.key));
|
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.entry = bucket.entry;
|
||||||
target.state = State::occupied;
|
target.state = State::occupied;
|
||||||
++size_;
|
++replacement_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buckets_.swap(replacement);
|
||||||
|
size_ = replacement_size;
|
||||||
|
tombstones_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Hash hasher_{};
|
Hash hasher_{};
|
||||||
|
|||||||
@@ -231,6 +231,31 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <class Predicate>
|
||||||
|
[[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 <class Predicate, class Function>
|
||||||
|
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 <class Function>
|
template <class Function>
|
||||||
void for_each_with_location(Function&& function) const {
|
void for_each_with_location(Function&& function) const {
|
||||||
for (const auto& leaf : leaves_) {
|
for (const auto& leaf : leaves_) {
|
||||||
@@ -262,21 +287,22 @@ public:
|
|||||||
if (source.empty()) {
|
if (source.empty()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const auto target_occupancy = bulk_target_occupancy(
|
// Allocate the complete destination, including its directory, before
|
||||||
result.config_.leaf_capacity);
|
// moving the first value. For nothrow-move T the following loop and
|
||||||
const auto count = (source.size() + target_occupancy - 1)
|
// the final TieredStorage move cannot fail, so an allocation failure
|
||||||
/ target_occupancy;
|
// leaves source completely untouched.
|
||||||
result.leaves_.reserve(count);
|
const auto target_occupancy = result.prepare_bulk_destination(source.size());
|
||||||
|
std::size_t leaf_index = 0;
|
||||||
for (auto& value : source) {
|
for (auto& value : source) {
|
||||||
if (result.leaves_.empty()
|
auto& leaf = *result.leaves_[leaf_index];
|
||||||
|| result.leaves_.back()->values.size() == target_occupancy) {
|
leaf.values.push_back(std::move(value));
|
||||||
result.leaves_.push_back(result.make_leaf());
|
|
||||||
}
|
|
||||||
result.leaves_.back()->values.push_back(std::move(value));
|
|
||||||
++result.size_;
|
++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) {
|
for (std::size_t leaf = 0; leaf < result.leaves_.size(); ++leaf) {
|
||||||
result.refresh_leaf(leaf, relocate);
|
result.refresh_leaf(leaf, relocate);
|
||||||
}
|
}
|
||||||
@@ -335,6 +361,31 @@ public:
|
|||||||
return result;
|
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,
|
[[nodiscard]] std::size_t logical_index(leaf_id_type leaf_id,
|
||||||
std::size_t local) const {
|
std::size_t local) const {
|
||||||
if (leaf_id >= leaf_positions_.size()) {
|
if (leaf_id >= leaf_positions_.size()) {
|
||||||
@@ -387,6 +438,31 @@ private:
|
|||||||
return std::max<std::size_t>(1, capacity - capacity / 8);
|
return std::max<std::size_t>(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<std::size_t> 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<Leaf> make_leaf() {
|
[[nodiscard]] std::unique_ptr<Leaf> make_leaf() {
|
||||||
if (next_leaf_id_ == std::numeric_limits<leaf_id_type>::max()) {
|
if (next_leaf_id_ == std::numeric_limits<leaf_id_type>::max()) {
|
||||||
throw std::length_error("tiered leaf id space exhausted");
|
throw std::length_error("tiered leaf id space exhausted");
|
||||||
|
|||||||
@@ -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<uint32_t, true>` 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).
|
||||||
@@ -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
|
||||||
|
+1
-1
@@ -730,7 +730,7 @@ private:
|
|||||||
}
|
}
|
||||||
return uc::CostModelPolicy(adaptation, config);
|
return uc::CostModelPolicy(adaptation, config);
|
||||||
}
|
}
|
||||||
uc::AdaptiveSequence<T> values_;
|
uc::AdaptiveSequence<T, false, uc::CostModelPolicy> values_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <class Adapter, class T>
|
template <class Adapter, class T>
|
||||||
|
|||||||
@@ -0,0 +1,896 @@
|
|||||||
|
#include "universal_container/adaptive_sequence.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <bit>
|
||||||
|
#include <charconv>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <concepts>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<Value, true>;
|
||||||
|
|
||||||
|
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<std::size_t>::max();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Trace {
|
||||||
|
std::vector<Value> initial;
|
||||||
|
std::vector<Operation> mixed;
|
||||||
|
std::vector<std::size_t> reads;
|
||||||
|
std::vector<FindQuery> finds;
|
||||||
|
std::vector<Operation> inserts;
|
||||||
|
std::vector<Operation> 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<std::size_t> initial_logical_capacity;
|
||||||
|
std::optional<std::size_t> logical_capacity;
|
||||||
|
std::optional<std::size_t> initial_hash_buckets;
|
||||||
|
std::optional<std::size_t> 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<std::size_t>::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 <class Integer>
|
||||||
|
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<std::size_t>(value, option);
|
||||||
|
} else if (option == "--max-n") {
|
||||||
|
options.max_n = parse_integer<std::size_t>(value, option);
|
||||||
|
} else if (option == "--operations") {
|
||||||
|
options.operations = parse_integer<std::size_t>(value, option);
|
||||||
|
} else if (option == "--repeats") {
|
||||||
|
options.repeats = parse_integer<std::size_t>(value, option);
|
||||||
|
} else if (option == "--output") {
|
||||||
|
options.output = std::filesystem::path(value);
|
||||||
|
} else if (option == "--seed") {
|
||||||
|
options.seed = parse_integer<std::uint64_t>(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<std::size_t> power_of_two_sizes(const Options& options) {
|
||||||
|
std::vector<std::size_t> 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::size_t>(std::sqrt(static_cast<long double>(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<std::size_t>(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<std::uint64_t>(index)
|
||||||
|
^ (static_cast<std::uint64_t>(value) << 17)
|
||||||
|
^ (tag << 3);
|
||||||
|
guard += std::rotl(word, static_cast<int>(tag & 7U));
|
||||||
|
guard ^= guard >> 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
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<Value>(container.size()));
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish_guard(std::uint64_t& guard, std::uint64_t state_digest) noexcept {
|
||||||
|
guard_event(guard, 0x7f, static_cast<std::size_t>(state_digest),
|
||||||
|
static_cast<Value>(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<Value>(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<OperationKind> 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<std::ptrdiff_t>(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<std::ptrdiff_t>(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<Value>(
|
||||||
|
std::numeric_limits<Value>::max() - find_ordinal);
|
||||||
|
}
|
||||||
|
guard_event(trace.expected_mixed_guard, 2, operation.index, operation.value);
|
||||||
|
break;
|
||||||
|
case OperationKind::insert:
|
||||||
|
operation.index = static_cast<std::size_t>(random() % (mixed_shadow.size() + 1));
|
||||||
|
operation.value = static_cast<Value>(n + insert_ordinal++);
|
||||||
|
mixed_shadow.insert(mixed_shadow.begin()
|
||||||
|
+ static_cast<std::ptrdiff_t>(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<std::ptrdiff_t>(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<Value>(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<Value>(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<Value>(std::numeric_limits<Value>::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<std::size_t>(random() % (insert_shadow.size() + 1));
|
||||||
|
operation.value = static_cast<Value>(n + i);
|
||||||
|
trace.inserts.push_back(operation);
|
||||||
|
insert_shadow.insert(insert_shadow.begin()
|
||||||
|
+ static_cast<std::ptrdiff_t>(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<Value>(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<std::ptrdiff_t>(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<Value>(erase_shadow.size()));
|
||||||
|
finish_guard(trace.expected_erase_guard, erase_digest);
|
||||||
|
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
Value indexed_read(Container& container, std::size_t index) {
|
||||||
|
return static_cast<Value>(container[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
void insert_at(Container& container, std::size_t index, Value value) {
|
||||||
|
if constexpr (std::same_as<Container, std::vector<Value>>) {
|
||||||
|
container.insert(container.begin() + static_cast<std::ptrdiff_t>(index), value);
|
||||||
|
} else {
|
||||||
|
container.insert(index, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
void erase_at(Container& container, std::size_t index) {
|
||||||
|
if constexpr (std::same_as<Container, std::vector<Value>>) {
|
||||||
|
container.erase(container.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
} else {
|
||||||
|
container.erase(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
std::size_t find_value(const Container& container, Value value) {
|
||||||
|
if constexpr (std::same_as<Container, std::vector<Value>>) {
|
||||||
|
const auto found = std::find(container.begin(), container.end(), value);
|
||||||
|
return found == container.end()
|
||||||
|
? missing_index
|
||||||
|
: static_cast<std::size_t>(found - container.begin());
|
||||||
|
} else {
|
||||||
|
return container.find_one(value).value_or(missing_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
std::optional<std::size_t> logical_capacity(const Container& container) {
|
||||||
|
if constexpr (requires(const Container& value) {
|
||||||
|
{ value.capacity() } -> std::convertible_to<std::size_t>;
|
||||||
|
}) {
|
||||||
|
return static_cast<std::size_t>(container.capacity());
|
||||||
|
} else {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
std::optional<std::size_t> hash_bucket_count(const Container& container) {
|
||||||
|
if constexpr (requires(const Container& value) {
|
||||||
|
{ value.hash_bucket_count() } -> std::convertible_to<std::size_t>;
|
||||||
|
}) {
|
||||||
|
return static_cast<std::size_t>(container.hash_bucket_count());
|
||||||
|
} else {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
std::size_t allocated_bytes(const Container& container) {
|
||||||
|
if constexpr (requires(const Container& value) {
|
||||||
|
{ value.allocated_bytes() } -> std::convertible_to<std::size_t>;
|
||||||
|
}) {
|
||||||
|
return static_cast<std::size_t>(container.allocated_bytes());
|
||||||
|
} else {
|
||||||
|
return sizeof(container) + container.capacity() * sizeof(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Container>
|
||||||
|
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 <class Container>
|
||||||
|
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 <class Container>
|
||||||
|
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<double>(duration).count();
|
||||||
|
return seconds > 0.0 ? static_cast<double>(count) / seconds : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Builder>
|
||||||
|
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<std::size_t>(read_guard),
|
||||||
|
static_cast<Value>(read_guard >> 32));
|
||||||
|
guard_event(result.checksum, 0x62, static_cast<std::size_t>(find_guard),
|
||||||
|
static_cast<Value>(find_guard >> 32));
|
||||||
|
guard_event(result.checksum, 0x63, static_cast<std::size_t>(insert_guard),
|
||||||
|
static_cast<Value>(insert_guard >> 32));
|
||||||
|
guard_event(result.checksum, 0x64, static_cast<std::size_t>(erase_guard),
|
||||||
|
static_cast<Value>(erase_guard >> 32));
|
||||||
|
benchmark_sink ^= result.checksum;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sequence build_forced_vector(const std::vector<Value>& 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<Value>& initial, uc::TieredConfig config) {
|
||||||
|
auto sequence = build_forced_vector(initial, config);
|
||||||
|
sequence.force_tiered_mode(config);
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sequence build_adaptive(const std::vector<Value>& 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<Result>& 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<double> 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<Result>& results,
|
||||||
|
const std::vector<std::size_t>& 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<double> mixed;
|
||||||
|
std::vector<double> reads;
|
||||||
|
std::vector<double> finds;
|
||||||
|
std::vector<double> inserts;
|
||||||
|
std::vector<double> 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<std::uint64_t>(n) * 0x9e37'79b9'7f4a'7c15ULL);
|
||||||
|
value ^= static_cast<std::uint64_t>(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<Result> 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<std::size_t>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
@@ -72,6 +73,12 @@ struct DeferredTransitionPolicy {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct WraparoundHash {
|
||||||
|
[[nodiscard]] std::size_t operator()(std::uint32_t) const noexcept {
|
||||||
|
return std::numeric_limits<std::size_t>::max();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
template <class Sequence, class Reference>
|
template <class Sequence, class Reference>
|
||||||
void expect_equal(const Sequence& sequence, const Reference& reference) {
|
void expect_equal(const Sequence& sequence, const Reference& reference) {
|
||||||
EXPECT(sequence.size() == reference.size());
|
EXPECT(sequence.size() == reference.size());
|
||||||
@@ -172,6 +179,157 @@ void indexed_duplicates_and_ids() {
|
|||||||
EXPECT(values.size() == 2);
|
EXPECT(values.size() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void indexed_ids_reuse_slots_without_reviving_stale_handles() {
|
||||||
|
uc::AdaptiveSequence<std::uint32_t, true> 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<decltype(values)::stable_id>({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<decltype(values)::stable_id> 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<std::uint32_t, true>;
|
||||||
|
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<std::uint32_t, true,
|
||||||
|
uc::ResizePolicy, WraparoundHash>;
|
||||||
|
Sequence values;
|
||||||
|
std::vector<Sequence::stable_id> 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<Sequence::stable_id>({duplicate_id}));
|
||||||
|
EXPECT(values.hash_bucket_count() == buckets);
|
||||||
|
}
|
||||||
|
|
||||||
void indexed_proxy_detects_structural_invalidation() {
|
void indexed_proxy_detects_structural_invalidation() {
|
||||||
uc::AdaptiveSequence<std::uint32_t, true> values({8, 4, 2});
|
uc::AdaptiveSequence<std::uint32_t, true> values({8, 4, 2});
|
||||||
values.push_back(10);
|
values.push_back(10);
|
||||||
@@ -278,47 +436,66 @@ void tiered_shape_rebuild_preserves_order() {
|
|||||||
EXPECT(values.policy().telemetry().tiered_directory_rebuilds == 2);
|
EXPECT(values.policy().telemetry().tiered_directory_rebuilds == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void adaptation_is_deferred_until_a_safe_boundary() {
|
void capacity_boundaries_control_mode_and_geometry() {
|
||||||
using sequence_type = uc::AdaptiveSequence<
|
uc::ResizePolicyConfig policy_config;
|
||||||
std::uint32_t, false, DeferredTransitionPolicy>;
|
policy_config.minimum_tiered_size = 8;
|
||||||
|
uc::ResizePolicy policy(policy_config, {4, 4, 3});
|
||||||
|
uc::AdaptiveSequence<std::uint32_t> values({4, 4, 3}, policy);
|
||||||
|
|
||||||
sequence_type explicit_maintenance({8, 4, 2}, DeferredTransitionPolicy{});
|
for (std::uint32_t value = 0; value < 8; ++value) {
|
||||||
for (std::uint32_t value = 0; value < 4; ++value) {
|
values.push_back(value);
|
||||||
explicit_maintenance.push_back(value);
|
|
||||||
}
|
}
|
||||||
explicit_maintenance.insert(2, 99);
|
EXPECT(values.capacity() == 8);
|
||||||
EXPECT(explicit_maintenance.mode() == uc::StorageMode::vector);
|
EXPECT(values.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<std::uint32_t>({0, 1, 99, 2, 3}));
|
|
||||||
|
|
||||||
sequence_type next_mutation({8, 4, 2}, DeferredTransitionPolicy{});
|
// The ninth element is the next x2 capacity boundary. The representation
|
||||||
for (std::uint32_t value = 0; value < 4; ++value) {
|
// and sqrt(N) leaf are selected as part of that single rebuild.
|
||||||
next_mutation.push_back(value);
|
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
|
values.push_back(16);
|
||||||
// applied after the preceding, already committed insertion.
|
EXPECT(values.capacity() == 32);
|
||||||
next_mutation.push_back(4);
|
EXPECT(values.mode() == uc::StorageMode::tiered);
|
||||||
EXPECT(next_mutation.mode() == uc::StorageMode::tiered);
|
EXPECT(values.tiered_config().leaf_capacity == 5);
|
||||||
EXPECT(next_mutation.policy().decisions == 1);
|
|
||||||
EXPECT(next_mutation.policy().transitions == 1);
|
// Shrink is exactly one /2 step at 12.5% occupancy. At N=4 the same
|
||||||
expect_equal(next_mutation,
|
// rebuild crosses the cutoff in the opposite direction.
|
||||||
std::vector<std::uint32_t>({0, 77, 1, 2, 3, 4}));
|
while (values.size() > 4) {
|
||||||
|
values.erase(values.size() - 1);
|
||||||
|
}
|
||||||
|
EXPECT(values.capacity() == 16);
|
||||||
|
EXPECT(values.mode() == uc::StorageMode::vector);
|
||||||
|
expect_equal(values, std::vector<std::uint32_t>({0, 1, 2, 3}));
|
||||||
|
|
||||||
|
EXPECT(!values.adapt_now());
|
||||||
|
EXPECT(values.mode() == uc::StorageMode::vector);
|
||||||
|
}
|
||||||
|
|
||||||
|
void calibrated_default_cutoff_is_applied_at_resize() {
|
||||||
|
uc::AdaptiveSequence<std::uint32_t> 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<uc::AdaptationDecision> shape_decision(
|
std::optional<uc::AdaptationDecision> shape_decision(
|
||||||
@@ -540,69 +717,42 @@ void policy_changes_shape_with_scale_and_locality() {
|
|||||||
EXPECT(depth->tiered_config.directory_levels == 3);
|
EXPECT(depth->tiered_config.directory_levels == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
void container_applies_same_mode_shape_decision() {
|
void indexed_hash_capacity_tracks_container_capacity() {
|
||||||
uc::AdaptationConfig config;
|
uc::ResizePolicyConfig policy_config;
|
||||||
config.evaluation_interval = 64;
|
policy_config.minimum_tiered_size = 32;
|
||||||
config.minimum_observations = 64;
|
uc::AdaptiveSequence<std::uint32_t, true> values(
|
||||||
config.minimum_residency_operations = 0;
|
{8, 8, 3}, uc::ResizePolicy(policy_config, {8, 8, 3}));
|
||||||
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<std::uint32_t> 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();
|
|
||||||
|
|
||||||
for (std::size_t i = 0;
|
auto previous_capacity = values.capacity();
|
||||||
i < 256 && values.policy().telemetry().tiered_rebuilds == 0; ++i) {
|
auto previous_buckets = values.hash_bucket_count();
|
||||||
const auto position = (i * 104'729 + 17) % values.size();
|
for (std::uint32_t value = 0; value < 257; ++value) {
|
||||||
values.insert(position, 0xabcdefu);
|
values.push_back(value * 16);
|
||||||
values.erase(position);
|
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);
|
// These keys share low hash bits and exercise backward-shift deletion in
|
||||||
EXPECT(values.policy().telemetry().tiered_rebuilds == 1);
|
// the linear-probing table without an intermediate rehash.
|
||||||
EXPECT(values.tiered_config().leaf_capacity <= 128);
|
for (std::uint32_t value = 0; value < 257; value += 2) {
|
||||||
for (std::size_t i = 0; i < values.size(); ++i) {
|
EXPECT(values.erase_one(value * 16));
|
||||||
EXPECT(values[i] == i);
|
}
|
||||||
|
for (std::uint32_t value = 0; value < 257; ++value) {
|
||||||
|
EXPECT(values.contains(value * 16) == (value % 2 != 0));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void policy_switches_for_sustained_middle_edits() {
|
const auto buckets_before_shrink = values.hash_bucket_count();
|
||||||
uc::TieredConfig tiered{32, 16, 3};
|
while (values.size() > values.capacity() / 8) {
|
||||||
uc::AdaptationConfig adaptation;
|
values.erase(values.size() - 1);
|
||||||
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<std::uint32_t> values(
|
|
||||||
tiered, uc::CostModelPolicy(adaptation, tiered));
|
|
||||||
|
|
||||||
for (std::uint32_t i = 0; i < 512; ++i) {
|
|
||||||
values.push_back(i);
|
|
||||||
}
|
}
|
||||||
values.enable_auto_mode();
|
EXPECT(values.hash_bucket_count() <= buckets_before_shrink);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -611,17 +761,20 @@ int main() {
|
|||||||
ring_block_offsets();
|
ring_block_offsets();
|
||||||
randomized_differential();
|
randomized_differential();
|
||||||
indexed_duplicates_and_ids();
|
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();
|
indexed_proxy_detects_structural_invalidation();
|
||||||
iterator_and_contiguous_contract();
|
iterator_and_contiguous_contract();
|
||||||
non_trivial_values();
|
non_trivial_values();
|
||||||
tiered_shape_rebuild_preserves_order();
|
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();
|
forecast_horizon_is_bounded_by_observed_evidence();
|
||||||
confirmation_requires_consecutive_supporting_windows();
|
confirmation_requires_consecutive_supporting_windows();
|
||||||
stationary_phases_do_not_thrash();
|
stationary_phases_do_not_thrash();
|
||||||
policy_changes_shape_with_scale_and_locality();
|
policy_changes_shape_with_scale_and_locality();
|
||||||
container_applies_same_mode_shape_decision();
|
indexed_hash_capacity_tracks_container_capacity();
|
||||||
policy_switches_for_sustained_middle_edits();
|
|
||||||
|
|
||||||
if (failures != 0) {
|
if (failures != 0) {
|
||||||
std::cerr << failures << " test assertion(s) failed\n";
|
std::cerr << failures << " test assertion(s) failed\n";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$build = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\out\build\msvc-$Profile"))
|
$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(
|
function Require-Flag(
|
||||||
[string]$Command,
|
[string]$Command,
|
||||||
|
|||||||
Reference in New Issue
Block a user