\Title{Гудов Д.О.}{Исследование алгоритма разрешения зависимостей в ALT RPM} \section*{Введение} Одной из задач пакетного менеджера является проверка совместимости устанавливаемого программного обеспечения с уже имеющимися или одновременно устанавливаемыми библиотеками. Традиционная зависимость от имени библиотеки и номера её версии не всегда достаточна: отдельный экспортируемый символ может быть удалён без изменения SONAME, а библиотеки с одинаковым SONAME могут предоставлять различные программные интерфейсы. В результате формально удовлетворённая зависимость от версии ещё не гарантирует, что динамический загрузчик найдёт все символы, необходимые программе. В ALT RPM эта задача решается при помощи специальных версий зависимостей вида \texttt{set:} [\Ref{SETC}]. Для отношения \texttt{Provides} такая строка описывает множество символов, экспортируемых библиотекой, а для отношения \texttt{Requires} --- символы, требуемые программой от конкретной библиотеки. При проверке зависимости обычное сравнение версий заменяется проверкой включения множеств. Полные имена символов при этом не сохраняются: они заменяются усечёнными хеш-значениями, сортируются и кодируются в компактную строку, допустимую как версия RPM. Цель настоящей работы --- исследовать алгоритм построения и сравнения \texttt{set:}-строк, оценить вероятностные свойства хэша и вычислительную сложность алгоритма, а также определить направления оптимизации реализации. %% В первых разделах формализуется решаемая задача, рассматриваются реальные данные репозитория Sisyphus, формат строки и путь её построения в исходной реализации \texttt{lib/set.c}. \section{Постановка задачи и данные ALT Linux} \label{PROBLEM} Пусть $P$ --- множество символов, предоставляемых библиотекой, а $R$ --- множество символов, требуемых от неё программой. Обозначим $p=|P|$ и $r=|R|$. Зависимость выполнима тогда и только тогда, когда \[ R\subseteq P. \] Списки символов формируются механизмом автоматического определения зависимостей \texttt{rpm-build}. Для \texttt{Provides} из динамической таблицы библиотеки выбираются доступные извне определённые символы. Для \texttt{Requires} утилита \texttt{ldd --bindings} устанавливает соответствие между требуемым символом и конкретной библиотекой-поставщиком; слабые неопределённые символы исключаются. Таким образом, одна строка \texttt{Requires} содержит только множество, связанное с данным поставщиком. Явное хранение имён увеличивало бы RPM-метаданные пропорционально их суммарной длине. Поэтому используется усечённая хеш-функция c целым $b$, называемого в реализации \texttt{bpp}, \[ h_b(x)=h_{32}(x)\bmod 2^b, \qquad H_b(S)=\{h_b(x)\mid x\in S\}, \] где $h_{32}$ --- 32-битная функция Jenkins one-at-a-time. Фактически \texttt{lib/set.c} проверяет условие \[ H_b(R)\subseteq H_b(P). \] Такое представление сохраняет включение: из $R\subseteq P$ следует $H_b(R)\subseteq H_b(P)$. Следовательно, коллизия хешей не создаёт ложного отказа для корректной зависимости. Обратное утверждение неверно: отсутствующий символ из $R\setminus P$ может получить то же усечённое значение, что и один из символов $P$, и привести к ложному принятию зависимости. Тем самым предоставляемая гарантия имеет вероятностный характер. Для оценки реальной нагрузки был исследован срез репозитория Sisyphus для архитектур \texttt{x86\_64} и \texttt{noarch}. Объём рассмотренных метаданных на момент 2026-08-20 приведён в табл.~\Ref{CORPUS}. \begin{table}[H] \begin{center} \caption{\label{CORPUS}Объём исследованного среза Sisyphus} \small \begin{tabular}{|l|r|} \hline Объект & Количество \\ \hline Пакеты & 47\,654 \\ \hline Отношения \texttt{Provides} с \texttt{set:}-версией & 14\,859 \\ \hline Отношения \texttt{Requires} с \texttt{set:}-версией & 69\,153 \\ \hline Сопоставленные пары $P,R$ & 68\,492 \\ \hline \end{tabular} \end{center} \end{table} 661 отношений удовлетворены обычным неверсионированным \texttt{Provides}. Для каждой сопоставленной пары декодировались мощности множеств $p$ и $r$, а также вычислялось индивидуальное отношение $p/r$. Квантили этих величин показаны в табл.~\Ref{CARDINALITIES}. Квантили отношения вычислялись непосредственно по парам. \begin{table}[H] \begin{center} \caption{\label{CARDINALITIES}Мощности множеств в парах \texttt{Provides}/\texttt{Requires}} \small \begin{tabular}{|c|r|r|r|} \hline Квантиль & $p$ & $r$ & $p/r$ \\ \hline 0,50 & 480 & 13 & 28,1 \\ \hline 0,75 & 1\,886 & 37 & 80 \\ \hline 0,90 & 6\,219 & 104 & 257 \\ \hline \end{tabular} \end{center} \end{table} В 92,4\% сопоставленных пар выполняется $p/r\geq 4$. Следовательно, типичный проверяемый набор требований существенно разрежен относительно множества предоставляемых символов. Это наблюдение важно для алгоритма сравнения: последовательный симметричный просмотр двух массивов не всегда использует характерное различие их мощностей. \section{Устройство существующей \texttt{set:}-строки} \label{FORMAT} Построение \texttt{set:}-строки выполняется как последовательность преобразований \[ \begin{aligned} \text{имена символов} &\longrightarrow \text{усечённые хеши} \longrightarrow \text{сортировка},\\ &\longrightarrow \text{удаление повторов и вычисление дельт},\\ &\longrightarrow \text{код Голомба--Райса} \longrightarrow \text{Base62-представление}. \end{aligned} \] Итоговая версия имеет вид \[ \texttt{set:}\langle b\rangle\langle m\rangle\langle payload\rangle. \] Префикс \texttt{set:} распознаётся RPM как признак специальной версии. Следующие два символа кодируют параметры $b=\texttt{bpp}$ и $m=\texttt{Mshift}$ по правилу $c=v-7+\texttt{'a'}$. Допустимы $10\leq b\leq32$, $7\leq m\leq31$ и $m0)=1-\prod_{i=0}^{n-1}\left(1-\frac{i}{2^b}\right) \approx 1-\exp\left(-\frac{n(n-1)}{2^{b+1}}\right). \] Эти выражения являются модельными: Jenkins OAAT не является случайным оракулом, поэтому окончательная оценка должна дополняться измерением коллизий на реальном корпусе символов. Эвристическую верхнюю оценку ложного принятия при $k=|R\setminus P|$ отсутствующих символов можно записать как \[ \Pr\bigl(H_b(R)\subseteq H_b(P)\mid R\nsubseteq P\bigr) \lesssim \left(\frac{|H_b(P)|}{2^b}\right)^k. \] Увеличение $b$ снижает вероятность ошибки, но увеличивает кодируемые значения и длину строки. Поэтому выбор \texttt{bpp} представляет собой компромисс между компактностью метаданных и риском коллизий; при формировании \texttt{set:}-строки для \texttt{Requires} набора используется точность, определённая по числу символов соответствующего \texttt{Provides}, а не по обычно намного меньшему числу требований. \subsection{Кодирование Голомба--Райса} После сортировки уникальных значений \[ 0\leq x_1r$, проверяется только $\widehat R\subseteq\widehat P$; обратное строгое включение невозможно. Случай $p1$ означает ускорение, а $K_{\mathrm{CPU}}<1$ --- замедление экспериментальной реализации. \begin{table}[H] \begin{center} \caption{\label{APT_RESULTS}Сравнение исходного \texttt{set.c} и \texttt{set9.c} в симуляциях APT} \scriptsize \begin{tabular}{|l|r|r|r|r|r|} \hline Сценарий & \multicolumn{2}{c|}{Исходный, с} & \multicolumn{2}{c|}{\texttt{set9.c}, с} & $K_{\mathrm{CPU}}$ \\ \cline{2-5} & user & system & user & system & \\ \hline \texttt{-s check} & 0,453 & 0,033 & 0,503 & 0,040 & 0,896 \\ \hline \texttt{-s autoremove} & 0,763 & 0,040 & 0,810 & 0,040 & 0,945 \\ \hline \texttt{-s install rpm-build} & 1,210 & 0,050 & 1,283 & 0,050 & 0,945 \\ \hline \texttt{-s install openuds-server} & 3,747 & 0,080 & 3,897 & 0,083 & 0,961 \\ \hline \texttt{-s install password-store} & 1,233 & 0,050 & 1,310 & 0,050 & 0,944 \\ \hline \end{tabular} \end{center} \end{table} Во всех пяти измеренных сценариях \texttt{set9.c} не превзошёл исходный вариант: суммарное процессорное время увеличилось примерно на 4,0--11,6\%. Это отрицательный результат, вызванный более простым потоковым декодером, который не превзошёл специализированного табличного декодера исходной реализации даже при условии остальных оптимизаций. Для проверки этого объяснения исходный слитый декодер отдельно сравнивался с вариантом, в котором Base62, код Голомба--Райса и восстановление дельт выполнялись последовательными стадиями. В зависимости от сценария слитый путь сокращал суммарное процессорное время в 1,47--2,82 раза. Следовательно, благодаря табличной обработке Base62/Golomb получается измеримый выигрыш. Практическое направление дальнейшей работы может состоять в разработке нового формата хранения и обработки \texttt{set:}-строк. \section{Варианты со сменой формата или API} \label{ALTERNATIVE_FORMATS} Оптимизации разд.~\Ref{COMPATIBLE_OPTIMIZATIONS} ограничены требованием побайтовой совместимости. Параллельно исследовались представления, снимающие это ограничение. Их результаты нельзя напрямую переносить на существующие RPM-метаданные: новый формат требует повторного формирования зависимостей репозитория. \subsection{Roaring Bitmap: отрицательный результат} Roaring Bitmap предназначен прежде всего для множеств, содержащих плотные участки целочисленного пространства. Усечённые хеши, напротив, распределены по диапазону приблизительно равномерно. Поэтому контейнеры bitmap не получают длинных серий соседних значений, но сохраняют собственные заголовки и индексы. В микротесте использовались 1000 предоставляемых и 500 требуемых символов при $b=32$. Результаты приведены в табл.~\Ref{ROARING_RESULTS}. Коэффициенты времени вычислены относительно \texttt{set9.c}; значение больше единицы означает замедление. \begin{table}[H] \begin{center} \caption{\label{ROARING_RESULTS}Сравнение Golomb/Base62 и Roaring Bitmap} \small \begin{tabular}{|l|r|r|r|} \hline Представление & Длина строки & Cold compare & Warm compare \\ \hline Golomb/Base62 \texttt{set9} & 3\,994 & 1,00 & 1,00 \\ \hline Roaring/hex & 19\,924 & 4,50 & 76,14 \\ \hline Roaring+zstd & 14\,126 & 4,96 & 101,86 \\ \hline \end{tabular} \end{center} \end{table} Без дополнительного сжатия строка оказалась длиннее в 4,99 раза. Холодное сравнение заняло 1321,17 мкс вместо 293,87 мкс, а прогретое --- 399,20 мкс вместо 5,24 мкс. При этом построение bitmap занимало лишь $0,76$ времени \texttt{set\_fini()} варианта \texttt{set9}; ускорение генерации не компенсировало стоимость хранения и сравнения. Сжатие zstd уменьшило строку до 14\,126 символов, то есть до 3,54 длины \texttt{set9}, но добавило декомпрессию в горячий путь. В отдельном прогоне холодное и прогретое сравнения были медленнее соответственно в 4,96 и 101,86 раза. Таким образом, Roaring Bitmap для равномерных хешей является подтверждённым отрицательным вариантом: структура данных не соответствует распределению кодируемых значений. \subsection{Прямые хеши в Base64} Другой вариант, обозначенный как D1, исключает дельты и код Голомба--Райса. $n$ отсортированных уникальных хешей записываются подряд по $b$ бит, после чего битовый массив преобразуется в Base64-строку. Если \[ Q=\left\lceil\frac{nb}{8}\right\rceil \] --- число байтов упакованного массива, то доминирующая часть длины payload равна \[ L_{\mathrm{payload}}= \left\lceil\frac{4Q}{3}\right\rceil, \] а заголовок формата добавляет постоянное число символов. В отличие от Golomb--Rice, эта оценка почти не зависит от расстояний между соседними хешами: цена быстрого произвольного доступа --- увеличение метаданных. Для тех же 1000/500 символов при $b=32$ длина строки выросла с 3994 до 5340 символов, то есть на 33,7\%. Результаты теста показаны в табл.~\Ref{DIRECT_RESULTS}; отношение меньше единицы означает, что D1 затратил меньшую долю времени \texttt{set9.c}. \begin{table}[H] \begin{center} \caption{\label{DIRECT_RESULTS}Тест прямого представления D1} \small \begin{tabular}{|l|r|r|r|} \hline Операция & \texttt{set9}, мкс & D1, мкс & D1/\texttt{set9} \\ \hline \texttt{set\_fini()} & 147,45 & 105,09 & 0,71 \\ \hline \texttt{rpmsetcmp()}, cold & 129,67 & 113,04 & 0,87 \\ \hline \texttt{rpmsetcmp()}, warm & 2,99 & 1,04 & 0,35 \\ \hline \end{tabular} \end{center} \end{table} Для крупного требования прямое представление ускорило все три измеряемые операции, особенно попадание в кэш. Однако при одном требуемом символе холодное сравнение занимало 83,22 мкс ($0,88$ времени \texttt{set9}), а прогретое --- 0,86 мкс ($1,02$), то есть преимущество исчезало. Это подчёркивает зависимость результата от формы нагрузки: выигрыш одного сравнения не доказывает ускорение полного потока зависимостей APT. D1 представляет интерес как компромисс между размером и стоимостью декодирования, но требует отдельной сквозной проверки на преобразованных метаданных репозитория. \subsection{Альтернативные хеш-функции} Отдельно сравнивались Jenkins OAAT, xxHash32 и CityHash32. Сравнение было необходимо, так как xxHash и CityHash --- хэш функции, разработанные значительно позже последнего обновления \texttt{set.c}. В каждом замере использовались три запуска; в табл.~\Ref{HASH_SPEED} приведены медианы. Для коротких строк указано время одного хеширования, для длинных --- пропускная способность. \begin{table}[H] \begin{center} \caption{\label{HASH_SPEED}Скорость 32-битных хеш-функций} \small \begin{tabular}{|l|r|r|} \hline Функция & 32 байта, нс/хеш & 1024 байта, ГиБ/с \\ \hline Jenkins OAAT & 50,98 & 0,53 \\ \hline xxHash32 & 11,56 & 5,18 \\ \hline CityHash32 & 16,70 & 4,10 \\ \hline \end{tabular} \end{center} \end{table} На строках длиной 32 байта xxHash32 был быстрее Jenkins примерно в 4,4 раза, CityHash32 --- в 3,1 раза. При 1024 байтах различие по пропускной способности достигало соответственно 9,8 и 7,7 раза. Следовательно, Jenkins OAAT не является оптимальным по скорости, особенно на длинных входах. Для оценки качества использовался другой тест, в нём сравнивались Jenkins OAAT, 64-битный xxHash и \texttt{t1ha2\_atonce};. Поскольку формат \texttt{set:version} сохраняет не более 32 бит хеша, при сопоставлении учитывались младшие 32 выходных бита всех трёх функций. Тест измерял лавинный эффект на реальном корпусе экспортируемых C++-символов ALT Linux p11. Утилита \texttt{provided\_symbols} извлекла из 259 ELF-файлов пяти пакетов 577\,509 уникальных имён. Пакеты выбирались среди имеющих наиболее длинные \texttt{Provides set:}-строки. Корпус специально является сложным для хеширования похожих строк: 99,60\% символов имеют с другим символом общий префикс длиной не менее 12 знаков, 95,05\% --- не менее 24 знаков, а медиана максимального общего префикса равна 61 знаку. Для каждого бита вычислялась доля $q_i$ корпусных хешей, в которых этот бит равен единице. Идеальному распределению соответствует $q_i=0{,}5$. Результаты обоих тестов представлены в табл.~\Ref{HASH_QUALITY_EXOTIC}. Значения приведены в процентах от полной шкалы вероятности; последний столбец содержит максимальное среднее абсолютное отклонение младших 32 бит от $0{,}5$. Среднее абсолютное отклонение по младшим 32 битам вычислялось как: \[ \overline{\Delta}=\frac{1}{32}\sum_{i=0}^{31}|p_i-0{,}5|. \] Меньшее значение $\overline{\Delta}$ соответствует более равномерному лавинному эффекту. \begin{table}[H] \begin{center} \caption{\label{HASH_QUALITY_EXOTIC}Качество перемешивания младших 32 бит хеш-функций} \scriptsize \begin{tabular}{|l|r|r|r|r|} \hline Функция & \makecell{Корпус\\$\overline{\Delta}$, \%} & \makecell{Корпус\\$\Delta_{\max}$, \%} \\ \hline Jenkins OAAT & 0,0475 & 0,167 \\ \hline xxHash64 & 0,0431 & 0,122 \\ \hline t1ha2 & 0,0615 & 0,188 \\ \hline \end{tabular} \end{center} \end{table} Все три функции продемонстрировали близкое к равномерному распределение. На реальном корпусе лучший результат получен для xxHash64, однако разность между функциями составляла сотые доли процента, а максимальное отклонение отдельного бита не превысило 0,188\%. Таким образом, тестирование на похожих C++-символах не выявило недостаточного перемешивания Jenkins OAAT. xxHash64 и t1ha2 остаются кандидатами для отдельной оценки производительности, но качество распределения само по себе не обосновывает замену Jenkins. \References \begin{enumerate} \item \Label{SETC} Tourbin A. \emph{set.c --- base62, Golomb and set-string routines}. ALT RPM source code, 2010--2012. [Электронный ресурс] URL: \url{https://git.altlinux.org/gears/r/rpm.git?a=blob;f=lib/set.c}. \item \Label{JENKINS} \item \Label{GOLOMB-RICE} \end{enumerate}