11 KiB
Исследование алгоритма разрешения зависимостей в 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. Альтернативные хэш функции
тут про разные хэши, их быстрдействие и тесты коллизий