Files
ARSV/articles/for-alt/article.md
T
2026-08-18 13:14:15 +03:00

11 KiB

Исследование и улучшение механизма работы set-строк в ALT RPM

Ориентир на 700 слов, 9к символов (!!!)/(???) - дополнить/уточнить всё ещё куда-то надо включить "хэш клёвый, но медленный, вот тесты" Проверить, чтобы всё, что нужно в это попало

1. Зачем нужны set-строки

Обычная зависимость от версии библиотеки не гарантирует, что в ней остались все нужные программе символы: символ можно удалить, не сменив SONAME, а одинаковые SONAME могут скрывать разные наборы экспортов. Поэтому ALT RPM использует версии зависимостей вида set:<encoded-set>. Для Provides такая строка описывает символы, предоставляемые библиотекой, а для Requires - символы, которые конкретный потребитель требует от неё. В такой конфигурации сравниваются не номера версий, а включение множеств: все хэши символов из Requires должны присутствовать в Provides. Гарантия вероятностная, поскольку вместо полноценных имён символов хранятся усечённые хэши, но ответ "символ отсутствует", когда он есть мы не получим

2. Структура set-строки

set-строка формируется следующим образом:

  1. Список символов формируется автодепами rpm-build.
  2. По количеству Provides символов (cnt) вычисляется bpp - количество бит до которых обрезается хэш символа при формировании строки. (!!!)
  3. Для каждого символа считается хэш-функция (используется Jenkins OAAT), хэш обрезается до bpp бит.
  4. Массив хэшей сортируются, повторы удаляются.
  5. Абсолютные значения заменяются дельтами между значениями
  6. Для кодировки Golomb-Rice вычисляется параметр Mshift=bpp - log2(cnt) - 1.
  7. Массив дельт сжимаются кодировкой Golomb-Rice.
  8. Битовый поток преобразуется в Base62-строку.

Примечание: при формировании set-строки для req символов, bpp вычисляется по количеству prov символов в актуальной (???) версии библиотеки для избежания сильного усечения хэшей при небольшом количестве требуемых символов.

Итоговая строка выглядит следующим образом:

set:<bpp_char><Mshift_char><base62 строка>
bpp_char = bpp - 7 + 'a'
Mshift_char = Mshift - 7 + 'a'

Краткая схема:

массив строк
  | (Jenkins OAAT)
  v
массив усечённых хэшей
  | (qsort)
  v
отсортированный массив хэшей
  | (вычисление разницы между элементами)
  v
массив delta
  | (Rice-Golomb преобразование)
  v
битовый массив
  | (base62 преобразование)
  v
set-строка

Механизм сравнения set-строк

Для проверки включения множества символов req в множество prov необходимо выполнить обратное декодирование следующим образом:

set-строка
  | (обратное base62 преобразование)
  v
битовый массив
  | (обратное Rice-Golomb преобразование)
  v
массив delta
  | (вычисление изначальных значений)
  v
массив усечённых хэшей (отсортированный)

Значения хэшей в массивах приводятся к минимальному bpp из двух set-строк (сохраняется отсортированность и дистинктивность(???)).

После получения отсортированных массивов хэшей из set-строк, включение символов (а точнее их усечённых хэш-значений) одного множества во второе проверить не составляет труда.

3. Практическая реализация

Несмотря на описанную структуру set-строки, на практике в текущем lib/set.c применяется множество оптимизаций и улучшений, направленных на ускорение работы rpmsetcmp(const char *set1, const char *set2) (функции, выдающей результат включения множеств). Рассмотрим основные из них.

3.1. Слитый декодер

Вместо описанной выше последовательности декодирования set-строк применяется функция, объединяющая этапы base62 и golomb.

decode_base62_golomb() - оптимизированная версия стадий decode_base62 и decode_golomb. Функция считывает сразу по два байта, с помощью пре-compiled таблицы преобразует их в битовую последовательность, набирая блоки до 24 бит, после декодирует по Rice-Golomb.

Благодаря такому подходу удаётся ускорить работу алгоритма на сравнении строк в ~2 раза. (!!!)

3.2. Кэширование Provides set-строк

При передачи первого параметра (const char *set1) в функцию сравнения set-строк (rpmsetcmp()), set-строка кэшируется. Используется простой LRU кэш (массив размером 256), который сохраняет fingerprint оригинальной set-строки, саму set-строку и декодированный массив хэшей.

При cache_hit элемент смещается на первую позицию, а при первом попадании попадает на позицию min(243, len(cache)).

Очевидным недостатком такого подхода является:

  1. Малый размер кэша при увеличении размера кэша до 512 элементов, производительность увеличилась на X% (!!!)

  2. Затраты на realloc при cache_hit из-за хранения элементов кэша как массив (а не списком, например), после каждого попадания кэша приходится смещать до 255 записей.

3.3. Быстрое сравнение включения множеств символов

После получения отсортированных и усечённых до одинакового bpp масивов хэшей, необходимо проверить включение множеств. Отметим, что множество req символов будет, как правило, разреженным относительно множества prov символов.

lib/set.c делает это с помощью макроса IFLT4(IFLT8).

Данный макрос отвечает за быстрые прыжки на 4(8) элементов массива, и последующее уточнение на 2(4), 1(2) и 0(1) элемента, пока не найдём позицию, где hash_arr1[i] <= hash_arr2[j] > hash_arr1[i+1].

Макрос IFLT8 с первоначальным прыжком на 8 элементов выбирается при len(hash_arr1) >= 16 * len(hash_arr2). В остальных случаях используется макрос IFLT4.

Из недостатков данного способа выделяется фиксированная длина прыжка, которую имеет смысл увеличивать пропорционально len(hash_arr1) / len(hash_arr2).

4. Дальнейшие оптимизации

(???) Говорить ли про python-реализацию вовсе

Текущая реализация lib/set.c трудночитаемая и труднопонимаемая, а также не содержит некоторых оптимизаций, которые могли бы сильнее ускорить работу библиотеки.

В связи с этим, было принято решение о реимплементации кода с сохранением совместимости к текущему формату set-строк.

4.1. Слитый энкодер

В прошлом разделе говорилось о ускорении работы функции rpmsetcmp(), однако для части кода отвечающей за создание set-строк как таковых оптимизаций не существует. Поэтому энкодер в новой версии пропускает стадию создания битового масива и напрямую декодирует base62 строки в массив delta.

4.2. Общая память под строки

Для улучшения encode составляющей также была изменена работа с памятью под символы. В новой версии вместо множества указателей на строки, под каждый из которых требуется свой malloc, введён единый указатель, в котором хранятся все символы последовательно, а индекс начала каждого из символа хранится отдельно.

Это позволяет снизить часть расходов на malloc.

4.3. Radix-sort

4.3. Изменённый кэш