11 KiB
Исследование и улучшение механизма работы set-строк в ALT RPM
Ориентир на 700 слов, 9к символов
(!!!)/(???) - дополнить/уточнить
всё ещё куда-то надо включить "хэш клёвый, но медленный, вот тесты"
Проверить, чтобы всё, что нужно в это попало
1. Зачем нужны set-строки
Обычная зависимость от версии библиотеки не гарантирует, что в ней остались все нужные программе символы: символ можно удалить, не сменив SONAME, а одинаковые SONAME могут скрывать разные наборы экспортов. Поэтому ALT RPM использует версии зависимостей вида set:<encoded-set>. Для Provides такая строка описывает символы, предоставляемые библиотекой, а для Requires - символы, которые конкретный потребитель требует от неё. В такой конфигурации сравниваются не номера версий, а включение множеств: все хэши символов из Requires должны присутствовать в Provides.
Гарантия вероятностная, поскольку вместо полноценных имён символов хранятся усечённые хэши, но ответ "символ отсутствует", когда он есть мы не получим
2. Структура set-строки
set-строка формируется следующим образом:
- Список символов формируется автодепами
rpm-build. - По количеству
Providesсимволов (cnt) вычисляетсяbpp- количество бит до которых обрезается хэш символа при формировании строки. (!!!) - Для каждого символа считается хэш-функция (используется Jenkins OAAT), хэш обрезается до
bppбит. - Массив хэшей сортируются, повторы удаляются.
- Абсолютные значения заменяются дельтами между значениями
- Для кодировки Golomb-Rice вычисляется параметр
Mshift=bpp - log2(cnt) - 1. - Массив дельт сжимаются кодировкой Golomb-Rice.
- Битовый поток преобразуется в 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)).
Очевидным недостатком такого подхода является:
-
Малый размер кэша при увеличении размера кэша до 512 элементов, производительность увеличилась на X% (!!!)
-
Затраты на
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.