# Работа над `set.c`: краткий план статьи ## 1. Зачем ALT RPM нужны set-строки Обычная зависимость от версии библиотеки не гарантирует, что в ней остались все нужные программе символы: символ можно удалить, не сменив SONAME, а одинаковые SONAME могут скрывать разные наборы экспортов. Поэтому ALT RPM использует версии зависимостей вида `set:`. В `Provides` такая строка описывает символы, предоставляемые библиотекой, а в `Requires` - символы, которые конкретный потребитель требует именно от неё. Сравниваются не номера версий, а включение множеств: все хэши из `Requires` должны присутствовать в `Provides`. Гарантия вероятностная, поскольку вместо имён хранятся усечённые хэши, но ответ "символ отсутствует", когда он есть мы не получим (ложноположительное срабатывание) ## 2. Как строится set-строка Список символов формируется автодепами `rpm-build`. Для `Provides` из ELF извлекаются отфильтрованные экспортируемые базовые имена символов. Для `Requires` `ldd --bindings` связывает каждый сильный неопределённый символ с конкретной библиотекой, после чего для каждого провайдера строится отдельное множество. Далее `mkset` использует API `set_new()` → `set_add()` → `set_fini()`: 1. для каждого имени вычисляется Jenkins OAAT и оставляются младшие `bpp` бит; 2. хэши сортируются, повторы удаляются, о коллизиях выдаётся предупреждение; 3. абсолютные значения заменяются дельтами; 4. дельты сжимаются кодом Голомба-Райса с параметром `Mshift`; 5. битовый поток переводится в RPM-безопасную Base62-строку. При сравнении `rpmsetcmp()` выполняет обратное декодирование, приводит строки с разным `bpp` к общей точности и проверяет включение отсортированных наборов. В рабочем вызове первым операндом выгодно передавать `Provides`: исходный код кэширует прежде всего его декодирование. (переформулировать в сторону кэш есть только для) (сказать про подсёт bpp и Mshift на стороне) ## 3. Разбор исходного кода Работа началась с изучения примерно десятилетнего `lib/set.c`. Код оказался большим по объёму и очень плотным: объединённый Base62/Golomb-декодер, таблица всех пар входных байтов, макросы состояний, прыжки по массиву через defined инструкции и подобие LRU-кэш. Отдельно были прослежены реальные точки вызова в `rpm` и `rpm-build`: во время сборки бинарник `mkset` создаёт строки, а `rpmsetcmp()` через `rpmRangesOverlap()` участвует в проверке зависимостей RPM и APT. Результатом этого этапа стала документация: описание главных пяти функций API, формата строки, кодирования и декодирования, нормализации `bpp`, кэша, макросов сравнения и встроенного `SELF_TEST`. Таким образом, прежде чем менять алгоритм, для него была построена читаемая модель и зафиксированы его неочевидные инварианты. ## 4. Переписывание и последовательные оптимизации Сначала алгоритм был перенесён в Python как понятная проверочная реализация. Затем появился читаемый C-вариант, а после него - оптимизированный `set9.c`, сохраняющий старый wire-format и публичный API. В нём были опробованы: (добавить про все промежуточные тоже) - строковая арена вместо отдельного выделения памяти под каждое имя; - `qsort()` для малых наборов и radix sort для больших; - потоковое кодирование и декодирование без промежуточных массивов битов и дельт; (было и в оригинале, но не было в читаемом) - два раздельных bucketed LRU-кэша для `Provides` и `Requires`; (теперь даже LRU, нет постоянных realloc) - кэширование результата понижения `bpp`; (надо лучше вспомнить разницу) - более простой адаптивный поиск включения в отсортированных массивах; - явная проверка метаданных, размеров и кодов внутренних ошибок. (в оригинале по несколько раз) Не все «читаемые» замены оказались быстрыми. Удаление специальных оптимизаций исходного файла замедляло обычные APT-сценарии примерно в 2–3 раза. `set9.c` вернулся к уровню оригинала и в проведённых симуляциях оказался быстрее/медленне него примерно на 1–10%, однако отдельный анализ показал, что старый слитый декодер и прыгающий проход по массивам всё ещё сильны. Поэтому был собран гибридный `set_frank.c`: новые структуры, encoder и кэш соединены с декодером и сравнением из исходного `set.c`. (сказать, что прироста не сильно, читаемость хуже, проверить реальные числа) ## 5. Как проверялись изменения Тесты строились вокруг инвариантов формата: (логично, его и надо сохранить) - исходный `SELF_TEST` и расширенные проверки публичного API; - побайтовое совпадение результата `set_fini()` старой и новой реализации для `bpp=10…32`; (уточнить, что на ранд данных) - дифференциальные случайные тесты равенства, включения, несравнимости и обоих направлений сравнения; - повреждённые строки, граничные `bpp`/`Mshift`, разные точности, заполнение и вытеснение кэша; - проверки освобождения памяти; (надо добавить про алгоритм, что этого не было, хе) - проверка реальных `Provides`/`Requires` из Sisyphus; - холодные и прогретые микробенчмарки, затем симуляции и настоящие вызовы ALT APT с проверкой одинакового результата. (более того, потом всё равно предполагается запуск в сборочнице. вроде.) (куда-то эти мысли про хэш надо впихнуть) Отдельно сравнивались Jenkins OAAT, CityHash и xxHash по скорости и коллизиям на случайных данных и ASCII-строках. Этот эксперимент помог отделить свойства хэш-функции от стоимости остального формата, но простая замена хэша не стала готовым решением: она меняет совместимость строк и не устраняет основную цену декодирования и поиска. ## 6. Альтернативные форматы и результаты Параллельно были проверены варианты, не совместимые со старым форматом. **Roaring Bitmap.** Создание bitmap могло быть быстрым, но равномерно распределённые хэши плохо подходят для такого представления. Для 1000 символов при `bpp=32` строка выросла с 3994 до 19924 символов; холодное сравнение было примерно в 4,5 раза, прогретое - в 76 раз медленнее `set9`. Zstd уменьшал строку до 14126 символов, но добавлял ещё больше работы при сравнении. Вариант оставлен как отрицательный эксперимент. **Прямой массив хэшей в Base64.** Отказ от delta/Golomb ускорял отдельные микротесты: в одном прогоне холодное сравнение занимало `0,87×`, а прогретое `0,35×` времени `set9`, при росте строки с 3994 до 5340 символов. Но полный поток реальных пар Sisyphus показал обратное (эту стороны надо добить) - текущая реализация была примерно в 1,4 раза медленнее. На отдельном синтетическом APT-графе resolver, напротив, ускорялся на 7–18%, зато `gencaches` замедлялся примерно на 12%. Кроме того, стандартный Base64 использует padding `=` (да, я забыл это пофиксить), который RPM запрещает внутри версии зависимости. Поэтому прямой формат интересен как компромисс между размером и стоимостью декодирования, но в текущем виде не готов заменить set-строки. **Другие направления.** Рассматривались отрицательный Bloom-подобный prefilter с обязательной точной проверкой, адаптивный индекс для больших `Provides`, Elias–Fano и глобальные идентификаторы символов. Общий вывод: (в это возможно стоит углубиться и сделать его) ## 7. Итого Суть работы была в понимании "магии" кода. Сначала были восстановлены назначение, формат и реальные пути использования; создана документация и читаемая реализация; после этого каждая оптимизация проверялась на совместимость, безопасность и скорость. Эксперименты показали, что код содержит оправданные низкоуровневые решения, а локально более простой или быстрый формат не обязательно выигрывает на полном потоке RPM/APT (still check). Наиболее практичное продолжение - (что-то)