New adaptation policy
This commit is contained in:
+118
-82
@@ -2,101 +2,137 @@
|
||||
|
||||
## Представления
|
||||
|
||||
`AdaptiveSequence<T>` хранит `variant<vector<T>, TieredStorage<T>>`. Векторный
|
||||
режим предназначен для random/sequential read и append. Tiered-режим состоит из
|
||||
независимо выделенных циклических leaf-блоков (`RingBlock`) и многоуровневого
|
||||
каталога весов. Каталог строит только необходимое число уровней до заданного
|
||||
максимума; поиск спускается по уровням, а отдельный Fenwick index восстанавливает
|
||||
логическую позицию stable ID.
|
||||
`AdaptiveSequence<T>` хранит `variant<vector<T>, TieredStorage<T>>`.
|
||||
Contiguous vector выгоден для малых последовательностей и индексного чтения.
|
||||
Tiered storage состоит из независимо выделенных циклических leaf-блоков
|
||||
(`RingBlock`) и многоуровневого каталога весов.
|
||||
|
||||
Текущий `TieredStorage` — сегментированный исследовательский вариант, а не
|
||||
полная реализация implicit tiered vector с offsets на каждом внутреннем узле.
|
||||
Offsets реально используются внутри leaf, но split/merge меняет массив
|
||||
дескрипторов leaf и перестраивает каталог. Это важное ограничение: некоторые
|
||||
uniform-edit результаты отражают O(number_of_leaves) обслуживание split, а не
|
||||
теоретическую границу полноценного tiered vector.
|
||||
У обоих backend есть единая **логическая вместимость** `capacity()`. Это не
|
||||
сумма физического slack в leaf-блоках, а управляющая граница, на которой
|
||||
разрешена автоматическая O(N)-перестройка.
|
||||
|
||||
## Адаптивное решение
|
||||
Текущий `TieredStorage` остаётся сегментированным исследовательским вариантом, а
|
||||
не полной реализацией implicit tiered vector с offsets/carry на каждом
|
||||
внутреннем узле. Offsets используются внутри leaf, но split/merge может менять
|
||||
массив leaf-дескрипторов и перестраивать каталог за O(number_of_leaves). Это
|
||||
ограничение нужно учитывать при интерпретации результатов.
|
||||
|
||||
Storage и policy разделены. Контейнер передаёт `OperationSample`, policy
|
||||
возвращает:
|
||||
## Текущая default-policy: перестройка только с capacity
|
||||
|
||||
```cpp
|
||||
struct AdaptationDecision {
|
||||
StorageMode target;
|
||||
TieredConfig tiered_config;
|
||||
double expected_saving;
|
||||
};
|
||||
```
|
||||
`ResizePolicy` не пытается распознать workload после каждой операции. В
|
||||
auto-mode представление и геометрия пересматриваются только вместе с изменением
|
||||
логической вместимости:
|
||||
|
||||
Для каждого окна оцениваются vector и все tiered-кандидаты. Главная форма
|
||||
модели edit-cost:
|
||||
1. Перед вставкой, которая не помещается, capacity удваивается до достаточного
|
||||
значения. Для обычного роста это одна граница `C -> 2C`.
|
||||
2. После удаления при `size <= capacity / 8`, то есть при заполнении не более
|
||||
12.5%, capacity уменьшается на один шаг `C -> C/2` (но не ниже `size`).
|
||||
3. Внутри той же перестройки выбираются mode, leaf geometry и размер hash
|
||||
table. Между такими границами они остаются неизменными.
|
||||
|
||||
В auto-mode целевое представление определяется размером на момент перестройки:
|
||||
|
||||
```text
|
||||
vector: fixed + move_unit * sizeof(T) * (N - position)
|
||||
|
||||
tiered: fixed
|
||||
+ move_unit * sizeof(T) * distance_inside_leaf
|
||||
+ directory_unit * locality_multiplier * N / leaf
|
||||
+ lookup(levels)
|
||||
target_size < 4096 -> vector
|
||||
target_size >= 4096 -> tiered
|
||||
```
|
||||
|
||||
Первая переменная часть tiered растёт с leaf, вторая — с `N/leaf`. Поэтому
|
||||
минимум сдвигается к большим блокам при росте N. Для близких последовательных
|
||||
edit-позиций directory multiplier уменьшается: горячая область не создаёт
|
||||
split во множестве разных leaf, и policy выбирает меньший блок.
|
||||
`4096` — cutoff, подтверждённый текущим focused-run 7 x 100000 для
|
||||
`AdaptiveSequence<uint32_t, true>` и заданного mix; это не универсальный
|
||||
результат для всех типов и workload. Само пересечение `size=4096` немедленного перехода не
|
||||
вызывает: mode меняется только на следующей capacity-boundary. Например, при
|
||||
обычном последовательном росте контейнер остаётся vector при capacity 4096 и
|
||||
переходит в tiered, когда следующая вставка меняет capacity на 8192. На shrink
|
||||
boundary правило применяется заново, поэтому возможен обратный переход
|
||||
`tiered -> vector`.
|
||||
|
||||
Для каждого leaf вычисляется минимально достаточная глубина при текущих `N` и
|
||||
fanout. Сравниваются три альтернативы: остаться, перейти в другое
|
||||
представление, либо перестроить tiered с другой геометрией. Решение требует:
|
||||
Явные `force_vector_mode()`, `force_tiered_mode()` и `reserve()` остаются
|
||||
пользовательскими управляющими операциями и не являются автоматической
|
||||
адаптацией.
|
||||
|
||||
## Геометрия tiered storage
|
||||
|
||||
При capacity-boundary размер малого массива вычисляется из фактического размера
|
||||
последовательности на этой границе:
|
||||
|
||||
```text
|
||||
leaf_capacity = max(4, ceil(sqrt(target_size)))
|
||||
```
|
||||
|
||||
Значения fanout и допустимой глубины каталога берутся из `TieredConfig`.
|
||||
`leaf_capacity` не подстраивается после каждой вставки или удаления: новое
|
||||
`sqrt(N)` применяется только в общей перестройке. Поэтому rebuild одновременно
|
||||
может выполнить `vector -> tiered`, `tiered -> vector` или
|
||||
`tiered -> tiered` с новой геометрией.
|
||||
|
||||
## Flat hash index
|
||||
|
||||
`AdaptiveSequence<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
|
||||
(current_cost - candidate_cost) * forecast
|
||||
> rebuild_cost * safety_factor
|
||||
```
|
||||
|
||||
Для `vector→tiered`, `tiered→vector` и `tiered→tiered` используются разные
|
||||
safety factor. Дополнительно действуют minimum residency, минимальное улучшение
|
||||
shape, EWMA и два подтверждающих окна. После перехода статистическое окно
|
||||
начинается заново. Все коэффициенты находятся в `AdaptationConfig` и могут
|
||||
заменяться другой policy.
|
||||
|
||||
## Почему чтение по умолчанию не перестраивает storage
|
||||
|
||||
`ReadAdaptationMode::deferred` лишь записывает статистику. Pending-решение
|
||||
выполняется на следующей мутации или при явном `adapt_now()`. Это сохраняет
|
||||
важный контракт: обычное чтение не инвалидирует уже выданную ссылку.
|
||||
|
||||
`eager_nonconst` оставлен только как benchmark-эксперимент. Он может выполнить
|
||||
переход вокруг non-const `operator[]`, добавляет проверку в горячий read path и
|
||||
имеет более слабую семантику. Первые измерения не оправдали его как default.
|
||||
|
||||
Для read-only фазы, после которой мутаций нет, вызывающий код может поставить
|
||||
явную maintenance point:
|
||||
|
||||
```cpp
|
||||
values.adapt_now();
|
||||
```
|
||||
|
||||
## Инвалидация
|
||||
|
||||
- const-чтение и сбор статистики в deferred mode ничего не инвалидируют;
|
||||
- `set()` не меняет логические позиции, но ссылка на заменённое значение не
|
||||
должна использоваться как ссылка на старый объект;
|
||||
- любая структурная операция в auto mode может также выполнить rebuild, поэтому
|
||||
инвалидирует все references, pointers и iterators;
|
||||
- conversion, shape rebuild, `force_*`, `reserve`, `clear` и
|
||||
`make_contiguous()` инвалидируют всё позиционное;
|
||||
- stable ID переживает shifts, split и смену представления; он перестаёт быть
|
||||
живым только после удаления элемента или `clear()`.
|
||||
|
||||
Debug-проверка iterator хранит structural generation и бросает `logic_error`
|
||||
после инвалидирования.
|
||||
|
||||
## Hash index
|
||||
|
||||
`AdaptiveSequence<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).
|
||||
Также исследовались deferred/eager read adaptation, minimum residency и
|
||||
отдельные `vector -> tiered`, `tiered -> vector`, `tiered -> tiered` решения.
|
||||
Эта схема и соответствующая benchmark-матрица **retired**: они сохранены для
|
||||
воспроизводимости старых CSV и для экспериментов с явно выбранной
|
||||
`CostModelPolicy`, но не описывают текущую default-policy и не используются для
|
||||
нового cutoff.
|
||||
|
||||
+113
-75
@@ -1,99 +1,137 @@
|
||||
# Сборка и benchmark-методика
|
||||
|
||||
## Профили кода
|
||||
## Активная focused-постановка
|
||||
|
||||
Все сравниваемые бинарники — Release x64 `/O2`, `/MT`, без IPO/LTO. Отличается
|
||||
только ось SIMD:
|
||||
Текущий benchmark отвечает на один практический вопрос: при каком `N` для
|
||||
фиксированной малой доли правок становится выгодно переходить от contiguous
|
||||
vector к tiered storage. Старую широкую матрицу типов, SIMD-профилей, leaf и
|
||||
workload для этой калибровки не используют.
|
||||
|
||||
| Профиль | Ключи проекта | Назначение |
|
||||
Все три кандидата имеют один и тот же тип данных и один и тот же flat hash:
|
||||
|
||||
| Имя в CSV | Реализация | Режим |
|
||||
|---|---|---|
|
||||
| `scalar` | `/O2 /d2Qvec- /Qvec-report:1 /Oi-` | MSVC-specific `project-novec` |
|
||||
| `baseline` | `/O2`, без `/arch:AVX*` | обычный MSVC x64 |
|
||||
| `avx2` | `/O2 /arch:AVX2` | разрешён AVX2 |
|
||||
| `forced_vector_hash` | `AdaptiveSequence<uint32_t, true>` | принудительно vector |
|
||||
| `forced_tiered_hash` | `AdaptiveSequence<uint32_t, true>` | принудительно tiered |
|
||||
| `adaptive_hash` | `AdaptiveSequence<uint32_t, true>` | текущая resize-bound auto-policy |
|
||||
|
||||
`/d2Qvec-` — внутренний, недокументированный ключ MSVC, а не переносимая
|
||||
гарантия. Командная строка проверена автоматическим аудитом для MSVC 19.51;
|
||||
вывод `/Qvec-report` пока не разбирается как отдельное доказательство. `scalar`
|
||||
не означает «ни одной SIMD-инструкции в процессе»: x64 ABI имеет SSE2-baseline,
|
||||
а CRT `memmove` может runtime-dispatch-ить SIMD. Дополнительно задаётся
|
||||
`_USE_STD_VECTOR_ALGORITHMS=0`, чтобы STL не включала свои явно
|
||||
векторизованные алгоритмы. Это честнее называть `project-novec`.
|
||||
Таким образом, поиск по значению не даёт одному кандидату скрытого преимущества:
|
||||
стоимость хранения и обновления hash index присутствует у vector, tiered и
|
||||
adaptive. Для forced tiered начальный leaf равен `ceil(sqrt(N))`. Auto-mode
|
||||
выбирает mode и leaf только при capacity-boundary.
|
||||
|
||||
## Матрица
|
||||
## Трасса
|
||||
|
||||
Benchmark сравнивает:
|
||||
Mixed trace содержит ровно 100000 операций в default-запуске:
|
||||
|
||||
- reserved `std::vector`;
|
||||
- `std::deque`;
|
||||
- `std::list` с index semantics;
|
||||
- forced tiered для каждого leaf 64/128/256/512/1024;
|
||||
- adaptive deferred;
|
||||
- experimental adaptive eager-read.
|
||||
| Операция | Доля | Default count | Детали |
|
||||
|---|---:|---:|---|
|
||||
| индексное чтение | 97% | 97000 | случайный существующий индекс |
|
||||
| hash find по значению | 1% | 1000 | `find_one`, 50% hit / 50% miss |
|
||||
| вставка | 1% | 1000 | случайная позиция |
|
||||
| удаление | 1% | 1000 | случайная существующая позиция |
|
||||
|
||||
`std::deque` и `std::list` — только внешние фиксированные baselines;
|
||||
`AdaptiveSequence` никогда в них не переключается. Для `std::list` runner
|
||||
эмулирует index semantics линейным проходом, поэтому доступ по индексу имеет
|
||||
O(N).
|
||||
Виды операций перемешиваются детерминированным seed. Вставки и удаления
|
||||
сбалансированы, поэтому benchmark измеряет steady-size workload, но любая
|
||||
реально достигнутая capacity-boundary и стоимость перехода остаются внутри
|
||||
mixed timer. Построение исходного контейнера находится вне timer.
|
||||
|
||||
Типы: `uint32_t`, `uint64_t`, trivially-copyable 16/32/64-byte значения и
|
||||
non-trivial movable string wrapper. Workloads включают random access, traversal,
|
||||
append, 99.99/99/80/50% reads, uniform/localized edits, bursty edits и отдельный
|
||||
phase trace:
|
||||
Кроме общей `mixed ops/s`, runner измеряет отдельные homogeneous batches и
|
||||
выводит `read ops/s`, `find ops/s`, `insert ops/s`, `erase ops/s`. Быстрые read
|
||||
и find batches содержат не менее 100000 вызовов для устойчивости таймера;
|
||||
фактические batch counts записываются в CSV. Поэтому отдельные колонки — это
|
||||
пропускная способность соответствующего типа операции, а не время его 1%-доли
|
||||
в mixed trace.
|
||||
|
||||
```text
|
||||
uniform edits -> maintenance
|
||||
localized edits -> maintenance
|
||||
reads -> maintenance
|
||||
Один и тот же материализованный trace и seed применяются к трём кандидатам.
|
||||
Порядок кандидатов циклически меняется между paired repeats, чтобы фиксированная
|
||||
первая или последняя позиция не принадлежала всегда одному представлению.
|
||||
Checksum проверяет совпадение состояния и результатов операций.
|
||||
|
||||
## Capacity, leaf и hash во время измерения
|
||||
|
||||
Focused benchmark проверяет текущий контракт контейнера, а не внешнюю
|
||||
эмуляцию:
|
||||
|
||||
- capacity увеличивается `x2`, когда очередная вставка не помещается;
|
||||
- при `size <= capacity / 8` выполняется один shrink-шаг `capacity / 2`;
|
||||
- mode и `leaf = ceil(sqrt(size))` пересчитываются только внутри этой
|
||||
capacity-перестройки;
|
||||
- focused cutoff auto-mode равен 4096;
|
||||
- hash buckets рассчитываются из новой логической capacity с worst-case load
|
||||
не выше 70% и не меняются независимо между capacity-boundaries.
|
||||
|
||||
Raw CSV сохраняет initial/final mode, leaf и logical capacity mixed-контейнера,
|
||||
что позволяет увидеть включённый в mixed throughput переход `vector -> tiered`
|
||||
или обратный переход. Для четырёх отдельных homogeneous batches поля
|
||||
`read_mode`, `find_mode`, `insert_mode`, `erase_mode` и соответствующие
|
||||
`*_leaf` отдельно фиксируют их фактический режим/геометрию; эти batches
|
||||
стартуют с новых контейнеров и потому не обязаны повторять final mode/leaf
|
||||
mixed-трассы.
|
||||
|
||||
## Воспроизводимый default-run
|
||||
|
||||
Из обычной командной строки Windows:
|
||||
|
||||
```bat
|
||||
benchmark.bat
|
||||
```
|
||||
|
||||
Последний trace нужен именно для проверки смены leaf, а не только
|
||||
`vector↔tiered`. Время rebuild остаётся внутри измеряемого end-to-end интервала.
|
||||
Скрипт выполняет ровно один focused-run:
|
||||
|
||||
Tuning перебирает leaf 64…1024 и maximum directory levels 2/3/4. Каталог сам
|
||||
останавливает построение на минимально достаточной глубине.
|
||||
1. `build.bat baseline test` — Release x64 `/O2`, `/MT`, без `/arch:AVX*` и
|
||||
без IPO/LTO, затем correctness-тесты.
|
||||
2. Создаёт `results\benchmarks\<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
|
||||
|
||||
Raw CSV содержит profile/type/workload/N/seed, initial и final tier config,
|
||||
total/ns-per-op, p50/p95/p99/max edit latency, checksum, allocated bytes,
|
||||
storage switches, shape rebuilds и final mode. Checksum должен совпасть у всех
|
||||
контейнеров одной трассы.
|
||||
Каждая raw-строка содержит:
|
||||
|
||||
`tools/analyze_results.ps1` сначала берёт median повторов, затем сравнивает с
|
||||
лучшим фиксированным baseline в каждой ячейке. Итоговый geometric mean считается
|
||||
иерархически по sizes → workloads → types → profiles, чтобы семейство с большим
|
||||
числом строк не получило лишний вес.
|
||||
- `profile`, `n`, `container`, `repeat`, `seed`;
|
||||
- initial/final mixed `mode`, `leaf`, `logical_capacity` и отдельные mode/leaf
|
||||
поля read/find/insert/erase batches;
|
||||
- mixed counts, включая hit/miss hash find;
|
||||
- реальные размеры отдельных read/find/insert/erase batches;
|
||||
- `mixed_ops_per_sec`, `read_ops_per_sec`, `find_ops_per_sec`,
|
||||
`insert_ops_per_sec`, `erase_ops_per_sec`;
|
||||
- оценку allocated bytes и checksum.
|
||||
|
||||
## Масштабы запуска
|
||||
Публикуемая таблица использует медиану семи повторов по каждой паре
|
||||
`(N, container)`. Значения отдельных повторов не отбрасываются и остаются в
|
||||
raw CSV.
|
||||
|
||||
```bat
|
||||
benchmark.bat smoke rem проверка всего конвейера
|
||||
benchmark.bat quick rem рабочее исследование
|
||||
benchmark.bat full rem финальные повторы, длительный запуск
|
||||
```
|
||||
## Historical / retired benchmark matrix
|
||||
|
||||
Отдельная large-матрица с обязательным reserved vector, fixed leaf 64…1024 и
|
||||
adaptive запускается внешним process pool. Второй аргумент — число workers:
|
||||
Старый `uc_bench`, вызовы `benchmark.bat smoke|quick|full`, multi-profile
|
||||
сравнение `scalar/baseline/avx2`, типы от `uint32_t` до blob/string wrapper,
|
||||
fixed leaf 64...1024, `std::deque`, `std::list`, phase traces и
|
||||
`large_benchmark.bat` до N=100M относятся к **historical / retired** методике.
|
||||
|
||||
```bat
|
||||
large_benchmark.bat smoke 12
|
||||
large_benchmark.bat full 12 3
|
||||
large_benchmark.bat full 12 3 huge
|
||||
```
|
||||
Их CSV сохранены для аудита истории проекта, но не участвуют в выборе текущего
|
||||
cutoff: набор кандидатов, mix операций и наличие hash index отличаются от
|
||||
focused постановки. Старые aggregate scores и speedup нельзя смешивать с новой
|
||||
таблицей operations/second.
|
||||
|
||||
Каждый worker закрепляется за отдельным logical CPU. Третий аргумент ограничивает
|
||||
число одновременно работающих N=100M-ячеек и лишь снижает риск pagefile; это
|
||||
эвристика, а не измеритель доступной памяти. Четвёртый аргумент `huge` оставляет
|
||||
только N=100M. Large raw,
|
||||
per-cell partial/log, план, environment и сводки сохраняются в отдельной папке
|
||||
`results/benchmarks/`.
|
||||
|
||||
`large_benchmark.bat full 12 3 huge` воспроизводит только N=100M с удлинённым
|
||||
512-edit horizon и сам проверяет поддержку AVX2. Прямой PowerShell runner такой
|
||||
CPU-проверки не выполняет: при его ручном запуске `avx2` надо исключить на
|
||||
неподдерживаемой машине.
|
||||
|
||||
Для публикации финального числа нужны одинаковый power plan, отсутствие тяжёлой
|
||||
фоновой нагрузки, закрепление на физическом ядре, randomized paired order и
|
||||
holdout workloads/seeds. Текущие quick-данные являются calibration set, а не
|
||||
финальным доказательством.
|
||||
Профили `scalar` и `avx2` по-прежнему можно собирать для отдельных инженерных
|
||||
экспериментов. `scalar` использует внутренний MSVC `/d2Qvec-` и не является
|
||||
универсальной гарантией отсутствия SIMD в CRT/ABI; это ещё одна причина не
|
||||
расширять текущий baseline-run до старой матрицы без отдельной задачи.
|
||||
|
||||
+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 дал разные оптимумы, поэтому
|
||||
фиксированный `leaf=512` отвергнут как итоговая стратегия. Для baseline
|
||||
@@ -26,7 +69,7 @@
|
||||
доказательство, а сигнал, что старую таблицу leaf нужно заново проверить на
|
||||
одинаковом quick holdout с тремя профилями.
|
||||
|
||||
## Hysteresis и итоговый quick-checkpoint
|
||||
### Hysteresis и итоговый quick-checkpoint
|
||||
|
||||
После v13 выполнены три контрольные серии:
|
||||
|
||||
@@ -138,7 +181,7 @@ adaptive-deferred sequential `uint32` найден устойчивый AVX2 cod
|
||||
1.85–1.98x относительно baseline во всех пяти повторах при верных checksums.
|
||||
Он требует отдельного анализа generated code, а не постфактум объяснения SIMD.
|
||||
|
||||
## Large-scale sweep до 100 миллионов элементов
|
||||
### Large-scale sweep до 100 миллионов элементов
|
||||
|
||||
Серии [`large v1`](../results/benchmarks/20260811-large-full-12c-v1/README.md)
|
||||
и [`seed-jittered v2`](../results/benchmarks/20260811-large-n100m-512edit-12c-v2/README.md)
|
||||
@@ -160,7 +203,7 @@ fixed oracle: до первого перехода он успевал выпо
|
||||
делят L3/DRAM, а N>1M имеет один repeat на профиль. Поэтому SIMD-профили сохранены
|
||||
отдельно, но точный AVX2 speedup по этой серии не утверждается.
|
||||
|
||||
## Чего пока нельзя утверждать
|
||||
### Чего нельзя было утверждать по retired-сериям
|
||||
|
||||
- Не доказано, что adaptive implementation уже имеет geometric mean > 1 против
|
||||
лучшего фиксированного контейнера на независимом full holdout.
|
||||
@@ -183,7 +226,7 @@ fixed oracle: до первого перехода он успевал выпо
|
||||
зафиксированной версии компилятора project-novec, но не публичный контракт
|
||||
MSVC и не доказательство отсутствия SIMD внутри CRT или x64 runtime.
|
||||
|
||||
## Следующая исследовательская граница
|
||||
### Бывшая исследовательская граница
|
||||
|
||||
Phase-aware hysteresis завершён и прошёл трёхпрофильный quick-validator.
|
||||
Следующий этап — независимый full holdout с несколькими seeds, affinity/pinning
|
||||
|
||||
Reference in New Issue
Block a user