New adaptation policy

This commit is contained in:
Efim Beshmenev
2026-08-13 23:42:26 +03:00
parent bbb43dbe57
commit 3770224eaf
16 changed files with 2533 additions and 489 deletions
+118 -82
View File
@@ -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.