190 lines
16 KiB
Markdown
190 lines
16 KiB
Markdown
# Исследование и улучшение механизма работы `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` символов в актуальной (???) версии библиотеки для избежания сильного усечения хэшей при небольшом количестве требуемых символов.
|
||
|
||
Итоговая строка выглядит следующим образом:
|
||
|
||
```text
|
||
set:<bpp_char><Mshift_char><base62 строка>
|
||
```
|
||
|
||
```text
|
||
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
|
||
|
||
Вместо `qsort` при сортировке хэшей символов теперь используется `radix sort` на количестве элементов массива >128. (при <=128 остаётся `qsort`)
|
||
|
||
### 4.4. Изменённый кэш
|
||
|
||
Кэш претерпел множество изменений, т.к. давал сильный прирост в скорости (!!!)
|
||
|
||
1. Изменён размер до 512 значений.
|
||
2. Добавлен кэш также и для `req` set-строк. Теперь порядок аргументов не имеет значения для производительности.
|
||
3. Кэш теперь строится на списках, а не на массиве, благодаря чему более нет затрат на `realloc` при cache hit.
|
||
|
||
### 4.5. Изменённый декодер
|
||
|
||
Оставляя изначальную идею слитого декодера, была написана его реимлементация, использующая более простую логику, но использующая 64-битные блоки, а также упрощённую precompiled таблицу. (???) если есть результаты ускорения, сюда надо
|
||
|
||
### 4.6. Тесты производительности
|
||
|
||
Для проверки производительности сравнивались две локальные сборки `librpm`: исходная реализация и версия с описанными изменениями. Замеры выполнялись на одинаковых симуляционных командах `apt-get`.
|
||
|
||
Коэффициент считается как отношение времени исходной реализации к времени изменённой. Значение больше `1` - ускорение, меньше `1` - замедление.
|
||
|
||
| Команда | `set.c` user, с | `set.c` system, с | `set9.c` user, с | `set9.c` system, с | Ускорение по user | Ускорение по user+system |
|
||
| --------------------------- | --------------: | ----------------: | ---------------: | -----------------: | ----------------: | -----------------------: |
|
||
| `-s check` | 0.453 | 0.033 | 0.503 | 0.040 | 0.901× | 0.896× |
|
||
| `-s autoremove` | 0.763 | 0.040 | 0.810 | 0.040 | 0.942× | 0.945× |
|
||
| `-s install rpm-build` | 1.210 | 0.050 | 1.283 | 0.050 | 0.943× | 0.945× |
|
||
| `-s install openuds-server` | 3.747 | 0.080 | 3.897 | 0.083 | 0.962× | 0.961× |
|
||
| `-s install password-store` | 1.233 | 0.050 | 1.310 | 0.050 | 0.941× | 0.944× |
|
||
|
||
## 5. Прочие исследования
|
||
|
||
В данной главе собраны исследования, которые не вошли непосредственно в реализацию новой версии, однако важны своими идеями и результатами.
|
||
|
||
### 5.1. Реализация без промежуточной кодировки хэшей
|
||
|
||
Первое направление - отказаться от `delta` и `Golomb-Rice` и хранить отсортированные усечённые хэши почти напрямую. Заметная часть времени при сравнении тратится на декодирование set-строки. Если сделать строку дешевле в декодировании, можно получить выигрыш на холодном сравнении и проигрыш в длине строки.
|
||
|
||
(!!!) Результаты теста
|
||
|
||
Также было рассмотрено исользование `Roaring Bitmap`, однако его эффективность достигается для плотных множеств, что не актуально при работе с хэш-функциями.
|
||
|
||
### 5.2. Тестирование коллизий хэш-функций
|
||
|
||
Отдельно проверялась идея заменить `Jenkins OAAT` на более современную хэш-функцию. По скорости на длинных строках Jenkins действительно проигрывает (!!!)
|
||
|
||
Однако при тестировании коллизий оказалость, что все три функции на вероятностной побитовой карте показывают близкие к "идеальным" результаты. (!!!)
|
||
|
||
## 6. Итоги
|
||
|
||
(нужны ли..)
|