24 KiB
set:version
what is happening inside set:version
Актуальный код с наработками расположен в репозитории
Зачем
set:version в alt-rpm позволяет сопоставлять Provides и Requires пакетов не обычным сравнением версий, а сравнением специальных set-строк зависимостей вида
libfoo.so.X = set:<encoded-set>
encoded-set формируется на основе символов, необходимых/предоставляемых пакетом. Данный механизм позволяет гарантировать (с точностью до коллизий хэша, об этом будет далее) наличие всех требуемых символов в библиотеке. Это исключает ситуации, при которых ">= версий" ломается при удалении символа из библиотеки, а также ситуаций совпадения SONAME библиотек с разным набором символов.
set-строки (являющиеся перекодированным списком символов) генерируются способом, который позволяет их сравнивать между собой на предмет включения одного множества символов в другой.
Реализация set.c
set.c предоставляет 5 "публичных" API для работы set:version
int rpmsetcmp(const char *set1, const char *set2);
struct set *set_new(void);
void set_add(struct set *set, const char *sym);
const char *set_fini(struct set *set, int bpp);
struct set *set_free(struct set *set);
rpmsetcmp()
Основная функция, сравнивающая строки и выдающая результат в зависимости от включения:
- 1: set1 > set2
- 0: set1 == set2
- -1: set1 < set2
- -2: set1 != set2
- -3: set1 decoder error
- -4: set2 decoder error
на основе данной заметки, set1 лучше делать как Provides для лучшей производительности.
set_new()
Создаёт пустой объект struct set — контейнер для строк символов и их будущих hash-значений.
Из реализации:
internally struct set is just a bag of strings and their hash values.
set_add()
Добавляет строковый символ в set.
Использование:
set_add(s, "printf@@GLIBC_2.2.5");
set_add(s, "malloc@@GLIBC_2.2.5");
set_fini()
Финализирует множество и возвращает готовую set-version строку.
В качестве параметров принимает struct set *set и int bpp
При bpp < 10 или bpp > 32 функция возвращает NULL.
Работа функции описана далее.
set_free()
Освобождает struct set и его внутренние строки.
!!! NOTE В данный момент не освобождается сама структура struct set, память под которую в set_new() выделяется с xmalloc
Работа внутри set_fini()
Основной процесс преобразования массива строк в set-строку происходит следующим образом:
массив строк
| (Jenkins OAAT)
v
массив хэшей
| (qsort)
v
отсортированный массив хэшей
| (вычисление разницы между элементами)
v
массив delta
| (Rice-Golomb преобразование)
v
битовый массив
| (base62 преобразование)
v
set-строка
hash
Для каждого элемента (строки) высчитывается 32-битный хэш и обрезается до bpp младших бит
unsigned mask = (1u << bpp) - 1;
set->sv[i].v = hash(set->sv[i].s) & mask;
В качестве хэш-функции используется Jenkins OAAT.
Получившийся массив сортируется по хэш-значениям по возрастанию. При коллизии печатается warning: hash collision в stderr
С помощью функции uniqv() в массиве остаются только уникальные значения, возвращаемое значение - размер массива после удаления повторов.
Все дальнейшие преобразования происходят внутри функции encode_set(). Возвращаемое значение - длина итоговой set-строки, включая bpp и Mshift первыми двумя символами.
!!! NOTE Возвращаемое значение encode_set() может использоваться как код ошибки при отрицательных значениях
delta
Внутри функции encode_delta() происходит преобразование массива отсортированных по возрастанию чисел в массив дельт между числами, т.е.
Пример исходного массива:
unsigned *v = {1, 4, 16, 22};
Пример результирующего массива:
unsigned *v = {1, 3, 12, 6};
Rice-Golomb
encode_golomb()- main golomb encoding routine: package integers into bits.
Для сокращения длины итоговой строки применяется Rice-Golomb кодирование, которое позволяет с параметром Mshift записать число n как n >> Mshift, записанный последовательностью нулей, и n & 2^Mshift, записанный стандартным двоичным кодированием. Целая часть и остаток разделяются единичным битом.
Mshift выбирается как bpp - log2(c) - 1, т.к. при равномерном распределении c хэшей по диапазону 2^bpp, средняя delta будет равна 2^bpp / c
Пример:
Mshift = 8
n = 553
q = 553 >> 8 = 2
r = 553 & 255 = 42
bits = 00 1 00101010
Функция encode_golomb() возвращает длину итоговой битовой последовательности. Сама последовательность находится в char *bitv (char = [1,0])
base62
encode_base62()- pack bitv into base62 string
Последним шагом является преобразование битовой последовательности в base62 строку
Алфавитом для кодирования является 0-9,a-z,A-Z, но символ Z кодирует сразу 61, 62 и 63 следующим образом:
- в битовую последовательность после кодирования
Zдобавляется 2 бита:00для 6101для 6210для 63
- битовая последовательность продолжает кодироваться стандартным образом
Заметим, что данным образом невозможно получить последовательность ZZ, т.к. символ Z требует двух старших бит, выставленных в 11
Пример преобразования:
Mshift = 5
bits = 1 01111 00 1 00010
Или же
bits = 101111 001000 10
Биты читаются младшим битом вперёд. Первые 6 бит:
101111 = 1*1 + 0*2 + 1*4 + 1*8 + 1*16 + 1*32 = 61 = Z
Тогда в последовательность добавляется два бита 00 сразу после битов, кодирующих Z.
bits = 101111 000010 0010
000010 = 16 = g
0010(00) = 4 = 4
Итоговая строка в base62:
10111100100010 = Zg4
Сравнение set-строк
При сравнении set-строк происходит обратный процесс преобразования до получения хэш-значений
set-строка
| (обратное base62 преобразование)
v
битовый массив
| (обратное Rice-Golomb преобразование)
v
массив delta
| (вычисление изначальных значений)
v
массив хэшей
После массивы хэшей сравниваются между собой на наличие элементов одного массива в другом.
Магия внутри rpmsetcmp()
- У строки обрезается префикс
set:, если он присутствует, декодируется значениеbppиMshiftс помощьюdecode_set_init() set1декодируется с помощьюcache_decode_set()set2декодируется с помощьюdecode_set()- для каждого массива хэшей (
v1иv2) создаётся два буферных массиваv(1|2)buf(A|B)для функцииdownsample_set() - С помощью функции
downsample_set()bppобоих хэшей выравнивается до минимального изbpp1иbpp2. - Создаётся два флага
geиle, для определения вложенности множеств - Вложенность множеств проверяется с помощью трёх макросов:
IFGEIFLT4IFLT8 - Возвращается значение в зависимости от вложенности:
- 1: set1 > set2
- 0: set1 == set2
- -1: set1 < set2
- -2: set1 != set2
decode_set()
Под if(0) в функции decode_set() описано интуитивное преобразование с помощью функций decode_base62(), decode_golomb() и decode_delta().
На деле же используется оптимизированный вариант из decode_base62_golomb() и decode_delta(), благодаря которым set-строка сразу преобразуется в массив delta значений и после восстанавливается до массива хэшей.
decode_base62_golomb()
decode_base62_golomb() - оптимизированная версия стадий decode_base62 и decode_golomb. Функция считывает сразу по два байта, с помощью таблицы word_to_num преобразует их в битовую последовательность, затем, набирая до 24 бит, декодирует по Rice-Golomb.
enum
enum- word types (when two bytes from base62 string cast to unsigned short).
В коде используется enum для обозначения особых случаев при считывании символов:
enum {
W_AA = 0x0000, // два обычных символа (явно не используется)
W_AZ = 0x1000, // обычный символ + Z
W_ZA = 0x2000, // Z + обычный символ
W_A0 = 0x3000, // обычный символ + конец строки
W_0X = 0x4000, // конец строки
W_EE = 0xeeee, // невозможная ситуация
};
CCI macros
CCI - макрос, объединяющий два символа в индекс таблицы word_to_num, с учётом порядка байтов
word_to_num[]
static const unsigned short word_to_num[65536];
Предварительно скомпилированная таблица соответствия любой комбинации из двух байт с битовым представлением из 12 бит + возможный флаг
Таблица строится следующим образом:
-
все значения заполняются
W_EEкак ошибочные -
с помощью макроса
AA1строятся макросыAA1x2,AA1x25и т.д. вплоть доAA10x10и ему аналогичных.- итоговые макросы позволяют быстро заполнить таблицу значениями
[CCI(c1, c2)] = (c1 - b1) | ((c2 - b2) << 6)
- итоговые макросы позволяют быстро заполнить таблицу значениями
-
аналогичный процесс заполнения для
AZ, но с добавлением флагаW_AZк значениям в таблице. -
заполняется таблица для
ZAзначений с флагомW_ZA. Недопустимость старших бит11во втором символе (т.е. символы со значения 48) проверяется лишь внутри функций,W_EEдля таких значений не возвращается -
заполняется таблица для
A0и0Xзначений с флагамиW_A0иW_0Xсоответственно.
Считывание
Функция считывает с помощью макросов GetXX несколько бит (стандартно блоками до 24 бит) и передаёт их на декодирование
при 24 битах:
- берутся именно 4 base62-символа (две пары)
- значение помещается в 32-битный
unsigned - при ограничении
Mshift >= 7в одну последовательность может поместиться максимум 3 закодированных Golomb-числа.
"12 бит" считыватель используется в ситуациях, когда первая пара обычная, а вторая содержит специальный случай
"10 бит" считыватель используется в ситуациях с Z-escape
"6 бит" считыватель используется для одного символа.
Rice-Golomb декодер
Декодер постоянно находится в двух состояниях:
q-state: ищет unary-префикс и разделительную1r-state: добирает Mshift бит остаткаr
декодированное число восстанавливается как
value = (q << Mshift) | r;
Обработка Z-escape
Для обработки Z-escape используются макросы Esc1 и Esc2.
Esc1 вызывается при обнаружении символа Z и считывает старшие 2 бита у следующего символа. В результате Esc1 получает 10 бит для Golomb-декодера
Esc2 вызывается после завершения escape-пары и решает, что делать дальше:
- обычный символ — обработать 6 бит
- конец строки — завершить декодирование
- недопустимый символ — вернуть ошибку
- новый
Z— снова перейти вEsc1
QMake и RMake макросы
QMake ищет разделительную единицу в текущем блоке бит. Если блок содержит только нули, все они добавляются к q, после чего функция читает следующий блок.
Если в блоке есть единица, используется __builtin_ffs(bits) для определения позиции первого бита, разделительной для Golomb-кода. Оставшиеся биты заполняют r.
RMake проверяет, набралось ли Mshift бит остатка, записывает готовое значение
*v++ = (q << Mshift) | r;
Завершение строки
Допустимо завершение строки в q-state, но с не более чем 5 нулями. Завершение в r-state невозможно, т.к. остаток длиной Mshift не набрался.
cache_decode_set()
cache_decode_set()- special decode_set version with LRU caching.
Возвращает количество элементов хэша в массиве, помещает указатель *pv на массив хэшей.
Внутри cache_decode_set() создаются статические массивы:
static unsigned hv[CACHE_SIZE]- для хранения fingerprint set-строкиstatic struct cache_ent *ev[CACHE_SIZE]- для хранения полной информации о декодированной строке
В качестве fingerprint используется простой и быстро вычисляемый
unsigned hash = str[0] | (str[2] << 8) | (str[3] << 16);
массивы hv и ev представляют собой аналог LRU-кэша размером CACHE_SIZE=256.
Если fingerprint set-строки на декодирование совпал с имеющимся в hv массиве, проверяется полное соответствие строки с ev[i]->str. В случае hit - элемент помещается на нулевую позицию, остальные элементы сдвигаются.
При неудачной проверке полной set-строки, поиск по кэшу продолжается.
В случае, если элемента не оказалось в кэше, строка декодируется функцией decode_set(), добавляется SENTINELS=8 бит и помещается в кэш следующим образом:
- Если кэш не заполнен, декодируемая строка записывается на первую свободную позицию
- Если кэш заполнен, элемент ставится на
PIVOT_SIZE=243позицию, элементы с этой позиции сдвигаются.
SENTINELS значения
Данные ~0u значения необходимы при дальнейшем проходе по массиву внутри функции rpmsetcmp(), т.к. там происходят прыжки по 4 и 8 элементов.
downsample_set()
downsample_set()- reduce a set of (bpp + 1) values to a set of bpp values
Входной массив для работы - v.
Итоговый массив будет доступен по указателю w.
Возвращаемое значение - количество элементов в новом массиве w.
Т.к. первоначальный входной массив отсортирован, после обрезания до bpp бит массив будет поделён на две части, обе из которых будут отсортированы. Деление массива будет происходить в месте, где старший бит на позиции bpp+1 становится равным 1.
Далее обе половины массива объединяются в буфере w, дубликаты значений удаляются.
Пример:
v = [1, 3, 6, 8, 10, 14]
bpp = 3
mask = 7
v_mask = [1, 3, 6, 0, 2, 6]
w = [0, 1, 2, 3, 6]
return value = 5
Макросы IFGE IFLT4 IFLT8
Макросы IFLT*
IFLT8 быстро продвигает v1, пока *v1 < v2val. Сперва макрос "грубо" прыгает по 8 элементов, далее уточняет с шагом 4, 2, 1.
+8 +8 +8 ... // грубый поиск
-4 // откат
±2 // уточнение
±1 // уточнение
+1 возможно // финальная коррекция
В итоге v1 оказывается на первом элементе, который не меньше v2val.
IFLT4 делает аналогичную работу, но с шагом в 4. IFLT8 выбирается в случае, когда массив v1 превосходит по размеру массив v2 более чем в 16 раз.
Макрос IFGE
После IFLT* вариант *v1 < v2val уже невозможен.
Значит остаётся:
*v1 > v2val
То есть текущий элемент v2val есть во втором множестве, но отсутствует в первом.
Следовательно v1 уже не может быть надмножеством v2:
ge = 0;
v2++;
SELF_TEST флаг
При выставленном SELF_TEST флаге происходит следующее:
- Явно отключается
NDEBUGдля работыassert() - Компилируются
test_*функции - Компилируется
main(), запускающая всеtest_*функции
test_base62()
Проверяет работу функций:
encode_base62()
decode_base62()
test_golomb()
Проверяет работу функций:
encode_golomb()
decode_golomb()
test_word_table()
В таблице word_to_num[65536], необходимой для функции decode_base62_golomb(), проверяет, чтобы последовательность AA (двух не escape-символов) была равна (char_to_num[i] | (char_to_num[j] << 6). Для всех остальных ситуаций проверяется лишь значение одного из символов большее 61 char_to_num[i] >= 61 || char_to_num[j] >= 61.
test_base62_golomb()
Проверяет оптимизированный комбинированный декодер decode_base62_golomb(), сравнивая с эталонными:
decode_base62()
decode_golomb()
test_delta()
Проверяет работу функций:
encode_delta()
decode_delta()
test_set()
Проверяет полный encode/decode pipeline для множества чисел. Используемый bpp = 16.
Примечание о encode_set()
Внутри encode_set() есть строки:
#ifdef SELF_TEST
decode_delta(c, v);
#endif
это необходимо, т.к. далее в функции test_set() сравниваются изначальный и "после pipeline" массивы.
test_api()
Проверяет публичный API:
set_new()
set_add()
set_fini()
rpmsetcmp()
set_free()
Комментарии
additional
коды от Арсения для наглядности происходящего
some funny msg's
по коду неоднократно раскидано bpp < 10 || bpp > 32 проверки
запись char = [1,0] мне не нравится.
про "отрицательные значения = код ошибки" встречается в многих местах, стоит вынести отдельно проверить кодом примеры, особенно base62
кэш вечно копируется и переносится, немного странно PIVOT_SIZE странный
прыжки IFLT8 и IFLT4 я бы возможно делал как c1/c2
учитывая оптимизации, возможно стоит самостоятельно менять массивы местами до начала всех операций (но проблема с кэшем возможна)
хочу себе день, чтобы переписать это всё на English