242 lines
11 KiB
Markdown
242 lines
11 KiB
Markdown
# Исследование алгоритма разрешения зависимостей в ALT RPM
|
||
|
||
## Замысел и структура статьи
|
||
|
||
Статья будет как решение одной формальной задачи. В ALT RPM пакет-потребитель задаёт множество
|
||
требуемых символов $R$ (`Requires`), а пакет-поставщик - множество предоставляемых
|
||
символов $P$ (`Provides`). Зависимость выполнима тогда и только тогда, когда
|
||
|
||
$$
|
||
R \subseteq P.
|
||
$$
|
||
|
||
Явное перечисление тысяч ELF-, Python- и файловых символов раздувает RPM-метаданные,
|
||
поэтому множества представлены компактными set-строками.
|
||
|
||
## 1. Постановка задачи и данные ALT Linux
|
||
|
||
Ввести обозначения для bpp(b), множеств, хэш функций(H), длин строк и т.д.
|
||
|
||
Проверяемое программой условие имеет вид
|
||
|
||
$$
|
||
H_b(R)\subseteq H_b(P).
|
||
$$
|
||
|
||
Отмечаем вероятностную природу: из $R\subseteq P$ следует
|
||
$H_b(R)\subseteq H_b(P)$, поэтому коллизия не создаёт ложного отказа. Обратное
|
||
неверно: отсутствующий символ может совпасть по хешу с символом из $P$ и дать
|
||
ложное принятие зависимости.
|
||
|
||
(Представить данные ниже табличкой)
|
||
Привести срез Sisyphus `x86_64 + noarch`: 47 367 пакетов, 14 761 `Provides`-отношение и 68 857
|
||
`Requires`-отношений. Для 68 198 реально сопоставимых пар медианы равны
|
||
$p=480$, $r=13$, $p/r=28.1$; p75 - 1886, 37 и 80; p90 - 6219, 104 и 257.
|
||
В 92.4% пар выполняется $p/r\ge 4$. Это к теме, почему рассматриваем разреженные множества.
|
||
|
||
## 2. Устройство существующей `set:`-строки
|
||
|
||
Даём схему формата:
|
||
|
||
```
|
||
массив строк
|
||
| (Jenkins OAAT)
|
||
v
|
||
массив хэшей
|
||
| (qsort)
|
||
v
|
||
отсортированный массив хэшей
|
||
| (вычисление разницы между элементами)
|
||
v
|
||
массив delta
|
||
| (Rice-Golomb преобразование)
|
||
v
|
||
битовый массив
|
||
| (base62 преобразование)
|
||
v
|
||
set-строка
|
||
```
|
||
|
||
Нужно отдельно разобрать заголовок, диапазоны `bpp` и `Mshift`, внешний префикс
|
||
`set:` и роль Base62: строка должна оставаться допустимым токеном версии RPM.
|
||
Полезна небольшая иллюстрация на 5-8 символах: исходные имена, хеши,
|
||
отсортированный массив, дельты, биты кода и итоговая строка.
|
||
Но. Это много альт-специфики, может настолько глубоко не стоит (хотя и не очень глубоко)
|
||
|
||
### 2.1. Хеширование и вероятность коллизий
|
||
|
||
! Всё ещё нет нормального анализа коллизий
|
||
|
||
Текущий код использует 32-битный Jenkins OAAT и оставляет $b$ бит. Для модели
|
||
равномерного независимого хеширования ожидаемое число столкнувшихся пар среди
|
||
$n$ различных символов равно
|
||
|
||
(будет мудрёная формула, теория)
|
||
|
||
В статье формулы дополняем разными табличками по реальным данным из Sisyphus: распределения $p,r,b$, ожидаемые коллизии, реальные.
|
||
|
||
(Другие хэши идут далее)
|
||
|
||
### 2.2. Кодирование Golomb-Rice
|
||
|
||
Для отсортированных значений $0\le x_1<\dots<x_n<N$ определить
|
||
$\delta_1=x_1$, $\delta_i=x_i-x_{i-1}$.
|
||
|
||
Занимаеи столько и столько бит. В реализации параметр выбирается из
|
||
$b$ и $n$ приблизительно как
|
||
|
||
$$
|
||
m=b-\lfloor\log_2 n\rfloor-1
|
||
$$
|
||
|
||
с ограничением допустимым диапазоном формата. При средней дельте
|
||
$\mathbb E\delta\approx 2^b/n$ это даёт ожидаемую длину порядка
|
||
|
||
...
|
||
|
||
### 2.3. Base62
|
||
|
||
Пояснить экзотичность Base62, его алфавит, принцип кодирования, Z-escape,
|
||
|
||
## 3. Построение `set:`-строк в `set.c`
|
||
|
||
(фактически его можно в 2 упихать)
|
||
|
||
Рассказать путь `set_new()` - `set_add()` - `set_fini()` и отдельно объяснить
|
||
сложные участки:
|
||
|
||
(а тут и нет сложного, всё в cmp запихали)
|
||
Можно рассказать о слитом энкодере, но это есть в переписанной части
|
||
|
||
В оригинальном варианте используется `qsort()`
|
||
|
||
Записать итоговую сложность построения:
|
||
|
||
...
|
||
|
||
И доп памяти:
|
||
|
||
...
|
||
|
||
## 4. Сравнение `set:`-строк
|
||
|
||
### 4.1. Обратное декодирование
|
||
|
||
Кратко пройти обратную цепочку:
|
||
|
||
```
|
||
set-строка
|
||
| (обратное base62 преобразование)
|
||
v
|
||
битовый массив
|
||
| (обратное Rice-Golomb преобразование)
|
||
v
|
||
массив delta
|
||
| (вычисление изначальных значений)
|
||
v
|
||
массив хэшей
|
||
```
|
||
|
||
Если `bpp` различается, более точное множество проецируется на меньшее число бит,
|
||
после чего снова удаляются дубликаты. (btw, тут есть сортировка слиянием, интересный момент)
|
||
|
||
### 4.2. Кэш декодированных множеств
|
||
|
||
В исходном `set.c` кэш содержит 256 записей (`CACHE_SIZE=256`), при вытеснении
|
||
использует позицию 243 (`PIVOT_SIZE=243`), `free()` и два `memmove()`. Ключ включает
|
||
короткий fingerprint, а окончательная проверка требует сравнения строки.
|
||
|
||
- что именно кэшируется;
|
||
- стоимость линейного поиска, `strcmp`, перемещений и аллокаций;
|
||
- слабое повторное использование при потоке десятков тысяч различных пар;
|
||
|
||
(всё надо сильно короче расписывать)
|
||
|
||
### 4.3. Проверка включения и «прыжок» по массивам
|
||
|
||
После декодирования задача сводится к двум возрастающим массивам. Базовый вариант
|
||
|
||
рассказ про IFLT4 и IFLT8
|
||
|
||
Худшая асимптотика остаётся
|
||
|
||
$$
|
||
T_{\subseteq}(p,r)=O(p+r),
|
||
$$
|
||
|
||
Полная сложность алгоритма холодного сравнения:
|
||
|
||
...
|
||
|
||
При попадании в кэш:
|
||
|
||
...
|
||
|
||
## 5. Оптимизации с сохранением текущего формата
|
||
|
||
### 5.1. Новый кэш
|
||
|
||
Заменить небольшой кэш на два раздельных bucketed LRU для `Provides` и `Requires`, увеличить вместимость. Хранить длину и более сильный fingerprint, избегать лишних
|
||
`strlen`/`strcmp`, `free` и полных `memmove`. Отдельно убрать повторные `realloc` при построении входных наборов.
|
||
Проверить cold, warm, поток меняющихся `Requires` к одному `Provides` и поток
|
||
полностью уникальных пар.
|
||
|
||
### 5.2. Radix sort
|
||
|
||
Для больших наборов заменить $O(n\log n)$ `qsort()` стабильной LSD radix sort по
|
||
байтам хеша. Число проходов
|
||
|
||
$$
|
||
k=\left\lceil\frac b8\right\rceil,
|
||
\qquad T_{\mathrm{radix}}=O(kn),
|
||
$$
|
||
|
||
дополнительная память $O(n+256)$. Для малых $n$ оставить `qsort()`; в `set9.c`
|
||
порог равен 128.
|
||
|
||
### 5.3. Оптимальный проход по массивам
|
||
|
||
Сделать адаптивный алгоритм:
|
||
|
||
- равные мощности - `memcmp` и быстрый вывод «равны/несравнимы»;
|
||
- близкие мощности - обычное линейное слияние;
|
||
- $p/r$ велико - galloping/jump search с монотонной нижней границей;
|
||
- немедленный выход при первом отсутствующем хеше.
|
||
|
||
## 6. Варианты со сменой формата или API
|
||
|
||
### 6.1. Roaring Bitmap - отрицательный результат
|
||
|
||
Равномерно распределённые хеши не образуют плотных диапазонов, ради которых создан
|
||
Roaring. Для 1000 символов при `bpp=32` получено:
|
||
|
||
| Представление | Длина строки | Cold compare | Warm compare |
|
||
| -------------------- | -----------: | -----------: | -----------: |
|
||
| Golomb/Base62 `set9` | 3994 | 293.87 мкс | 5.24 мкс |
|
||
| raw Roaring/hex | 19 924 | 1321.17 мкс | 399.20 мкс |
|
||
|
||
То есть строка длиннее в 4.99 раза, cold-сравнение медленнее в 4.50 раза, warm -
|
||
в 76.14 раза. Zstd уменьшал строку до 14 126 символов, но оставлял её в 3.54 раза
|
||
длиннее `set9`; в соответствующем прогоне cold- и warm-сравнение проиграли в 4.96
|
||
и 101.86 раза. Этот вариант нужен в статье как важный отрицательный эксперимент.
|
||
|
||
### 6.2. Прямые хеши и Base64
|
||
|
||
В формате D1 отсортированные усечённые хеши хранятся фиксированной шириной без
|
||
дельт и Golomb-кода, затем упаковываются в Base64. Теоретическая длина payload:
|
||
|
||
...
|
||
|
||
Для 1000 символов и `bpp=32` строка выросла с 3994 до 5340
|
||
символов (+33.7%). Приложить разные результаты тестов. (тесты бы прогнать снова)
|
||
|
||
Отдельно про увеличение размера и влияние на apt команды от этого
|
||
|
||
выводы насчёт формата
|
||
|
||
### 6.3. Альтернативные хэш функции
|
||
|
||
тут про разные хэши, их быстрдействие и тесты коллизий
|
||
|
||
## 8. Вывод
|