307 lines
30 KiB
TeX
307 lines
30 KiB
TeX
\Title{Гудов Д.О.}{Исследование алгоритма разрешения зависимостей в ALT RPM}
|
||
|
||
\section*{Введение}
|
||
|
||
Одной из задач пакетного менеджера является проверка совместимости устанавливаемого программного обеспечения с уже имеющимися или одновременно устанавливаемыми библиотеками. Традиционная зависимость от имени библиотеки и номера её версии не всегда достаточна: отдельный экспортируемый символ может быть удалён без изменения SONAME, а библиотеки с одинаковым SONAME могут предоставлять различные программные интерфейсы. В результате формально удовлетворённая зависимость от версии ещё не гарантирует, что динамический загрузчик найдёт все символы, необходимые программе.
|
||
|
||
В ALT RPM эта задача решается при помощи специальных версий зависимостей вида \texttt{set:<encoded-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$ и $m<b$. Оставшаяся часть строки содержит закодированный в \texttt{Base62} битовый поток.
|
||
|
||
\subsection{Хеширование и вероятность коллизий}
|
||
|
||
Для каждого имени вычисляется 32-битный Jenkins one-at-a-time [\Ref{JENKINS}], после чего сохраняются только $b$ младших бит. Пусть $n$ различных имён независимо и равномерно отображаются в пространство из $N=2^b$ значений. Тогда математическое ожидание числа столкнувшихся пар равно
|
||
|
||
\[
|
||
\mathbb{E}C=\binom{n}{2}\frac{1}{2^b}
|
||
=\frac{n(n-1)}{2^{b+1}},
|
||
\]
|
||
а вероятность хотя бы одной коллизии имеет вид
|
||
\[
|
||
\P(C>0)=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_1<x_2<\dots<x_n<2^b
|
||
\]
|
||
они заменяются дельтами
|
||
\[
|
||
\delta_1=x_1,
|
||
\qquad
|
||
\delta_i=x_i-x_{i-1},\quad i=2,\dots,n.
|
||
\]
|
||
Для равномерных хешей средний промежуток имеет порядок $2^b/n$, поэтому дельты значительно меньше абсолютных значений и хорошо подходят для кодирования Голомба--Райса [\Ref{GOLOMB-RICE}].
|
||
|
||
В реализации модуль $M$ равен степени двойки $M=2^m$. Для каждой дельты вычисляются
|
||
\[
|
||
q_i=\left\lfloor\frac{\delta_i}{M}\right\rfloor,
|
||
\qquad
|
||
r_i=\delta_i\bmod M.
|
||
\]
|
||
Частное $q_i$ записывается унарно как $q_i$ нулей и завершающая единица, после которой следуют $m$ младших бит остатка $r_i$. Длина кода одного значения равна
|
||
\[
|
||
\ell_i=q_i+1+m.
|
||
\]
|
||
Параметр выбирается приближённо как
|
||
\[
|
||
m=b-\lfloor\log_2 n\rfloor-1
|
||
\]
|
||
и ограничивается диапазоном формата $7\leq m\leq31$. При принятой модели суммарная длина битового потока оценивается как
|
||
\[
|
||
B=\sum_{i=1}^{n}\ell_i
|
||
=O\!\left(n\left(1+\log_2\frac{2^b}{n}\right)\right).
|
||
\]
|
||
|
||
\subsection{Base62-представление}
|
||
|
||
Битовый поток нельзя непосредственно поместить в поле версии RPM: необходима строка из допустимых символов. Исходная реализация использует алфавит \texttt{0--9}, \texttt{a--z}, \texttt{A--Z}. Значения от 0 до 60 записываются обычным символом, а \texttt{Z} служит escape-символом и позволяет представить также шестибитные значения 61, 62 и 63. Их различают два старших бита следующего символа: \texttt{00}, \texttt{01} или \texttt{10}. Комбинация \texttt{11} не используется, поэтому escape-последовательности не могут образовать неоднозначную цепочку. Вследствие этого один символ несёт от пяти до шести бит, и длина текстовой части приближённо пропорциональна $B/6$.
|
||
|
||
Рассмотрим небольшой пример с $b=10$. Для пяти имён Jenkins OAAT после усечения даёт значения, приведённые в табл.~\Ref{ENCODING_EXAMPLE}.
|
||
|
||
\begin{table}[H]
|
||
\begin{center}
|
||
\caption{\label{ENCODING_EXAMPLE}Пример хеширования символов при $b=10$}
|
||
\small
|
||
\begin{tabular}{|l|r|r|}
|
||
\hline
|
||
Символ & 32-битный хеш & $h_{10}$ \\
|
||
\hline
|
||
\texttt{fclose} & \texttt{0x6743def6} & 758 \\
|
||
\hline
|
||
\texttt{fopen} & \texttt{0x0e984918} & 280 \\
|
||
\hline
|
||
\texttt{free} & \texttt{0xa5bbbac7} & 711 \\
|
||
\hline
|
||
\texttt{malloc} & \texttt{0x07c1b8ff} & 255 \\
|
||
\hline
|
||
\texttt{printf} & \texttt{0xfd1ad5cc} & 460 \\
|
||
\hline
|
||
\end{tabular}
|
||
\end{center}
|
||
\end{table}
|
||
|
||
После сортировки получается массив $(255,280,460,711,758)$, а после вычисления разностей --- $(255,25,180,251,47)$. Для $n=5$ формула даёт $m=7$; полученный 43-битный поток преобразуется в payload \texttt{ZvpACXZy}. Параметрам $b=10$ и $m=7$ соответствуют заголовочные символы \texttt{d} и \texttt{a}, поэтому итоговая строка равна \texttt{set:daZvpACXZy}.
|
||
|
||
\section{Построение \texttt{set:}-строк в \texttt{set.c}}
|
||
\label{CONSTRUCTION}
|
||
|
||
Благодаря сценариям автозависимостей \texttt{rpm-build} утилита \texttt{mkset} получает символы из ELF-файлов и далее использует \texttt{lib/set.c}, вызывая интерфейс построения множества:
|
||
\[
|
||
\texttt{set\_new()}\ \longrightarrow\
|
||
\texttt{set\_add()}\ \longrightarrow\
|
||
\texttt{set\_fini()}.
|
||
\]
|
||
|
||
Функция \texttt{set\_new()} создаёт пустую структуру \texttt{struct set}. В исходной реализации она представляет собой растущий массив пар «указатель на строку --- хеш». Функция \texttt{set\_add()} увеличивает ёмкость массива блоками по 1024 элемента и копирует каждое имя отдельным вызовом \texttt{xstrdup()}. На этом этапе хеши ещё не вычисляются, поэтому один объект можно финализировать с заданным значением \texttt{bpp}.
|
||
|
||
Основную работу выполняет \texttt{set\_fini()}. Её действия следуют в фиксированном порядке:
|
||
\begin{enumerate}
|
||
\item проверяется непустота множества и условие $10\leq b\leq32$;
|
||
\item для каждого имени вычисляется Jenkins OAAT и применяется маска из $b$ младших бит;
|
||
\item массив пар сортируется стандартной функцией \texttt{qsort()} по хеш-значению;
|
||
\item для равных хешей различных имён выводится предупреждение о коллизии;
|
||
\item хеши копируются в числовой массив, а повторяющиеся значения удаляются;
|
||
\item массив кодируется последовательно как дельты, код Голомба--Райса и Base62;
|
||
\item сформированная строка копируется в динамическую память и возвращается вызывающей стороне.
|
||
\end{enumerate}
|
||
|
||
Удаление повторов необходимо в двух случаях. Во-первых, одно имя может несколько раз попасть во входной поток. Во-вторых, различные имена могут совпасть после усечения хеша. Для семантики $H_b(S)$ оба случая означают один элемент множества. При этом предупреждение о коллизии формируется до удаления повторов, пока реализации ещё доступны исходные строки и можно различить совпадение имён от совпадения их хешей.
|
||
|
||
Оценим вычислительную сложность. Пусть $L$ --- суммарная длина всех входных имён, $n$ --- их количество, а $B$ --- длина потока Голомба--Райса в битах. Хеширование требует $O(L)$ операций, сортировка --- $O(n\log n)$ сравнений, линейные проходы сортированного массива --- $O(n)$, кодирование --- $O(B)$. Поэтому для исходного варианта
|
||
\[
|
||
T_{\mathrm{build}}=O(L)+O(n\log n)+O(B).
|
||
\]
|
||
Хранимые копии строк, массив пар, массив уникальных хешей, битовый буфер и результирующая строка дают дополнительную память порядка
|
||
\[
|
||
M_{\mathrm{build}}=O(L+n+B).
|
||
\]
|
||
Главным асимптотическим слагаемым для больших наборов остаётся \texttt{qsort()}, тогда как большое число отдельных копирований строк и наличие промежуточного битового массива влияют на постоянные затраты. Это определяет два независимых направления дальнейшего улучшения: замена сортировки для крупных множеств и потоковое кодирование без промежуточных представлений. При этом любые изменения должны сохранять байтовую совместимость формата.
|
||
|
||
\section{Сравнение \texttt{set:}-строк}
|
||
\label{COMPARISON}
|
||
|
||
Функция \texttt{rpmsetcmp()} получает две строки и определяет отношение включения между закодированными множествами. В штатном пути RPM первым операндом передаётся \texttt{Provides}, а вторым --- \texttt{Requires}. Возвращаемые значения различают четыре результата: 1, если первое множество строго содержит второе; 0 при равенстве; $-1$, если первое множество строго содержится во втором; $-2$, если множества несравнимы по включению. Ошибки декодирования первого и второго операндов отображаются соответственно в коды $-3$ и $-4$.
|
||
|
||
\subsection{Обратное декодирование и нормализация точности}
|
||
|
||
После удаления необязательного префикса \texttt{set:} проверяются параметры $b$ и $m$, а затем выполняется обратная цепочка преобразований
|
||
\[
|
||
\begin{aligned}
|
||
\text{Base62-строка}
|
||
&\longrightarrow \text{битовый поток}
|
||
\longrightarrow \text{дельты Голомба--Райса},\\
|
||
&\longrightarrow \text{возрастающий массив хешей}.
|
||
\end{aligned}
|
||
\]
|
||
Логически это преобразование обратно рассмотренному в разд.~\Ref{FORMAT}. Однако исходный \texttt{lib/set.c} не создаёт отдельный битовый массив: функция \texttt{decode\_base62\_golomb()} объединяет первые две стадии. Она считывает обычные символы попарно, преобразует их предварительно вычисленной таблицей и обрабатывает блоки до 24 бит. Частное и остаток кода Голомба--Райса восстанавливаются непосредственно из этих блоков. После этого единственный линейный проход суммирует дельты и получает исходные усечённые хеши.
|
||
|
||
Две строки могут иметь разные значения \texttt{bpp}. Сравнение выполняется при общей точности
|
||
\[
|
||
b_* = \min(b_P,b_R).
|
||
\]
|
||
Для этого более точный массив последовательно проецируется на один бит вниз. На одном шаге применяется отображение
|
||
\[
|
||
\pi_t(x)=x\bmod 2^t.
|
||
\]
|
||
После удаления старшего бита исходный возрастающий массив распадается на две возрастающие части. Обе части сливаются с сохранением возрастания элементов, а появившиеся совпадения удаляются. Поэтому после понижения точности сохраняются одновременно сортировка и представление множества без повторов. При разности точностей $d=|b_P-b_R|$ слияние повторяется $d$ раз.
|
||
|
||
В результате сравниваются два возрастающих массива
|
||
\[
|
||
\widehat P=H_{b_*}(P),\qquad
|
||
\widehat R=H_{b_*}(R).
|
||
\]
|
||
|
||
Понижение \texttt{bpp} необходимо, так как сравнение множеств хэшей отличающейся точности невозможно.
|
||
|
||
\subsection{Кэш декодированных множеств}
|
||
|
||
Декодирование длинной строки существенно дороже проверки нескольких уже готовых целых значений. Кроме того, один поставщик обычно сопоставляется с требованиями нескольких пакетов. Поэтому исходная реализация кэширует декодированный первый операнд, то есть в обычном вызове множество \texttt{Provides}.
|
||
|
||
Кэш состоит из двух массивов по 256 элементов: коротких отпечатков и указателей на записи. Отпечаток формируется из трёх байтов строки и служит только предварительным фильтром. Корректность попадания подтверждается полным сравнением исходной строки. Запись одним выделением памяти хранит декодированный массив, его длину и копию строки.
|
||
|
||
Поиск выполняется линейно. При попадании запись перемещается в начало двумя вызовами \texttt{memmove()}. Если кэш заполнен, последняя запись освобождается, а новый элемент вставляется на позицию 243 (\texttt{PIVOT\_SIZE}), после чего хвост обоих массивов также сдвигается. Такая схема приближает LRU-политику, но не является строгим LRU: новая, ещё не подтвердившая полезность запись помещается около конца кэша, тогда как повторно использованная попадает в начало.
|
||
|
||
Второй операнд исходная реализация каждый раз декодирует во временный массив на стеке. Кроме того, кэш хранит результат при исходном \texttt{bpp}; если для конкретного сравнения его требуется понизить, проекция вычисляется заново. Следовательно, кэш особенно полезен для потока сравнений одного \texttt{Provides} с разными \texttt{Requires} одинаковой точности, но не устраняет стоимость декодирования требований и нормализации.
|
||
|
||
\subsection{Проверка включения возрастающих массивов}
|
||
|
||
Наивная проверка $\widehat R\subseteq\widehat P$ последовательно продвигает указатель по $\widehat P$ до очередного требуемого значения. Исходная реализация одновременно вычисляет оба отношения включения с помощью флагов \texttt{ge} и \texttt{le}. Это позволяет одним проходом получить все четыре значения публичного API, а не только ответ на типичный для RPM вопрос $\widehat R\subseteq\widehat P$.
|
||
|
||
Для ускорения поиска используются макросы \texttt{IFLT4} и \texttt{IFLT8}. Первый перемещает указатель по массиву первого операнда блоками по четыре элемента, после превышения искомого значения возвращается на два элемента и уточняет позицию единичными шагами. Второй выполняет ту же схему с начальным шагом восемь и уточнениями 4, 2 и 1. В конец массива добавляются восемь значений-сентинелов $\mathtt{UINT\_MAX}$, благодаря чему пробный прыжок за последний настоящий элемент остаётся допустимым обращением к памяти. Макрос \texttt{IFLT8} выбирается при $p\geq16r$, в остальных случаях используется \texttt{IFLT4}.
|
||
|
||
Такой выбор учитывает разреженность требований, установленную в разд.~\Ref{PROBLEM}, но лишь двумя фиксированными режимами. При медианном $p/r=28{,}1$ шаг восемь уже применим, тогда как при $p/r=257$ он всё ещё остаётся равным восьми и не отражает фактическое среднее расстояние между требуемыми значениями.
|
||
|
||
Пусть $s_P$ и $s_R$ --- длины payload двух строк, а $d=|b_P-b_R|$. Холодное сравнение имеет оценку
|
||
\[
|
||
T_{\mathrm{cold}}=
|
||
O(s_P+s_R)+O\bigl(d\max(p,r)\bigr)+O(p+r).
|
||
\]
|
||
Поскольку формат ограничивает $10\leq b\leq32$, величина $d$ ограничена константой, и итоговая асимптотика линейна по размеру входных строк и декодированных множеств. При попадании первого операнда в кэш исчезает его декодирование, но остаются линейный поиск по кэшу, проверка строки, декодирование второго операнда, возможная нормализация и проход по массивам:
|
||
\[
|
||
T_{\mathrm{hit}}=
|
||
O(C+s_P+s_R)+O\bigl(d\max(p,r)\bigr)+O(p+r),
|
||
\qquad C=256.
|
||
\]
|
||
Здесь член $s_P$ отражает окончательную проверку ключа; на практике она выполняется только для записей с совпавшим коротким отпечатком. Дополнительная память одного холодного вызова составляет $O(p+r)$, не считая сохраняемого в процессе кэша первого операнда.
|
||
|
||
\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}
|