Compare commits
44
Commits
7518ed0b2f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e38d1365cd | ||
|
|
9e9c99fc2b | ||
|
|
87b588f63e | ||
|
|
8a53f2186b | ||
|
|
1fac5b277d | ||
|
|
7106d0c9ea | ||
|
|
f86537626a | ||
|
|
548159e5ea | ||
|
|
b1806cda5e | ||
|
|
e7836fdf31 | ||
|
|
45033e7f6d | ||
|
|
8c25773fcc | ||
|
|
7b5fa1864b | ||
|
|
be1fcd06b5 | ||
|
|
9f3b01f445 | ||
|
|
74e3c9d6d3 | ||
|
|
4eb56ae891 | ||
|
|
60ffed1f6f | ||
|
|
6fe66fa1ba | ||
|
|
d33caacfef | ||
|
|
0b7f6ea13a | ||
|
|
f4c170058e | ||
|
|
ac894a86b4 | ||
|
|
53332b0f62 | ||
|
|
a5e583f600 | ||
|
|
8b65eb5e37 | ||
|
|
39b4673421 | ||
|
|
4ed05f2061 | ||
|
|
ebb00ee69f | ||
|
|
eda5e3454a | ||
|
|
3ecc491022 | ||
|
|
3efd0bf037 | ||
|
|
69151d54dd | ||
|
|
da43582394 | ||
|
|
e8d99cf676 | ||
|
|
97353ebddc | ||
|
|
8fa1b39d12 | ||
|
|
6584a628ce | ||
|
|
1c9a9be086 | ||
|
|
f0a952c083 | ||
|
|
4bd99cf68f | ||
|
|
292421d970 | ||
|
|
b95decdffb | ||
|
|
086dda83ca |
+2
-1
@@ -1,3 +1,4 @@
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 2
|
||||
ColumnLimit: 100
|
||||
BracedInitializerIndentWidth: 2
|
||||
ColumnLimit: 100
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Сравнение set-зависимостей Sisyphus с `newset.c`
|
||||
|
||||
Скрипт [`scripts/compare_sisyphus_set_versions.sh`](../scripts/compare_sisyphus_set_versions.sh)
|
||||
сравнивает set-строки из RPM-заголовка с результатом `reimplement/newset.c`.
|
||||
|
||||
## Что именно сравнивается
|
||||
|
||||
1. Из изолированных APT-индексов `Sisyphus/x86_64` и `Sisyphus/noarch`
|
||||
берутся `Package`, `Architecture`, `Version`, `Filename`, `MD5Sum`,
|
||||
`Provides` и `Depends`/`Pre-Depends`.
|
||||
2. RPM скачивается непосредственно из `Sisyphus/files/<arch>/RPMS` и
|
||||
проверяется по `MD5Sum` из индекса.
|
||||
3. Эталонные зависимости читаются из RPM-заголовка через массивы тегов:
|
||||
`PROVIDENAME/PROVIDEFLAGS/PROVIDEVERSION` и
|
||||
`REQUIRENAME/REQUIREFLAGS/REQUIREVERSION`.
|
||||
4. Для воспроизведения зависимостей вызываются установленные вместе с
|
||||
`rpm-build` программы `%_rpmlibdir/find-provides` и
|
||||
`%_rpmlibdir/find-requires` с методом `none,lib`.
|
||||
5. В каталоге инструментов заменяется **только** `mkset`: вместо штатного
|
||||
`mkset` подставляется совместимая обвязка над `newset.c`. `provided_symbols`,
|
||||
`ldd --bindings`, `eu-readelf`, `objdump`, `file` и остальные средства
|
||||
отбора/извлечения символов остаются штатными инструментами `rpm-build`.
|
||||
|
||||
Для `Requires` APT получает точный уже скачанный RPM-файл как вход и скачивает
|
||||
его полный runtime dependency closure в пустой пользовательский RPM root. Так
|
||||
версия и архитектура целевого пакета не выбираются повторно по имени. APT
|
||||
запускается с пустым `APT_CONFIG`; source list, package cache, archives, lists,
|
||||
status и preferences перенаправлены во временный каталог, поэтому настройки,
|
||||
hooks и RPM-база основной системы не участвуют в разрешении зависимостей.
|
||||
|
||||
Перед распаковкой проверяются имена элементов cpio и цели символических ссылок.
|
||||
Абсолютная RPM-ссылка считается путём внутри виртуального package root: например,
|
||||
`/usr/sbin/update-alternatives -> /bin/true` безопасно переписывается в
|
||||
`../../bin/true`, а не направляется в `/bin/true` основной системы. Все symlink
|
||||
сначала собираются из RPM-заголовков и создаются в порядке от родительских путей
|
||||
к дочерним; затем `cpio` извлекает только нессылочные элементы. Благодаря этому
|
||||
usrmerge-ссылки вроде `/bin -> usr/bin` создаются до файлов `/bin/*`.
|
||||
|
||||
Имена элементов с выходом через `..` и относительные symlink, фактически
|
||||
выходящие из временного root, по-прежнему отклоняются. После проверки RPM-файлы
|
||||
распаковываются без установки. Это позволяет штатному `rpm-build/ldd`
|
||||
использовать интерпретатор и библиотеки из того же снимка Sisyphus, не изменяя
|
||||
систему.
|
||||
|
||||
## Ограниченный тест
|
||||
|
||||
На ALT p11 с установленным `rpm-build`:
|
||||
|
||||
```sh
|
||||
bash scripts/compare_sisyphus_set_versions.sh \
|
||||
--package strace \
|
||||
--report strace-set-report.tsv
|
||||
```
|
||||
|
||||
Несколько пакетов:
|
||||
|
||||
```sh
|
||||
bash scripts/compare_sisyphus_set_versions.sh \
|
||||
--package strace \
|
||||
--package libdw \
|
||||
--report sample-set-report.tsv
|
||||
```
|
||||
|
||||
Либо первые десять записей индекса:
|
||||
|
||||
```sh
|
||||
bash scripts/compare_sisyphus_set_versions.sh \
|
||||
--limit 10 \
|
||||
--report sample-set-report.tsv
|
||||
```
|
||||
|
||||
Без `--all`, `--limit` или `--package` скрипт завершится до обращения к
|
||||
репозиторию. Это защищает от случайного полного запуска.
|
||||
|
||||
## Полный запуск
|
||||
|
||||
Команда предусмотрена, но в ходе разработки не запускалась:
|
||||
|
||||
```sh
|
||||
bash scripts/compare_sisyphus_set_versions.sh \
|
||||
--all \
|
||||
--report sisyphus-set-report.tsv
|
||||
```
|
||||
|
||||
Продолжение после прерывания:
|
||||
|
||||
```sh
|
||||
bash scripts/compare_sisyphus_set_versions.sh \
|
||||
--all \
|
||||
--resume \
|
||||
--report sisyphus-set-report.tsv
|
||||
```
|
||||
|
||||
`--resume` пропускает только уже завершённую комбинацию
|
||||
`package/architecture/version`; обновившийся пакет будет обработан заново.
|
||||
Завершённость подтверждается маркером `complete=1` в полной 11-польной строке
|
||||
`SUMMARY`, поэтому оборванная при записи строка не считается результатом.
|
||||
|
||||
Во время работы отчёт можно наблюдать отдельно:
|
||||
|
||||
```sh
|
||||
tail -f sisyphus-set-report.tsv
|
||||
```
|
||||
|
||||
`START` записывается до обработки пакета, а `DEPENDENCY` и `SUMMARY` — сразу
|
||||
после сравнения пакета. Поэтому файл обновляется в процессе, а не только после
|
||||
завершения всего обхода.
|
||||
|
||||
## Статусы отчёта
|
||||
|
||||
- `match` — set-строка из RPM совпала с результатом `newset.c`;
|
||||
- `mismatch` — capability и оператор совпали, set-строки различаются;
|
||||
- `missing_generated` — RPM содержит set-зависимость, но повторный запуск
|
||||
`rpm-build` её не сгенерировал;
|
||||
- `extra_generated` — повторный запуск сгенерировал зависимость, которой нет в
|
||||
RPM-заголовке;
|
||||
- `no_set_metadata` — в APT metadata пакета нет set-строк;
|
||||
- `*_error` — ошибка скачивания, контрольной суммы, распаковки либо генератора.
|
||||
|
||||
`extra_generated` показывается в строках `DEPENDENCY` и делает итоговый статус
|
||||
пакета равным `mismatch`. При анализе нужно учитывать, что бинарный RPM не
|
||||
содержит исходных spec-фильтров (`filter_from_requires` и аналогичных), поэтому
|
||||
часть таких расхождений может относиться не к `newset.c`, а к невозможности
|
||||
полностью воспроизвести фильтрацию исходного spec. Их число отдельно указано в
|
||||
поле `extras=N` строки `SUMMARY`.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Notes
|
||||
|
||||
## to read
|
||||
|
||||
https://www.altlinux.org/Features/Specific
|
||||
|
||||
## Hash
|
||||
|
||||
в данный момент используется Jenkins OAAT. Считается чуть устаревшим(check), главным аналогом является:
|
||||
|
||||
https://github.com/cyan4973/xxhash
|
||||
|
||||
xxHash64 (XXH64): относительно простая имплементация на си, хвалят за скорость, 64 бит, параллель
|
||||
|
||||
необходимы тесты и графики:
|
||||
|
||||
1. насколько быстрее/медленнее
|
||||
2. коллизии на наборе букв в сравнении с рандомными данными
|
||||
3. коллизии у первого и второго хэшей между собой
|
||||
|
||||
> В XXH3 внутренний цикл, который оптимально обрабатывается векторизацией. Благодаря этому функция использует аппаратную поддержку на наборах инструкций SSE2, AVX2 и NEON. Производительность зависит от комилятора. Неожиданно оказалось, что версия, скомпилированная clang, намного превосходит остальные. Ян Колле даже подумал, что производительность хеш-функции здесь превысит пропускную способность памяти. Этой версии на графике соответствует пунктирная линия.
|
||||
|
||||
есть ещё cityhash от гугла, но она c++
|
||||
|
||||
в случае XXH64 строк кода ~200, можно вставить в файл
|
||||
в случае XXH3 строк кода ~2к, стоит использовать библиотеку напрямую, бессмысленно
|
||||
|
||||
## about collizions
|
||||
|
||||
> А вы пробовали побитовую вероятностную карту строить?
|
||||
> Есть у нас по условию множество из входных значений, которые замапились в N хешей (включая коллизии!). Эти N хешей имеют одинаковый размер и, по сути, являются массивами из K бит.
|
||||
> Так вот можно среди этих хешей посчитать 0 и посчитать 1, получится K распределений. Чем они все ближе к пополамному ± 6%, тем лучше.
|
||||
> https://raw.altlinux.team/arseny/snippets/2026-06/QSIrJSuwxp.txt
|
||||
> https://raw.altlinux.team/arseny/snippets/2026-06/QSIrJSuwxp.c
|
||||
|
||||
## free
|
||||
|
||||
free не делает очистку самой структуры, valgrind
|
||||
|
||||
## data
|
||||
|
||||
алфавит для символ библиотек:
|
||||
|
||||
```
|
||||
.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz
|
||||
```
|
||||
|
||||
- предоставленные/определенные символы из библиотек обычно используют @@VERSION
|
||||
- обязательные/неопределенные символы из двоичных файлов обычно используют @VERSION
|
||||
|
||||
## additional about set:version.md
|
||||
|
||||
[коды](https://altlinux.space/arseny/atsv-research) от Арсения для наглядности происходящего
|
||||
|
||||
some funny [msg's](https://lists.pld-linux.org/mailman/pipermail/pld-devel-en/2013-November/012467.html)
|
||||
|
||||
по коду неоднократно раскидано `bpp < 10 || bpp > 32` проверки
|
||||
|
||||
запись `char = [1,0]` мне не нравится.
|
||||
|
||||
про "отрицательные значения = код ошибки" встречается в многих местах, стоит вынести отдельно
|
||||
проверить кодом примеры, особенно base62
|
||||
|
||||
кэш вечно копируется и переносится, немного странно
|
||||
PIVOT_SIZE странный
|
||||
|
||||
прыжки IFLT8 и IFLT4 я бы возможно делал как c1/c2
|
||||
|
||||
учитывая оптимизации, возможно стоит самостоятельно менять массивы местами до начала всех операций (но проблема с кэшем возможна)
|
||||
|
||||
хочу себе день, чтобы переписать это всё на English
|
||||
@@ -1,46 +0,0 @@
|
||||
# raw_data
|
||||
|
||||
## messages from tg
|
||||
|
||||
[6/23/26 9:57 PM] Arseny: Кстати. У меня есть серьёзная математическая, она же очень серьёзная алгоритмическая задача на исследование. ALT set:version (https://git.altlinux.org/gears/r/rpm.git?a=blob;f=lib/set.c) нужен майнтейнер.
|
||||
[6/23/26 9:57 PM] Arseny: Вы хотели алгоритмов? Вот, расскажите мне про этот.
|
||||
[6/23/26 9:57 PM] Arseny: весь алгоритм можно разбить на 4 последовательных стадии.
|
||||
|
||||
0) Задача в том, чтобы проверять, выполняется ли R ⊂ P.
|
||||
множества, стоящие в позиции P, даются командой навроде nm --dynamic -j -U /usr/lib64/libyourfavourite.so.N.
|
||||
множества, стоящие в позиции R, даются командой навроде nm --dynamic -j -u /usr/bin/yourfavouriteprog, или там может быть иной .so.
|
||||
Тут, наверное, ещё и введение в предметную область должно быть.
|
||||
|
||||
1) hash: N элементов сбрасываются в этот самый хеш; его эффективность бы стоило оценить хорошенько. (гипотеза от меня: он вообще-то плохой и даёт много коллизий на интересных нам инпутах), в результате K <= N отсортированных хешей, чисел из [0; 2**10) (по умолчанию 10 бит на хеш);
|
||||
2) delta: K <= N чисел из прошлого шага заменяются на разности с предыдущими, и получаются по-другому распределённые числа, чаще маленькие и очень, очень мало сверхбольших; есть мнение, что они для равномерно распределённых хешей распределены геометрически;
|
||||
3) golomb: эти K разностей кодируются по Rice-Golomb (https://en.wikipedia.org/wiki/Golomb_coding), делитель по умолчанию 2**7; получается префиксный код из K битострок, которые можно слева направо прочесть без lookahead и восстановить массив разностей;
|
||||
4) (якобы) base62: их конкатенация кодируется в буквы.
|
||||
|
||||
(2) и (3) близки к очевидно-понятным; я поизучал, реализовал на питоне (https://altlinux.space/arseny/atsv-research) и снова поизучал.
|
||||
А вот (1) и (4) я не занимался совсем.
|
||||
От (4) у меня тоже башка взрывается, а (1) можно очень хорошо так поизучать, и существует, наверное, обширная литература.
|
||||
[6/23/26 9:57 PM] Arseny: grep -A16 Jenkins lib/set.c
|
||||
вот в этом дереве исходников (https://git.altlinux.org/gears/r/rpm.git?p=rpm.git;a=blob;f=lib/set.c;h=9474a2ee6d7c;hb=a01a87db6e8a)
|
||||
[6/23/26 9:57 PM] Arseny: Пусть параметр-делитель равен M, и энкодеру на вход поступает число A. Тогда ищутся такие q и r < q, что A = qM + r. Частное кодируется унарной кодировкой из единиц, за ним следует нулик, а далее ровно log₂M бит на остаток. Подобрать M нужно с умом.
|
||||
```sh
|
||||
python3 stringset.py 10 7 <<EOF
|
||||
foo
|
||||
bar
|
||||
quux
|
||||
EOF
|
||||
hashes=[298, 487, 588]
|
||||
deltas=[298, 189, 101]
|
||||
codes=[b'1100101010', b'100111101', b'01100101']
|
||||
golomb divisor: 128 (minimum word bit length: 8)
|
||||
total bit length: 27
|
||||
per-elem bit length: 9.000
|
||||
```
|
||||
Но здесь не хеш из файла выше, а питонический hash() обычный. Чтобы смотреть на актуальные результаты, нужно подсунуть актуальный.
|
||||
[6/23/26 9:58 PM] Arseny: А вот страничка set:version на ALT Wiki (https://www.altlinux.org/Set-version)
|
||||
[6/23/26 9:58 PM] Arseny: Putze, F.; Sanders, P.; Singler, J. (2007),
|
||||
253 * "Cache-, Hash- and Space-Efficient Bloom Filters",
|
||||
254 * http://algo2.iti.uni-karlsruhe.de/singler/publications/cacheefficientbloomfilters-wea2007.pdf
|
||||
[6/23/26 9:58 PM] Arseny: Сайта нет, но статью поискать https://t.me/c/1622441719/1359/9771
|
||||
[6/23/26 9:59 PM] Arseny: И ещё наследие Алексея Турбина:
|
||||
https://ftp.altlinux.org/pub/people/at/
|
||||
https://github.com/svpv
|
||||
@@ -1,444 +0,0 @@
|
||||
# `rpmquery` в ALT Linux
|
||||
|
||||
## Краткий вывод
|
||||
|
||||
`rpmquery` — отдельная точка входа в тот же механизм запросов RPM, что и
|
||||
`rpm -q`. В ALT Linux p11 `/usr/bin/rpmquery` является символьной ссылкой на
|
||||
ELF-программу `/usr/lib/rpm/rpmq`; имя запуска заранее включает режим запроса,
|
||||
поэтому `-q` обычно не нужен:
|
||||
|
||||
```sh
|
||||
rpmquery rpm
|
||||
rpmquery -q rpm
|
||||
rpm -q rpm
|
||||
```
|
||||
|
||||
Все три команды дают один и тот же результат. При этом `rpmquery` не является
|
||||
интерфейсом к репозиторию APT: без `-p` он читает только установленную базу RPM,
|
||||
по умолчанию `/var/lib/rpm`. Для поиска доступных, но не установленных пакетов
|
||||
нужны `apt-cache`, `apt-get` или веб/RDB-интерфейсы ALT.
|
||||
|
||||
Исследование проверено на официальном контейнере `alt:p11`, обновлённом до
|
||||
`rpm-4.13.0.1-alt45.x86_64`, и по ветке `p11` исходников RPM, commit
|
||||
`7e71ed3da438289d916f1cccc9c0a83e594455dc`.
|
||||
|
||||
## Где ищет `rpmquery`
|
||||
|
||||
| Вызов | Источник данных |
|
||||
|---|---|
|
||||
| `rpmquery NAME` | установленная база RPM |
|
||||
| `rpmquery -a` | все пакеты из установленной базы RPM |
|
||||
| `rpmquery -p FILE.rpm` | заголовок указанного RPM-файла, установка не требуется |
|
||||
| `rpmquery --root ROOT ...` | база и файловая система относительно `ROOT` |
|
||||
| `rpmquery --dbpath DIR ...` | явно указанная база RPM |
|
||||
|
||||
Практическая граница была проверена так: `apt-cache show strace` находил пакет
|
||||
в p11, тогда как `rpmquery strace` отвечал `package strace is not installed` и
|
||||
завершался с кодом `1`.
|
||||
|
||||
## Работа с APT-репозиторием
|
||||
|
||||
APT-RPM загружает индексы подключённых репозиториев в `/var/lib/apt/lists`.
|
||||
Обычный порядок работы:
|
||||
|
||||
```sh
|
||||
# Показать настроенные источники ALT, если установлен пакет apt-repo.
|
||||
apt-repo
|
||||
|
||||
# Обновить локальные индексы после изменения источников.
|
||||
apt-get update
|
||||
```
|
||||
|
||||
В минимальных контейнерах `apt-repo` может отсутствовать; источники всё равно
|
||||
задаются файлами `/etc/apt/sources.list` и `/etc/apt/sources.list.d/*.list`.
|
||||
|
||||
Сами `pkglist.*` являются бинарными header-list файлами RPM, поэтому искать в
|
||||
них обычным `grep` не следует. Для доступа к кэшу предназначен `apt-cache`:
|
||||
|
||||
| Задача | Команда |
|
||||
|---|---|
|
||||
| Поиск по имени и описанию | `apt-cache search REGEX` |
|
||||
| Перечень имён | `apt-cache pkgnames` |
|
||||
| Выбранная версия и приоритеты источников | `apt-cache policy PACKAGE` |
|
||||
| Полная доступная запись, включая версии зависимостей | `apt-cache show PACKAGE` |
|
||||
| Граф разрешения зависимостей | `apt-cache depends PACKAGE` |
|
||||
| Обратные зависимости | `apt-cache whatdepends PACKAGE` |
|
||||
| Низкоуровневое представление версий и зависимостей | `apt-cache showpkg PACKAGE` |
|
||||
| Метаданные исходного пакета | `apt-cache showsrc PACKAGE` |
|
||||
| Все доступные записи репозиториев | `apt-cache dumpavail` |
|
||||
| Проверка плана установки без изменений | `apt-get -s install PACKAGE` |
|
||||
| Только скачать бинарный пакет и зависимости | `apt-get -d install PACKAGE` |
|
||||
| Скачать исходники | `apt-get source PACKAGE` |
|
||||
|
||||
`apt-cache depends` удобен для просмотра найденных провайдеров, но в его
|
||||
обычном выводе set-версия зависимости теряется. Для полных set-строк нужны
|
||||
`apt-cache show`, `apt-cache showpkg`, скачанный RPM или RDB API.
|
||||
|
||||
## Set-строки пакета из APT-репозитория
|
||||
|
||||
### Способ 1: прямо из локального кэша APT
|
||||
|
||||
Это самый короткий способ, установка и скачивание RPM не нужны:
|
||||
|
||||
```sh
|
||||
pkg=libdw
|
||||
|
||||
# Сначала проверить, какую версию и из какого источника выбрал APT.
|
||||
apt-cache policy "$pkg"
|
||||
|
||||
# Полные поля с set-Requires и set-Provides.
|
||||
apt-cache show "$pkg" |
|
||||
sed -n -E '/^(Pre-Depends|Depends|Provides):/p' |
|
||||
grep -F 'set:'
|
||||
```
|
||||
|
||||
В проверенном p11 поля имели вид:
|
||||
|
||||
```text
|
||||
Depends: ... liblzma.so.5()(64bit) (>= set:kiyIz7cr3p0), ...
|
||||
Provides: libdw.so.1()(64bit) (= set:ldm5ZH1pjPBqjZ9bdJbLDnKGbC1hkJSX...)
|
||||
```
|
||||
|
||||
`Depends` и `Pre-Depends` соответствуют требованиям пакета, `Provides` — тому,
|
||||
что пакет предоставляет. `apt-cache show` может вывести несколько записей,
|
||||
если в подключённых источниках доступны разные версии; выбранный кандидат
|
||||
виден в `apt-cache policy`.
|
||||
|
||||
Для выгрузки всего подключённого репозитория можно использовать:
|
||||
|
||||
```sh
|
||||
apt-cache dumpavail >available.txt
|
||||
grep -E '^(Pre-Depends|Depends|Provides):.*set:' available.txt
|
||||
```
|
||||
|
||||
В отличие от HTML-страниц `packages.altlinux.org`, кэш APT содержит полные
|
||||
set-строки без визуального многоточия.
|
||||
|
||||
### Способ 2: скачать RPM через APT и запросить его заголовок
|
||||
|
||||
Это наиболее точный локальный способ: выводятся исходные массивы тегов RPM и
|
||||
их удобно преобразовать в TSV. `apt-get -d` скачивает также отсутствующие
|
||||
зависимости, но ничего не устанавливает.
|
||||
|
||||
```sh
|
||||
pkg=strace
|
||||
cache=$(mktemp -d)
|
||||
trap 'rm -rf "$cache"' EXIT
|
||||
mkdir -p "$cache/partial"
|
||||
|
||||
apt-get -y -d \
|
||||
-o "Dir::Cache::archives=$cache" \
|
||||
install "$pkg"
|
||||
|
||||
rpmfile=$(find "$cache" -maxdepth 1 -type f \
|
||||
-name "${pkg}_*.rpm" -print -quit)
|
||||
```
|
||||
|
||||
Set-Requires выбранного пакета:
|
||||
|
||||
```sh
|
||||
rpmquery -p --qf \
|
||||
'[%{REQUIRENAME}\t%{REQUIREFLAGS:depflags}\t%{REQUIREVERSION}\n]' \
|
||||
"$rpmfile" |
|
||||
awk -F '\t' '$3 ~ /^set:/'
|
||||
```
|
||||
|
||||
Set-Provides выбранного пакета:
|
||||
|
||||
```sh
|
||||
rpmquery -p --qf \
|
||||
'[%{PROVIDENAME}\t%{PROVIDEFLAGS:depflags}\t%{PROVIDEVERSION}\n]' \
|
||||
"$rpmfile" |
|
||||
awk -F '\t' '$3 ~ /^set:/'
|
||||
```
|
||||
|
||||
Для `strace-7.0-alt1.x86_64.rpm` в p11 получены два требования:
|
||||
|
||||
```text
|
||||
libdw.so.1()(64bit) >= set:lgHyMNBkvSTjjY0en8Yi3vFojgjVM7jRsRF4zCFBzNVmhhGF
|
||||
libselinux.so.1()(64bit) >= set:liZ0N709Wr2dbKPs2
|
||||
```
|
||||
|
||||
Set-Provides у этого пакета нет: исполняемый `strace` требует библиотеки, но не
|
||||
экспортирует собственную разделяемую библиотеку. У скачанного вместе с ним
|
||||
пакета `libdw` присутствует длинный Provide
|
||||
`libdw.so.1()(64bit) = set:...`.
|
||||
|
||||
Если пакет уже установлен и APT не скачивает его повторно, его заголовок можно
|
||||
прочитать напрямую командой `rpmquery PACKAGE`; для гарантированного получения
|
||||
конкретного репозиторного RPM удобнее использовать RDB API.
|
||||
|
||||
### Способ 3: структурированные данные RDB API
|
||||
|
||||
RDB удобен для автоматизации, другой ветки/архитектуры и больших выборок. Он
|
||||
возвращает полные строки и явно различает `provide`, `require` и `conflict`:
|
||||
|
||||
```sh
|
||||
branch=p11
|
||||
arch=x86_64
|
||||
pkg=libdw
|
||||
|
||||
hash=$(curl -fsS \
|
||||
"https://rdb.altlinux.org/api/site/pkghash_by_binary_name?branch=$branch&name=$pkg&arch=$arch" |
|
||||
jq -r '.pkghash')
|
||||
|
||||
curl -fsS \
|
||||
"https://rdb.altlinux.org/api/dependencies/binary_package_dependencies/$hash" |
|
||||
jq -r '
|
||||
.dependencies[]
|
||||
| select(
|
||||
(.type == "provide" or .type == "require") and
|
||||
(.version | startswith("set:"))
|
||||
)
|
||||
| [.type, .name, .version]
|
||||
| @tsv
|
||||
'
|
||||
```
|
||||
|
||||
Проверенный результат для `libdw` содержит один set-Provide и четыре
|
||||
set-Requires. Поле `flag_decoded` того же JSON позволяет восстановить оператор:
|
||||
`RPMSENSE_EQUAL` для обычного Provide и
|
||||
`RPMSENSE_GREATER` + `RPMSENSE_EQUAL` для `>=` Require.
|
||||
|
||||
Через RDB можно также получить URL самого RPM:
|
||||
|
||||
```sh
|
||||
curl -fsS \
|
||||
"https://rdb.altlinux.org/api/site/package_downloads_bin/$hash?branch=$branch&arch=$arch" |
|
||||
jq -r '.downloads[].packages[].url'
|
||||
```
|
||||
|
||||
## Основные запросы
|
||||
|
||||
### Пакет и его метаданные
|
||||
|
||||
```sh
|
||||
# Версия установленного пакета в стандартном формате NVRA.
|
||||
rpmquery rpm
|
||||
|
||||
# Подробная информация. В ALT вывод включает DistTag.
|
||||
rpmquery -i rpm
|
||||
rpmquery --info rpm
|
||||
|
||||
# Стабильный формат для скрипта.
|
||||
rpmquery --qf '%{NAME}|%{EPOCHNUM}|%{VERSION}|%{RELEASE}|%{DISTTAG}|%{ARCH}\n' rpm
|
||||
|
||||
# Доступные имена тегов заголовка.
|
||||
rpmquery --querytags
|
||||
```
|
||||
|
||||
В `rpmquery` короткая опция `-i` означает `--info`, а не установку пакета. Это
|
||||
ALT-алиас из `rpmpopt`; он появился в changelog RPM в версии
|
||||
`4.13.0-alt7`. Для установки используется другой режим программы `rpm`, но в
|
||||
обычной работе с ALT зависимости и репозитории следует поручать APT.
|
||||
|
||||
### Файлы
|
||||
|
||||
```sh
|
||||
# Какому установленному пакету принадлежит файл.
|
||||
rpmquery -f /usr/bin/rpmquery
|
||||
|
||||
# Все файлы пакета.
|
||||
rpmquery -l rpm
|
||||
|
||||
# Только конфигурационные или документационные файлы.
|
||||
rpmquery -c rpm
|
||||
rpmquery -d rpm
|
||||
|
||||
# Пакет из файла без установки.
|
||||
rpmquery -p ./package.rpm
|
||||
rpmquery -lp ./package.rpm
|
||||
rpmquery -ip ./package.rpm
|
||||
```
|
||||
|
||||
Для аргумента-пути `--whatprovides` сначала обращается к индексу установленных
|
||||
файлов, затем к индексу capability. Поэтому для существующего установленного
|
||||
файла эти команды обычно совпадают:
|
||||
|
||||
```sh
|
||||
rpmquery -f /usr/bin/rpmquery
|
||||
rpmquery --whatprovides /usr/bin/rpmquery
|
||||
```
|
||||
|
||||
### Provides, Requires и обратные запросы
|
||||
|
||||
```sh
|
||||
# Что объявляет один пакет.
|
||||
rpmquery --provides rpm
|
||||
rpmquery --requires rpm
|
||||
rpmquery -R rpm
|
||||
|
||||
# Какие установленные пакеты объявляют capability или требуют его.
|
||||
rpmquery --whatprovides 'libpopt.so.0()(64bit)'
|
||||
rpmquery --whatrequires 'libpopt.so.0()(64bit)'
|
||||
```
|
||||
|
||||
`--provides`, `--requires`, `--info`, `--last`, `--scripts` и ряд других
|
||||
удобных режимов реализованы как `popt`-алиасы над `--queryformat`, а не как
|
||||
отдельные алгоритмы запросов.
|
||||
|
||||
## Формат запросов для автоматической обработки
|
||||
|
||||
Стандартный вывод удобен человеку, но надёжнее не разбирать его пробелами.
|
||||
Массивы тегов RPM обходятся форматом в квадратных скобках. Чтобы внутри такого
|
||||
итератора повторять скалярный тег для каждого элемента массива, знак `=` ставят
|
||||
внутри имени тега: `%{=NAME}`.
|
||||
|
||||
### Все set-Requires в TSV
|
||||
|
||||
```sh
|
||||
LC_ALL=C rpmquery -a --qf \
|
||||
'[%{=NAME}\t%{REQUIRENAME}\t%{REQUIREFLAGS:depflags}\t%{REQUIREVERSION}\n]' |
|
||||
awk -F '\t' '$4 ~ /^set:/'
|
||||
```
|
||||
|
||||
Поля: пакет, имя capability, оператор, версия зависимости.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
rpm libpopt.so.0()(64bit) >= set:jgtcU6BLBccTnteGxrE0
|
||||
```
|
||||
|
||||
### Все set-Provides в TSV
|
||||
|
||||
```sh
|
||||
LC_ALL=C rpmquery -a --qf \
|
||||
'[%{=NAME}\t%{PROVIDENAME}\t%{PROVIDEFLAGS:depflags}\t%{PROVIDEVERSION}\n]' |
|
||||
awk -F '\t' '$4 ~ /^set:/'
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
libpopt libpopt.so.0()(64bit) = set:jdtcJcAdqxTmPJUyuYcVIbLNhqmj...
|
||||
```
|
||||
|
||||
Это предпочтительнее `grep` по человекочитаемому выводу, если нужны имена
|
||||
пакетов, операторы или однозначное разделение полей.
|
||||
|
||||
## Что `rpmquery` делает и не делает с `set:version`
|
||||
|
||||
### Показывает сохранённые зависимости
|
||||
|
||||
`rpmquery --requires` и `rpmquery --provides` извлекают из заголовков пакетов
|
||||
имя capability, флаги отношения и строку версии. Они не декодируют `set:` и не
|
||||
показывают исходные ELF-символы.
|
||||
|
||||
Для установленного `rpm` в p11 наблюдалось:
|
||||
|
||||
```text
|
||||
libpopt.so.0()(64bit) >= set:jgtcU6BLBccTnteGxrE0
|
||||
```
|
||||
|
||||
Установленный провайдер находился по одному имени capability:
|
||||
|
||||
```sh
|
||||
rpmquery --whatprovides 'libpopt.so.0()(64bit)'
|
||||
# libpopt-1.18-alt1.x86_64
|
||||
```
|
||||
|
||||
### Не сравнивает выражение, переданное в `--whatprovides`
|
||||
|
||||
Передача полного выражения не запускает `rpmsetcmp()`:
|
||||
|
||||
```sh
|
||||
rpmquery --whatprovides \
|
||||
'libpopt.so.0()(64bit) >= set:jgtcU6BLBccTnteGxrE0'
|
||||
```
|
||||
|
||||
Проверенный результат:
|
||||
|
||||
```text
|
||||
no package provides libpopt.so.0()(64bit) >= set:jgtcU6BLBccTnteGxrE0
|
||||
```
|
||||
|
||||
Для непутевого аргумента `--whatprovides` делает точный поиск по индексу
|
||||
`PROVIDENAME`. Поэтому сначала нужно искать провайдера по имени capability, а
|
||||
его set-строку получать отдельным запросом:
|
||||
|
||||
```sh
|
||||
name='libpopt.so.0()(64bit)'
|
||||
provider=$(rpmquery --whatprovides "$name" | head -n1)
|
||||
rpmquery --qf \
|
||||
'[%{PROVIDENAME}\t%{PROVIDEFLAGS:depflags}\t%{PROVIDEVERSION}\n]' \
|
||||
"$provider" |
|
||||
grep -F "$name"
|
||||
```
|
||||
|
||||
Настоящее сравнение двух `set:`-версий происходит в `rpmdsCompareEVR()` при
|
||||
проверке совместимости зависимостей. Если обе версии начинаются с `set:`, код
|
||||
вызывает `rpmsetcmp()`; если `set:` имеет только одна сторона, зависимости не
|
||||
пересекаются. Следовательно, `rpmquery` полезен для извлечения корпуса строк,
|
||||
но не является CLI для непосредственного сравнения двух наборов.
|
||||
|
||||
## Как `rpmquery` используется при сборке самого ALT RPM
|
||||
|
||||
В `alt/rpm.spec` ветки p11 после первой сборки выполняется:
|
||||
|
||||
```sh
|
||||
rpmquery -a --provides | fgrep '= set:' | sort >P
|
||||
rpmquery -a --requires | fgrep '= set:' | sort >R
|
||||
join -o 1.3,2.3 P R | shuf >setcmp-data
|
||||
time ./setcmp <setcmp-data >/dev/null
|
||||
```
|
||||
|
||||
Смысл конвейера:
|
||||
|
||||
1. Из локальной базы сборочного окружения собираются все set-Provides и
|
||||
set-Requires.
|
||||
2. Строка поиска `= set:` захватывает как Provides с `= set:`, так и Requires с
|
||||
`>= set:`, потому что вторая строка тоже содержит подстроку `= set:`.
|
||||
3. После сортировки `join` соединяет файлы по первому полю — имени capability.
|
||||
4. `-o 1.3,2.3` оставляет только третьи поля: set провайдера и set требования.
|
||||
5. Полученные реальные пары подаются в `tools/setcmp` для замера реализации
|
||||
`rpmsetcmp()` и профиль-управляемой пересборки `lib/set.c`.
|
||||
|
||||
В обновлённом минимальном контейнере p11 точный конвейер дал:
|
||||
|
||||
```text
|
||||
P: 75 строк
|
||||
R: 109 строк, из них 13 полных дублей
|
||||
setcmp-data: 109 пар
|
||||
```
|
||||
|
||||
Эти числа описывают только состав минимального контейнера, а не весь p11 или
|
||||
Sisyphus. Также конвейер spec не сохраняет имя capability и пакет-владелец;
|
||||
для исследовательского корпуса удобнее TSV-форматы выше.
|
||||
|
||||
Современная замена устаревающего `fgrep` без изменения смысла:
|
||||
|
||||
```sh
|
||||
grep -F '= set:'
|
||||
```
|
||||
|
||||
## Практические ограничения
|
||||
|
||||
- `rpmquery -a` означает все **установленные** пакеты, а не весь репозиторий.
|
||||
- `--whatprovides` и `--whatrequires` ищут заголовки по имени capability; они
|
||||
не принимают полноценное выражение зависимости как запрос сравнения.
|
||||
- Несколько установленных версий или провайдеров могут дать несколько строк.
|
||||
- Set-строки крупных библиотек очень длинные; лучше писать полный вывод в файл,
|
||||
а в терминале показывать только статистику или начало строки.
|
||||
- Для скриптов следует фиксировать `LC_ALL=C` и использовать `--queryformat`.
|
||||
- `--requires`/`--provides` отражают метаданные RPM, а не текущее содержимое ELF
|
||||
на диске. Для восстановления исходных символов нужны `nm`, `readelf` и
|
||||
генераторы автозависимостей rpm-build.
|
||||
- При запросе другой системы безопаснее явно использовать `--root`; `--dbpath`
|
||||
меняет только путь к БД и требует внимательности к относительным путям.
|
||||
- ALT предоставляет `--nowait-lock` как popt-алиас, отключающий ожидание
|
||||
блокировки БД. Его стоит применять только когда отказ предпочтительнее
|
||||
ожидания, а не как способ обходить активную транзакцию RPM/APT.
|
||||
|
||||
## Источники
|
||||
|
||||
1. [Документация платформы ALT 10.1: утилита RPM](https://docs.altlinux.org/ru-RU/platform/10.1/html/alt-platform/utilita_komandnoj_stroki_rpm.html) — RPM работает с файлами, пакетами, зависимостями и собственной БД, но не знает о репозиториях.
|
||||
2. [Настройка списка репозиториев APT в ALT](https://docs.altlinux.org/ru-RU/platform/10.1/html/alt-platform/nastrojka_spiska_repozitoriev_apt.html).
|
||||
3. [Пакет `rpm` в p11](https://packages.altlinux.org/en/p11/srpms/rpm/) и [specfile](https://packages.altlinux.org/en/p11/srpms/rpm/specfiles/).
|
||||
4. [RDB: текущий бинарный пакет `rpm` для p11/x86_64](https://rdb.altlinux.org/api/site/pkghash_by_binary_name?branch=p11&name=rpm&arch=x86_64).
|
||||
5. [`rpmqv.c`](https://git.altlinux.org/gears/r/rpm.git?p=rpm.git;a=blob;f=rpmqv.c;hb=7e71ed3da438289d916f1cccc9c0a83e594455dc) — выбор режима Query по имени `rpmquery`/`rpmq`.
|
||||
6. [`rpmpopt.in`](https://git.altlinux.org/gears/r/rpm.git?p=rpm.git;a=blob;f=rpmpopt.in;hb=7e71ed3da438289d916f1cccc9c0a83e594455dc) — ALT-алиасы `--requires`, `--provides`, `--info`, `-i`, `--nowait-lock`.
|
||||
7. [`lib/query.c`](https://git.altlinux.org/gears/r/rpm.git?p=rpm.git;a=blob;f=lib/query.c;hb=7e71ed3da438289d916f1cccc9c0a83e594455dc) — индексы для `--whatprovides`, запросы файлов и формат `%{nvra}` по умолчанию.
|
||||
8. [`lib/rpmds.c`](https://git.altlinux.org/gears/r/rpm.git?p=rpm.git;a=blob;f=lib/rpmds.c;hb=7e71ed3da438289d916f1cccc9c0a83e594455dc) — вызов `rpmsetcmp()` при сравнении двух set-версий.
|
||||
9. [`alt/rpm.spec`](https://git.altlinux.org/gears/r/rpm.git?p=rpm.git;a=blob;f=alt/rpm.spec;hb=7e71ed3da438289d916f1cccc9c0a83e594455dc) — построение `setcmp-data` через `rpmquery`.
|
||||
10. [Официальный контейнер ALT](https://hub.docker.com/_/alt) — среда воспроизведения `alt:p11`.
|
||||
@@ -1,80 +0,0 @@
|
||||
rg rpmsetcmp:
|
||||
|
||||
```
|
||||
tools/setcmp.c
|
||||
12-{
|
||||
13: int cmp = rpmsetcmp(s1, s2);
|
||||
14- switch (cmp) {
|
||||
|
||||
alt/rpm.spec
|
||||
709-_ Mon Nov 25 2019 Andrew Savchenko <bircoph@altlinux.org> 4.13.0.1-alt15
|
||||
710:- Support rpmsetcmp profiling on E2K.
|
||||
711-
|
||||
--
|
||||
713-- Added triggers circumvention for packagekit offline update (by Aleksei Nikiforov).
|
||||
714:- Imported rpmsetcmp optimization from rpm-build.
|
||||
715-
|
||||
--
|
||||
1094-- set.c: Increased cache size from 160 to 256 slots, 75 percent hit ratio.
|
||||
1095:- set.c: Implemented 4-byte and 8-byte steppers for rpmsetcmp main loop.
|
||||
1096-
|
||||
--
|
||||
1314-_ Mon Sep 20 2010 Alexey Tourbin <at@altlinux.ru> 4.0.4-alt98.47
|
||||
1315:- set.c (rpmsetcmp): Fixed check for set2 decoding error.
|
||||
1316-- brp-cleanup: Updated for /usr/lib64/perl5 and /usr/share/perl5.
|
||||
|
||||
lib/rpmds.c
|
||||
1087- if (aset && bset) {
|
||||
1088: sense = rpmsetcmp(AEVR, BEVR);
|
||||
1089- if (sense < -1) {
|
||||
|
||||
---
|
||||
|
||||
rg set_new:
|
||||
none
|
||||
|
||||
---
|
||||
|
||||
rg set_add
|
||||
none
|
||||
|
||||
---
|
||||
|
||||
rg set_fini:
|
||||
none
|
||||
|
||||
---
|
||||
|
||||
rg set_free:
|
||||
none
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
rg setcmp:
|
||||
|
||||
```
|
||||
Makefile.am
|
||||
218-
|
||||
219:noinst_PROGRAMS = setcmp
|
||||
220:setcmp_SOURCES = tools/setcmp.c
|
||||
221:setcmp_LDADD = lib/librpm.la
|
||||
222-
|
||||
|
||||
alt/rpm.spec
|
||||
311-rpmquery -a --requires |fgrep '= set:' |sort >R
|
||||
312:join -o 1.3,2.3 P R |shuf >setcmp-data
|
||||
313:time ./setcmp <setcmp-data >/dev/null
|
||||
314-rm lib/set.lo lib/librpm.la
|
||||
--
|
||||
318-%if_with profile
|
||||
319:time ./setcmp <setcmp-data >/dev/null
|
||||
320-rm lib/set.lo lib/librpm.la
|
||||
321-%make_build -C lib set.lo librpm.la CFLAGS="$set_c_cflags -fprofile-generate"
|
||||
322:./setcmp <setcmp-data >/dev/null
|
||||
323-%ifnarch %e2k
|
||||
--
|
||||
332-%make_build
|
||||
333:time ./setcmp <setcmp-data >/dev/null
|
||||
334-
|
||||
```
|
||||
@@ -1,99 +0,0 @@
|
||||
rg rpmsetcmp:
|
||||
|
||||
```
|
||||
tools/setcmp.c
|
||||
12-{
|
||||
13: int cmp = rpmsetcmp(s1, s2);
|
||||
14- switch (cmp) {
|
||||
|
||||
lib/depends.c
|
||||
124- if (aset && bset) {
|
||||
125: sense = rpmsetcmp(AEVR, BEVR);
|
||||
126- if (sense < -1) {
|
||||
|
||||
rpm-4_0.spec
|
||||
1231-- set.c: Increased cache size from 160 to 256 slots, 75 percent hit ratio.
|
||||
1232:- set.c: Implemented 4-byte and 8-byte steppers for rpmsetcmp main loop.
|
||||
1233-
|
||||
--
|
||||
1451-* Mon Sep 20 2010 Alexey Tourbin <at@altlinux.ru> 4.0.4-alt98.47
|
||||
1452:- set.c (rpmsetcmp): Fixed check for set2 decoding error.
|
||||
1453-- brp-cleanup: Updated for /usr/lib64/perl5 and /usr/share/perl5.
|
||||
|
||||
build/reqprov.c
|
||||
175- if (aset && bset) {
|
||||
176: sense = rpmsetcmp(Aevr, Bevr);
|
||||
177- if (sense < -1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
rg set_new:
|
||||
|
||||
```
|
||||
tools/mkset.c
|
||||
11- assert(bpp <= 32);
|
||||
12: struct set *set = set_new();
|
||||
13- char *line = NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
rg set_add:
|
||||
|
||||
```
|
||||
tools/mkset.c
|
||||
21- continue;
|
||||
22: set_add(set, line);
|
||||
23- added++;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
rg set_fini:
|
||||
|
||||
```
|
||||
tools/mkset.c
|
||||
25- assert(added > 0);
|
||||
26: const char *str = set_fini(set, bpp);
|
||||
27- assert(str);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
rg set_free:
|
||||
none
|
||||
|
||||
---
|
||||
|
||||
rg mkset:
|
||||
|
||||
```
|
||||
rpm-4_0.spec
|
||||
431-# set-version helpers
|
||||
432:%rpmattr %_rpmlibdir/mkset
|
||||
433-%rpmattr %_rpmlibdir/setcmp
|
||||
--
|
||||
1458-- rpmlibprov.c: Added rpmlib(SetVersions) feature.
|
||||
1459:- %_rpmlibdir/mkset: Command-line helper for making set-versions.
|
||||
1460-- lib.prov: Implemented soname set-versioning with exported symbols.
|
||||
|
||||
tools/Makefile.am
|
||||
37- relative \
|
||||
38: mkset \
|
||||
39- setcmp \
|
||||
--
|
||||
62-
|
||||
63:mkset_SOURCES = mkset.c
|
||||
64-setcmp_SOURCES = setcmp.c
|
||||
|
||||
autodeps/lib.prov.in
|
||||
112- Info "$f: $n symbols, $bpp bpp"
|
||||
113: set=$(printf '%s\n' "$sym" |"${RPMB_TOOLS_DIR-@RPMCONFIGDIR@}/mkset" "$bpp")
|
||||
114- printf '%s = %s\n' "$provname" "$set"
|
||||
|
||||
autodeps/lib.req.in
|
||||
306- #printf '%s\n' "$reqsym" |LC_ALL=C sort -c -u
|
||||
307: set=$(printf '%s\n' "$reqsym" |"${RPMB_TOOLS_DIR-@RPMCONFIGDIR@}/mkset" "$bpp")
|
||||
308- printf '%s >= %s\n' "$dep" "$set"
|
||||
```
|
||||
+586
@@ -0,0 +1,586 @@
|
||||
# Реализация set:version в `set9.c`
|
||||
|
||||
Документ описывает экспериментальную реализацию [`reimplement/set9.c`](../reimplement/set9.c).
|
||||
Общий смысл механизма set:version и устройство исходного [`set.c`](../set.c) разобраны в
|
||||
[`set:version_ru.md`](set:version_ru.md). Здесь основное внимание уделено отличиям `set9.c`:
|
||||
потоковому кодированию и декодированию, сортировке, двум LRU-кэшам и
|
||||
новому алгоритму сравнения отсортированных множеств.
|
||||
|
||||
## Назначение
|
||||
|
||||
ALT RPM записывает наборы ELF-символов в версии зависимостей вида:
|
||||
|
||||
```text
|
||||
libfoo.so.X()(64bit) = set:<encoded-set>
|
||||
libfoo.so.X()(64bit) >= set:<encoded-set>
|
||||
```
|
||||
|
||||
`Provides` содержит хэши предоставляемых символов, а `Requires` — хэши символов, требуемых
|
||||
от конкретной библиотеки. `rpmsetcmp()` сравнивает декодированные множества по включению,
|
||||
а не как обычные RPM-версии.
|
||||
|
||||
`set9.c` сохраняет формат строк и Jenkins OAAT из исходной реализации. Поэтому его задача —
|
||||
изменить внутреннее представление и горячие пути, не меняя смысл корректных set-строк.
|
||||
Совпадение формата не устраняет фундаментальную вероятность коллизий: сравниваются усечённые
|
||||
32-битные хэши, а не исходные имена символов.
|
||||
|
||||
## Основные отличия от исходного `set.c`
|
||||
|
||||
| Область | Исходный `set.c` | `set9.c` |
|
||||
| ------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| Хранение имён | отдельный `xstrdup()` для каждого имени | общая растущая строковая арена и массив смещений |
|
||||
| Сортировка | всегда `qsort()` | `qsort()` для `< 128` элементов, LSD radix sort для остальных |
|
||||
| Кодирование | отдельные массивы delta, битов и Base62 | один поток `hash -> delta -> Golomb-Rice -> Base62` |
|
||||
| Декодирование | сложный табличный декодер пар символов | простой потоковый декодер с 64-битным аккумулятором |
|
||||
| Кэш | один кэш первого операнда на 256 записей | два независимых кэша по 512 записей, по одному на каждый операнд |
|
||||
| Поиск в кэше | линейный просмотр и `memmove()` | хэш-бакеты и двусвязный LRU со сменой позиции за `O(1)` |
|
||||
| Нормализация `bpp` | выполняется для каждого сравнения | выполняется при промахе кэша и сохраняется в записи кэша |
|
||||
| Сравнение | флаги `ge`/`le`, sentinel-элементы и макросы прыжков | проверка мощности, `memcmp()` и `sorted_subset()` |
|
||||
| Освобождение `struct set` | сама структура не освобождается | освобождаются арена, массив символов и сама структура |
|
||||
|
||||
## Публичный API
|
||||
|
||||
Как и исходная реализация, файл предоставляет пять функций:
|
||||
|
||||
```c
|
||||
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);
|
||||
```
|
||||
|
||||
### `set_new()`
|
||||
|
||||
Создаёт пустой `struct set`. Все счётчики и ёмкости устанавливаются в ноль, указатели — в
|
||||
`NULL`.
|
||||
|
||||
### `set_add()`
|
||||
|
||||
Добавляет копию строки символа во внутреннюю строковую арену:
|
||||
|
||||
1. массив `symbols_v` увеличивается блоками по 1024 элемента;
|
||||
2. строковая арена при первом выделении получает 4096 байт;
|
||||
3. при нехватке места ёмкость арены удваивается, пока не вместит новую строку;
|
||||
4. в `symbols_v` сохраняются смещение строки в арене и нулевое начальное значение хэша.
|
||||
|
||||
В отличие от хранения отдельных указателей, перемещение арены через `xrealloc()` не делает
|
||||
записи массива недействительными: в них находятся смещения, а не адреса строк.
|
||||
|
||||
### `set_fini()`
|
||||
|
||||
Хэширует, сортирует и кодирует добавленные имена. Возвращаемая строка выделена через
|
||||
`xstrdup()` и **не содержит** префикс `set:`. Префикс добавляет вызывающий код, например
|
||||
`mkset`.
|
||||
|
||||
В `set9.c` контракт проверяется через `assert()`:
|
||||
|
||||
```text
|
||||
set != NULL
|
||||
set->cnt > 0
|
||||
10 <= bpp <= 32
|
||||
```
|
||||
|
||||
Это отличается от исходного `set.c`, где пустое множество или недопустимый `bpp` приводят к
|
||||
`NULL`. При сборке с `NDEBUG` проверки `assert()` исчезают, поэтому передавать некорректные
|
||||
аргументы нельзя.
|
||||
|
||||
### `set_free()`
|
||||
|
||||
Освобождает строковую арену, массив `symbols_v` и сам `struct set`, затем возвращает `NULL`.
|
||||
Типичный вызов:
|
||||
|
||||
```c
|
||||
set = set_free(set);
|
||||
```
|
||||
|
||||
### `rpmsetcmp()`
|
||||
|
||||
Сравнивает две set-строки. Префикс `set:` у каждого операнда необязателен.
|
||||
|
||||
Результаты совместимы с исходным API:
|
||||
|
||||
| Код | Значение |
|
||||
| ---: | ---------------------------------------------------- |
|
||||
| `1` | первое множество строго содержит второе |
|
||||
| `0` | множества равны |
|
||||
| `-1` | первое множество строго содержится во втором |
|
||||
| `-2` | множества несравнимы по включению |
|
||||
| `-3` | ошибка метаданных или декодирования первого операнда |
|
||||
| `-4` | ошибка метаданных или декодирования второго операнда |
|
||||
|
||||
В типичном RPM-вызове первым операндом остаётся `Provides`, вторым — `Requires`.
|
||||
|
||||
## Формат set-строки
|
||||
|
||||
После необязательного `set:` строка имеет структуру:
|
||||
|
||||
```text
|
||||
<bpp><Mshift><payload>
|
||||
```
|
||||
|
||||
Первые два символа кодируют числа формулой:
|
||||
|
||||
```c
|
||||
value = character + 7 - 'a';
|
||||
character = value - 7 + 'a';
|
||||
```
|
||||
|
||||
Ограничения:
|
||||
|
||||
```text
|
||||
10 <= bpp <= 32
|
||||
7 <= Mshift <= 31
|
||||
Mshift < bpp
|
||||
payload не пуст
|
||||
```
|
||||
|
||||
`payload` использует алфавит:
|
||||
|
||||
```text
|
||||
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
```
|
||||
|
||||
Его числовые значения расположены как `0..9`, `a..z`, `A..Z`. Символ `Z` имеет значение
|
||||
61 и одновременно служит escape-маркером для исходных шестибитных значений 61, 62 и 63.
|
||||
|
||||
## Внутреннее представление создаваемого множества
|
||||
|
||||
```c
|
||||
struct set {
|
||||
size_t cnt;
|
||||
size_t symbols_cap;
|
||||
size_t strings_len;
|
||||
size_t strings_cap;
|
||||
char *strings;
|
||||
struct symbols {
|
||||
size_t offset;
|
||||
unsigned hash;
|
||||
} *symbols_v;
|
||||
};
|
||||
```
|
||||
|
||||
Здесь:
|
||||
|
||||
- `cnt` — число добавленных имён;
|
||||
- `symbols_cap` — ёмкость массива метаданных;
|
||||
- `strings_len` и `strings_cap` — занятая и выделенная части строковой арены;
|
||||
- `strings` — последовательность NUL-терминированных имён;
|
||||
- `offset` — начало конкретного имени относительно `strings`;
|
||||
- `hash` — хэш, вычисляемый только при `set_fini()`.
|
||||
|
||||
Такое представление сокращает число отдельных выделений памяти при построении больших наборов.
|
||||
|
||||
## Работа `set_fini()`
|
||||
|
||||
Полный путь выглядит так:
|
||||
|
||||
```text
|
||||
строковая арена и offsets
|
||||
| Jenkins OAAT + mask(bpp)
|
||||
v
|
||||
массив struct symbols с хэшами
|
||||
| qsort или LSD radix sort
|
||||
v
|
||||
отсортированные хэши
|
||||
| предупреждения о коллизиях + удаление повторов
|
||||
v
|
||||
массив уникальных хэшей
|
||||
| потоковый delta + Golomb-Rice + Base62
|
||||
v
|
||||
строка <bpp><Mshift><payload>
|
||||
```
|
||||
|
||||
### Jenkins OAAT и усечение
|
||||
|
||||
Для каждого имени вычисляется тот же Jenkins one-at-a-time hash, что и в исходном файле.
|
||||
Начальное состояние равно `0x9e3779b9`. Результат ограничивается младшими `bpp` битами:
|
||||
|
||||
```c
|
||||
unsigned mask = (bpp < 32) ? (1u << bpp) - 1 : ~0u;
|
||||
hash_value = hash(symbol) & mask;
|
||||
```
|
||||
|
||||
### `sort_symbols()`
|
||||
|
||||
Для малых наборов (`count < 128`) используется `qsort()`. Для больших наборов применяется
|
||||
стабильная LSD radix sort по байтам хэша:
|
||||
|
||||
1. число проходов равно `ceil(bpp / 8)`;
|
||||
2. на каждом проходе строятся 256 счётчиков/смещений;
|
||||
3. элементы стабильно распределяются по текущему байту;
|
||||
4. источник и приёмник меняются местами;
|
||||
5. если итог оказался во временном массиве, он копируется назад.
|
||||
|
||||
Поскольку перед сортировкой хэш уже ограничен `bpp` битами, обработки
|
||||
`ceil(bpp / 8)` байтов достаточно. Временный массив размером `count` размещается на стеке.
|
||||
|
||||
### Коллизии и повторяющиеся символы
|
||||
|
||||
После сортировки соседние равные хэши проверяются попарно. Для разных строк печатается:
|
||||
|
||||
```text
|
||||
warning: hash collision: <left> <right>
|
||||
```
|
||||
|
||||
Две одинаковые строки предупреждения не создают. Затем все одинаковые **хэш-значения**
|
||||
схлопываются в одно. Поэтому настоящая хэш-коллизия после предупреждения всё равно становится
|
||||
одним элементом кодируемого множества — это свойство исходного формата.
|
||||
|
||||
### Выбор `Mshift`
|
||||
|
||||
Параметр Golomb-Rice вычисляется как:
|
||||
|
||||
```text
|
||||
Mshift = bpp - floor(log2(count)) - 1
|
||||
```
|
||||
|
||||
После этого значение ограничивается диапазоном `7..31` и проверяется условие
|
||||
`Mshift < bpp`. Идея та же, что в исходной реализации: при примерно равномерном распределении
|
||||
хэшей средняя дельта близка к `2^bpp / count`.
|
||||
|
||||
### Потоковый `encode_set()`
|
||||
|
||||
Исходный `set.c` сначала создаёт массив дельт, затем массив по одному байту на бит и лишь потом
|
||||
Base62-строку. В `set9.c` эти стадии объединены.
|
||||
|
||||
Для каждого отсортированного хэша вычисляется:
|
||||
|
||||
```c
|
||||
unsigned delta = current - previous;
|
||||
unsigned q = delta >> Mshift;
|
||||
unsigned r = delta & ((1u << Mshift) - 1);
|
||||
```
|
||||
|
||||
В `encode_writer` последовательно добавляются:
|
||||
|
||||
```text
|
||||
q нулевых битов
|
||||
1 — разделитель
|
||||
Mshift младших битов r
|
||||
```
|
||||
|
||||
Все биты идут младшими вперёд (LSB-first), как и в исходном формате.
|
||||
|
||||
### `struct encode_writer`
|
||||
|
||||
"Писатель" содержит:
|
||||
|
||||
- `uint64_t bits` — накопленные ещё не выведенные биты;
|
||||
- `filled` — число занятых битов;
|
||||
- `escaped` — ожидание второй половины escape-пары;
|
||||
- `pending_high` — старшие два бита значения 61, 62 или 63;
|
||||
- `output` — текущую позицию в выходной строке.
|
||||
|
||||
`encode_writer_put()` добавляет обычное поле битов. `encode_writer_zeros()` добавляет длинную
|
||||
unary-последовательность нулей блоками не более 56 бит, чтобы аккумулятор оставался в пределах
|
||||
`uint64_t`. `encode_writer_flush()` выводит накопленные Base62-цифры.
|
||||
|
||||
В обычном состоянии потребляется шесть потоковых битов. Если значение равно 61, 62 или 63:
|
||||
|
||||
1. выводится `Z`;
|
||||
2. различие между 61/62/63 сохраняется как два старших бита следующего символа;
|
||||
3. из потока для следующего символа берутся только четыре бита.
|
||||
|
||||
При завершении неполный символ дополняется нулями. После payload записывается `\0`.
|
||||
|
||||
## Метаданные декодируемой строки
|
||||
|
||||
Для декодирования используется `struct set_meta`:
|
||||
|
||||
```c
|
||||
struct set_meta {
|
||||
const char *str;
|
||||
const char *payload;
|
||||
size_t len;
|
||||
size_t payload_len;
|
||||
int bpp;
|
||||
int Mshift;
|
||||
int bit_capacity;
|
||||
int value_capacity;
|
||||
};
|
||||
```
|
||||
|
||||
Подготовка разделена на две части.
|
||||
|
||||
### `set_meta_init()` — быстрая проверка
|
||||
|
||||
Функция читает только начало строки:
|
||||
|
||||
1. проверяет наличие двух метасимволов и хотя бы одного символа payload;
|
||||
2. декодирует и проверяет `bpp`;
|
||||
3. декодирует и проверяет `Mshift`;
|
||||
4. сохраняет `str`, `payload`, `bpp` и `Mshift`.
|
||||
|
||||
На этой стадии `strlen()` не вызывается. Это позволяет сначала выполнить дешёвую проверку и
|
||||
поискать уже готовую запись в кэше.
|
||||
|
||||
### `set_meta_fini()` — вычисление размеров
|
||||
|
||||
Функция вызывается только при промахе кэша:
|
||||
|
||||
```text
|
||||
len = strlen(str)
|
||||
payload_len = len - 2
|
||||
bit_capacity = payload_len * 6
|
||||
value_capacity = bit_capacity / (Mshift + 1)
|
||||
```
|
||||
|
||||
`bit_capacity` является верхней границей: обычный символ даёт шесть битов, а два символа
|
||||
escape-пары дают десять битов, то есть не больше двенадцати. Каждое Golomb-Rice-значение
|
||||
требует как минимум `Mshift + 1` бит. Если места нет даже для одного значения,
|
||||
строка отклоняется.
|
||||
|
||||
## Потоковое декодирование
|
||||
|
||||
Путь декодирования объединяет три логических стадии:
|
||||
|
||||
```text
|
||||
Base62 payload
|
||||
| decode_chunk()
|
||||
v
|
||||
6- или 10-битные блоки
|
||||
| q-state + r-state
|
||||
v
|
||||
Golomb-Rice delta
|
||||
| previous += delta
|
||||
v
|
||||
отсортированные хэши
|
||||
```
|
||||
|
||||
Отдельные массивы битов и дельт не создаются.
|
||||
|
||||
### Таблица `char_to_num[]`
|
||||
|
||||
Таблица из 256 элементов классифицирует любой байт:
|
||||
|
||||
- `0..60` — обычная Base62-цифра;
|
||||
- `61` — `Z`;
|
||||
- `0xff` — конец строки;
|
||||
- `0xee` — недопустимый символ.
|
||||
|
||||
Индекс берётся как `unsigned char`, поэтому байты с установленным старшим битом тоже безопасно
|
||||
попадают в диапазон таблицы и отклоняются как недопустимые.
|
||||
|
||||
### `decode_chunk()`
|
||||
|
||||
Обычный символ со значением меньше 61 возвращает шесть битов. При `Z` функция читает следующий
|
||||
символ и:
|
||||
|
||||
1. отклоняет конец строки сразу после `Z`;
|
||||
2. отклоняет не-Base62 символ;
|
||||
3. отклоняет комбинацию старших битов `11`, которая создала бы новый `Z`;
|
||||
4. собирает один 10-битный блок из значения 61/62/63 и четырёх младших битов второго символа.
|
||||
|
||||
Таким образом escape-пара сразу преобразуется в исходные десять потоковых битов.
|
||||
|
||||
### `decode_set()`
|
||||
|
||||
Декодер хранит ещё не обработанные биты в `uint64_t bits`, а их количество — в `filled`.
|
||||
Для каждого значения он проходит два состояния.
|
||||
|
||||
#### Unary-часть `q`
|
||||
|
||||
Если аккумулятор пуст, загружается следующий блок. Полностью нулевой блок целиком прибавляется
|
||||
к `q`. Иначе `__builtin_ctzll(bits)` находит число нулей до разделительной единицы. Нули и сама
|
||||
единица удаляются из аккумулятора.
|
||||
|
||||
#### Остаток `r`
|
||||
|
||||
Пока доступно меньше `Mshift` битов, загружаются следующие блоки. Затем младшие `Mshift` битов
|
||||
образуют остаток, а дельта восстанавливается как:
|
||||
|
||||
```c
|
||||
unsigned delta = (q << Mshift) | r;
|
||||
```
|
||||
|
||||
Дельта сразу превращается в исходный хэш:
|
||||
|
||||
```c
|
||||
previous += delta;
|
||||
hash_arr[count++] = previous;
|
||||
```
|
||||
|
||||
#### Завершение строки
|
||||
|
||||
Конец payload допустим в состоянии чтения `q`, если осталось не более пяти нулевых битов
|
||||
Base62-дополнения. Более длинный нулевой хвост даёт внутреннюю ошибку `-10`.
|
||||
|
||||
Конец строки во время набора `Mshift` битов остатка означает незавершённое значение и даёт
|
||||
внутреннюю ошибку `-11`. Ошибки `decode_chunk()` также возвращаются вверх. Публичный
|
||||
`rpmsetcmp()` скрывает конкретный внутренний код и преобразует его в `-3` или `-4` в зависимости
|
||||
от операнда.
|
||||
|
||||
## Нормализация точности: `downsample_set()`
|
||||
|
||||
Строки с разным `bpp` нельзя сравнивать напрямую. Обе стороны приводятся к:
|
||||
|
||||
```text
|
||||
target_bpp = min(bpp1, bpp2)
|
||||
```
|
||||
|
||||
Один вызов `downsample_set()` уменьшает точность с `bpp + 1` до `bpp`:
|
||||
|
||||
1. маска равна `(1u << bpp) - 1`;
|
||||
2. бинарным поиском находится первый элемент с удаляемым старшим битом;
|
||||
3. исходный отсортированный массив делится на две отсортированные половины;
|
||||
4. у второй половины удаляется старший бит;
|
||||
5. половины сливаются как два отсортированных массива;
|
||||
6. появившиеся после усечения дубликаты удаляются.
|
||||
|
||||
Если требуется убрать несколько битов, операция повторяется по одному биту. В `set9.c` результат
|
||||
нормализации сохраняется в кэше, поэтому та же строка при том же `target_bpp` не проходит этот
|
||||
цикл повторно.
|
||||
|
||||
## Два кэша декодированных множеств
|
||||
|
||||
`cache_decode_set()` обслуживает два независимых кэша:
|
||||
|
||||
```text
|
||||
cache_id = 0 — первый операнд
|
||||
cache_id = 1 — второй операнд
|
||||
```
|
||||
|
||||
Каждый кэш содержит до `CACHE_SIZE = 512` записей и 1024 бакета. Разделение не даёт потоку
|
||||
часто повторяющихся `Requires` вытеснять кэш `Provides` и наоборот, хотя реальный эффект зависит
|
||||
от порядка вызовов решателя зависимостей.
|
||||
|
||||
### Ключ записи
|
||||
|
||||
Для быстрого предварительного отбора вычисляется fingerprint:
|
||||
|
||||
```c
|
||||
str[0] | (str[2] << 8) | (str[3] << 16)
|
||||
```
|
||||
|
||||
Он смешивается с `target_bpp`, после чего выбирается бакет. Совпадения fingerprint недостаточно:
|
||||
успешный hit требует также равного `target_bpp` и полного `strcmp()` строки. Поэтому коллизия
|
||||
fingerprint влияет только на длину цепочки, но не подменяет декодированное множество.
|
||||
|
||||
`target_bpp` входит в ключ, поскольку одна и та же исходная строка может участвовать в
|
||||
сравнениях с операндами разной точности и, следовательно, иметь разные нормализованные массивы.
|
||||
|
||||
### Устройство записи
|
||||
|
||||
Одним `xmalloc()` выделяются:
|
||||
|
||||
```text
|
||||
struct cache_ent
|
||||
массив unsigned для хэшей
|
||||
копия исходной строки
|
||||
```
|
||||
|
||||
Запись хранит fingerprint, исходную строку, число элементов и `target_bpp`.
|
||||
|
||||
### Промах
|
||||
|
||||
При промахе происходят следующие шаги:
|
||||
|
||||
1. `set_meta_fini()` вычисляет длины и верхнюю границу числа значений;
|
||||
2. выделяется новая запись;
|
||||
3. `decode_set()` создаёт отсортированный массив хэшей;
|
||||
4. при необходимости массив поэтапно уменьшается до `target_bpp`;
|
||||
5. строка и метаданные копируются в запись;
|
||||
6. запись добавляется в хэш-бакет и в начало логического LRU.
|
||||
|
||||
### LRU
|
||||
|
||||
Все записи одного кэша образуют двусвязный список от `oldest` к `newest`. При hit запись
|
||||
отцепляется от текущего места и становится `newest`; это не требует сдвига массива записей.
|
||||
При заполненном кэше удаляется `oldest`, включая поиск ссылки на него в соответствующей цепочке
|
||||
бакета.
|
||||
|
||||
## Сравнение в `rpmsetcmp()`
|
||||
|
||||
Последовательность работы:
|
||||
|
||||
1. снять `set:`, если он присутствует;
|
||||
2. выполнить `set_meta_init()` для обеих строк;
|
||||
3. выбрать минимальный `target_bpp`;
|
||||
4. получить первый операнд из кэша `0` или декодировать его;
|
||||
5. если строки полностью одинаковы, вернуть `0`;
|
||||
6. получить второй операнд из кэша `1` или декодировать его;
|
||||
7. сравнить мощности и содержимое нормализованных массивов.
|
||||
|
||||
Быстрый возврат для одинаковых строк выполняется после полноценного декодирования первого
|
||||
операнда. Поэтому равенство строк не позволяет принять некорректный payload без проверки.
|
||||
Метаданные второго операнда к этому моменту также уже прошли начальную проверку.
|
||||
|
||||
### Равная мощность
|
||||
|
||||
Нормализованные массивы отсортированы и не содержат дубликатов. Если `cnt1 == cnt2`, два
|
||||
множества могут быть либо равны, либо несравнимы. Поэтому достаточно:
|
||||
|
||||
```c
|
||||
memcmp(hash_arr1, hash_arr2, cnt1 * sizeof(unsigned))
|
||||
```
|
||||
|
||||
Совпадение даёт `0`, различие — `-2`.
|
||||
|
||||
### Разная мощность
|
||||
|
||||
Множество с большим числом уникальных хэшей не может быть строгим подмножеством меньшего.
|
||||
Поэтому проверяется только одно возможное направление:
|
||||
|
||||
```text
|
||||
cnt1 > cnt2: set2 ⊆ set1 ? 1 : -2
|
||||
cnt1 < cnt2: set1 ⊆ set2 ? -1 : -2
|
||||
```
|
||||
|
||||
Саму проверку выполняет `sorted_subset(small, large)`.
|
||||
|
||||
### `sorted_subset()`
|
||||
|
||||
Алгоритм выбирается по отношению размеров:
|
||||
|
||||
```c
|
||||
jump = large_count / small_count;
|
||||
```
|
||||
|
||||
Если `jump < 4`, используется обычное линейное слияние: указатель большого массива двигается
|
||||
до текущего элемента малого. Для близких по размеру множеств это последовательный проход с
|
||||
хорошей локальностью.
|
||||
|
||||
Если `jump >= 4`, вызывается `step_lower_bound()`. Функция сначала делает шаги примерно на
|
||||
среднее расстояние между искомыми элементами, затем делит шаг пополам, пока не найдёт первый
|
||||
элемент, который не меньше искомого. Это позволяет пропускать части большого массива при
|
||||
разреженном малом множестве без sentinel-значений и выхода за границы.
|
||||
|
||||
При первом отсутствующем элементе `sorted_subset()` возвращает ложь.
|
||||
|
||||
## `SELF_TEST`
|
||||
|
||||
При `SELF_TEST` компилируется `main()`, который проверяет только публичный API на коротких
|
||||
наборах:
|
||||
|
||||
1. строгое надмножество возвращает `1`;
|
||||
2. равные множества возвращают `0`;
|
||||
3. строгое подмножество возвращает `-1`;
|
||||
4. пересекающиеся несравнимые множества возвращают `-2`;
|
||||
5. выделенные строки и оба объекта освобождаются.
|
||||
|
||||
В отличие от исходного `set.c`, встроенный тест является smoke-тестом, а не полной проверкой совместимости.
|
||||
|
||||
## "Карта функций"
|
||||
|
||||
| Функция | Роль |
|
||||
| ------------------------ | -------------------------------------------- |
|
||||
| `set_new()` | создание контейнера символов |
|
||||
| `set_add()` | добавление имени в строковую арену |
|
||||
| `set_free()` | освобождение контейнера |
|
||||
| `hash()` | Jenkins OAAT |
|
||||
| `sort_symbols()` | выбор `qsort()` или radix sort |
|
||||
| `encode_golomb_Mshift()` | вычисление параметра Golomb-Rice |
|
||||
| `encode_writer_*()` | потоковая упаковка битов в Base62 |
|
||||
| `encode_set()` | объединённые delta, Golomb-Rice и Base62 |
|
||||
| `set_fini()` | полный путь от имён до set-строки |
|
||||
| `set_meta_init()` | быстрая проверка заголовка строки |
|
||||
| `set_meta_fini()` | длины и верхние границы буферов |
|
||||
| `decode_chunk()` | чтение обычного или escape Base62-блока |
|
||||
| `decode_set()` | объединённые Base62, Golomb-Rice и delta |
|
||||
| `cache_decode_set()` | два бакетных LRU-кэша |
|
||||
| `downsample_set()` | уменьшение точности на один бит |
|
||||
| `step_lower_bound()` | прыжок и уточнение позиции в большом массиве |
|
||||
| `sorted_subset()` | проверка включения отсортированных множеств |
|
||||
| `rpmsetcmp()` | публичное сравнение set-строк |
|
||||
+44
-28
@@ -1,35 +1,51 @@
|
||||
## original set.c
|
||||
# Результаты APT-RPM тестов
|
||||
|
||||
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status |
|
||||
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
|
||||
| -s check | 1.570747 | 1.621496 | 1.545001 | 1.545744 | 0 |
|
||||
| -s autoremove | 2.294502 | 2.312574 | 2.287133 | 2.283798 | 0 |
|
||||
| -s install rpm-build | 2.108121 | 2.142784 | 2.093474 | 2.088105 | 0 |
|
||||
| -s install openuds-server | 8.028098 | 8.147214 | 8.001140 | 7.935940 | 0 |
|
||||
| -s install password-store | 3.373377 | 3.396943 | 3.361290 | 3.361899 | 0 |
|
||||
| --enable-upgrade -s upgrade | 33.915979 | 33.895262 | 33.770192 | 34.082484 | 100 |
|
||||
| -s dist-upgrade | 88.722136 | 95.484250 | 85.808085 | 84.874073 | 100 |
|
||||
Коэффициент считается как отношение времени базовой реализации к времени проверяемой реализации. Значение больше `1×` означает ускорение, меньше `1×` — замедление.
|
||||
|
||||
- `original set.c` - код текущего `lib/set.c` в `rpm`/`rpm-build`
|
||||
- `set9.c` - drop-in замена оригинального `lib/set.c`.
|
||||
|
||||
## original set.c w/out optimizations
|
||||
|
||||
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status |
|
||||
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
|
||||
| -s check | 4.217343 | 4.310882 | 4.179343 | 4.161804 | 0 |
|
||||
| -s autoremove | 4.884413 | 4.876135 | 4.882347 | 4.894757 | 0 |
|
||||
| -s install rpm-build | 4.634789 | 4.652499 | 4.623077 | 4.628792 | 0 |
|
||||
| -s install openuds-server | 12.704538 | 12.640266 | 12.975482 | 12.497867 | 0 |
|
||||
| -s install password-store | 8.560463 | 8.561515 | 8.557596 | 8.562277 | 0 |
|
||||
| --enable-upgrade -s upgrade | 36.271838 | 35.511443 | 35.908641 | 37.395430 | 100 |
|
||||
| -s dist-upgrade | 90.550086 | 94.702198 | 92.174160 | 84.773901 | 100 |
|
||||
функция `decode_set` идёт по пути функций:
|
||||
|
||||
1. `decode_base62`
|
||||
2. `decode_golomb`
|
||||
3. `decode_delta`
|
||||
|
||||
| command | user, с | system, с | user+system, с |
|
||||
| :-------------------------- | -------: | --------: | -------------: |
|
||||
| `-s check` | 1.320000 | 0.050000 | 1.370000 |
|
||||
| `-s autoremove` | 1.626667 | 0.050000 | 1.676667 |
|
||||
| `-s install rpm-build` | 2.913333 | 0.070000 | 2.983333 |
|
||||
| `-s install openuds-server` | 5.526667 | 0.100000 | 5.626667 |
|
||||
| `-s install password-store` | 2.940000 | 0.070000 | 3.010000 |
|
||||
|
||||
## original set.c
|
||||
|
||||
функция `decode_set` идёт по пути функций:
|
||||
|
||||
1. `decode_base62_golomb`
|
||||
2. `decode_delta`
|
||||
|
||||
Коэффициенты посчитаны относительно `original set.c w/out optimizations`.
|
||||
|
||||
| command | user, с | system, с | user+system, с | коэфф. user | коэфф. user+system |
|
||||
| :-------------------------- | -------: | --------: | -------------: | ----------: | -----------------: |
|
||||
| `-s check` | 0.453333 | 0.033333 | 0.486666 | 2.912× | 2.815× |
|
||||
| `-s autoremove` | 0.763333 | 0.040000 | 0.803333 | 2.131× | 2.087× |
|
||||
| `-s install rpm-build` | 1.210000 | 0.050000 | 1.260000 | 2.408× | 2.368× |
|
||||
| `-s install openuds-server` | 3.746667 | 0.080000 | 3.826667 | 1.475× | 1.470× |
|
||||
| `-s install password-store` | 1.233333 | 0.050000 | 1.283333 | 2.384× | 2.345× |
|
||||
|
||||
## set9.c
|
||||
|
||||
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status |
|
||||
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
|
||||
| -s check | 1.550342 | 1.596307 | 1.532768 | 1.521950 | 0 |
|
||||
| -s autoremove | 2.200188 | 2.237645 | 2.185378 | 2.177540 | 0 |
|
||||
| -s install rpm-build | 2.031055 | 2.045757 | 2.023173 | 2.024234 | 0 |
|
||||
| -s install openuds-server | 7.580058 | 7.572994 | 7.582557 | 7.584622 | 0 |
|
||||
| -s install password-store | 3.337012 | 3.367696 | 3.309826 | 3.333514 | 0 |
|
||||
| --enable-upgrade -s upgrade | 31.494775 | 31.535714 | 31.585522 | 31.363090 | 100 |
|
||||
| -s dist-upgrade | 85.326580 | 85.389775 | 85.604566 | 84.985398 | 100 |
|
||||
Переписанная реализация `set.c`. Коэффициенты посчитаны относительно `original set.c`.
|
||||
|
||||
| command | user, с | system, с | user+system, с | коэфф. user | коэфф. user+system |
|
||||
| :-------------------------- | -------: | --------: | -------------: | ----------: | -----------------: |
|
||||
| `-s check` | 0.503333 | 0.040000 | 0.543333 | 0.901× | 0.896× |
|
||||
| `-s autoremove` | 0.810000 | 0.040000 | 0.850000 | 0.942× | 0.945× |
|
||||
| `-s install rpm-build` | 1.283333 | 0.050000 | 1.333333 | 0.943× | 0.945× |
|
||||
| `-s install openuds-server` | 3.896667 | 0.083333 | 3.980000 | 0.962× | 0.961× |
|
||||
| `-s install password-store` | 1.310000 | 0.050000 | 1.360000 | 0.941× | 0.944× |
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets.
|
||||
|
||||
- [Docs about current set:version implementation](Docs/set:version.md)
|
||||
- [Docs about rewrite implementation (ru)](Docs/set9_ru.md)
|
||||
|
||||
# Compare results
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# Исследование и улучшение механизма работы `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. Итоги
|
||||
|
||||
(нужны ли..)
|
||||
@@ -0,0 +1,74 @@
|
||||
# Работа над `set.c`: краткий план статьи
|
||||
|
||||
## 1. Зачем ALT RPM нужны set-строки
|
||||
|
||||
Обычная зависимость от версии библиотеки не гарантирует, что в ней остались все нужные программе символы: символ можно удалить, не сменив SONAME, а одинаковые SONAME могут скрывать разные наборы экспортов. Поэтому ALT RPM использует версии зависимостей вида `set:<encoded-set>`. В `Provides` такая строка описывает символы, предоставляемые библиотекой, а в `Requires` - символы, которые конкретный потребитель требует именно от неё. Сравниваются не номера версий, а включение множеств: все хэши из `Requires` должны присутствовать в `Provides`. Гарантия вероятностная, поскольку вместо имён хранятся усечённые хэши, но ответ "символ отсутствует", когда он есть мы не получим (ложноположительное срабатывание)
|
||||
|
||||
## 2. Как строится set-строка
|
||||
|
||||
Список символов формируется автодепами `rpm-build`. Для `Provides` из ELF извлекаются отфильтрованные экспортируемые базовые имена символов. Для `Requires` `ldd --bindings` связывает каждый сильный неопределённый символ с конкретной библиотекой, после чего для каждого провайдера строится отдельное множество.
|
||||
|
||||
Далее `mkset` использует API `set_new()` → `set_add()` → `set_fini()`:
|
||||
|
||||
1. для каждого имени вычисляется Jenkins OAAT и оставляются младшие `bpp` бит;
|
||||
2. хэши сортируются, повторы удаляются, о коллизиях выдаётся предупреждение;
|
||||
3. абсолютные значения заменяются дельтами;
|
||||
4. дельты сжимаются кодом Голомба-Райса с параметром `Mshift`;
|
||||
5. битовый поток переводится в RPM-безопасную Base62-строку.
|
||||
|
||||
При сравнении `rpmsetcmp()` выполняет обратное декодирование, приводит строки с разным `bpp` к общей точности и проверяет включение отсортированных наборов. В рабочем вызове первым операндом выгодно передавать `Provides`: исходный код кэширует прежде всего его декодирование. (переформулировать в сторону кэш есть только для)
|
||||
(сказать про подсёт bpp и Mshift на стороне)
|
||||
|
||||
## 3. Разбор исходного кода
|
||||
|
||||
Работа началась с изучения примерно десятилетнего `lib/set.c`. Код оказался большим по объёму и очень плотным: объединённый Base62/Golomb-декодер, таблица всех пар входных байтов, макросы состояний, прыжки по массиву через defined инструкции и подобие LRU-кэш. Отдельно были прослежены реальные точки вызова в `rpm` и `rpm-build`: во время сборки бинарник `mkset` создаёт строки, а `rpmsetcmp()` через `rpmRangesOverlap()` участвует в проверке зависимостей RPM и APT.
|
||||
|
||||
Результатом этого этапа стала документация: описание главных пяти функций API, формата строки, кодирования и декодирования, нормализации `bpp`, кэша, макросов сравнения и встроенного `SELF_TEST`. Таким образом, прежде чем менять алгоритм, для него была построена читаемая модель и зафиксированы его неочевидные инварианты.
|
||||
|
||||
## 4. Переписывание и последовательные оптимизации
|
||||
|
||||
Сначала алгоритм был перенесён в Python как понятная проверочная реализация. Затем появился читаемый C-вариант, а после него - оптимизированный `set9.c`, сохраняющий старый wire-format и публичный API. В нём были опробованы:
|
||||
(добавить про все промежуточные тоже)
|
||||
|
||||
- строковая арена вместо отдельного выделения памяти под каждое имя;
|
||||
- `qsort()` для малых наборов и radix sort для больших;
|
||||
- потоковое кодирование и декодирование без промежуточных массивов битов и дельт; (было и в оригинале, но не было в читаемом)
|
||||
- два раздельных bucketed LRU-кэша для `Provides` и `Requires`; (теперь даже LRU, нет постоянных realloc)
|
||||
- кэширование результата понижения `bpp`; (надо лучше вспомнить разницу)
|
||||
- более простой адаптивный поиск включения в отсортированных массивах;
|
||||
- явная проверка метаданных, размеров и кодов внутренних ошибок. (в оригинале по несколько раз)
|
||||
|
||||
Не все «читаемые» замены оказались быстрыми. Удаление специальных оптимизаций исходного файла замедляло обычные APT-сценарии примерно в 2–3 раза. `set9.c` вернулся к уровню оригинала и в проведённых симуляциях оказался быстрее/медленне него примерно на 1–10%, однако отдельный анализ показал, что старый слитый декодер и прыгающий проход по массивам всё ещё сильны. Поэтому был собран гибридный `set_frank.c`: новые структуры, encoder и кэш соединены с декодером и сравнением из исходного `set.c`.
|
||||
(сказать, что прироста не сильно, читаемость хуже, проверить реальные числа)
|
||||
|
||||
## 5. Как проверялись изменения
|
||||
|
||||
Тесты строились вокруг инвариантов формата: (логично, его и надо сохранить)
|
||||
|
||||
- исходный `SELF_TEST` и расширенные проверки публичного API;
|
||||
- побайтовое совпадение результата `set_fini()` старой и новой реализации для `bpp=10…32`; (уточнить, что на ранд данных)
|
||||
- дифференциальные случайные тесты равенства, включения, несравнимости и обоих направлений сравнения;
|
||||
- повреждённые строки, граничные `bpp`/`Mshift`, разные точности, заполнение и вытеснение кэша;
|
||||
- проверки освобождения памяти; (надо добавить про алгоритм, что этого не было, хе)
|
||||
- проверка реальных `Provides`/`Requires` из Sisyphus;
|
||||
- холодные и прогретые микробенчмарки, затем симуляции и настоящие вызовы ALT APT с проверкой одинакового результата.
|
||||
|
||||
(более того, потом всё равно предполагается запуск в сборочнице. вроде.)
|
||||
|
||||
(куда-то эти мысли про хэш надо впихнуть)
|
||||
Отдельно сравнивались Jenkins OAAT, CityHash и xxHash по скорости и коллизиям на случайных данных и ASCII-строках. Этот эксперимент помог отделить свойства хэш-функции от стоимости остального формата, но простая замена хэша не стала готовым решением: она меняет совместимость строк и не устраняет основную цену декодирования и поиска.
|
||||
|
||||
## 6. Альтернативные форматы и результаты
|
||||
|
||||
Параллельно были проверены варианты, не совместимые со старым форматом.
|
||||
|
||||
**Roaring Bitmap.** Создание bitmap могло быть быстрым, но равномерно распределённые хэши плохо подходят для такого представления. Для 1000 символов при `bpp=32` строка выросла с 3994 до 19924 символов; холодное сравнение было примерно в 4,5 раза, прогретое - в 76 раз медленнее `set9`. Zstd уменьшал строку до 14126 символов, но добавлял ещё больше работы при сравнении. Вариант оставлен как отрицательный эксперимент.
|
||||
|
||||
**Прямой массив хэшей в Base64.** Отказ от delta/Golomb ускорял отдельные микротесты: в одном прогоне холодное сравнение занимало `0,87×`, а прогретое `0,35×` времени `set9`, при росте строки с 3994 до 5340 символов. Но полный поток реальных пар Sisyphus показал обратное (эту стороны надо добить) - текущая реализация была примерно в 1,4 раза медленнее. На отдельном синтетическом APT-графе resolver, напротив, ускорялся на 7–18%, зато `gencaches` замедлялся примерно на 12%. Кроме того, стандартный Base64 использует padding `=` (да, я забыл это пофиксить), который RPM запрещает внутри версии зависимости. Поэтому прямой формат интересен как компромисс между размером и стоимостью декодирования, но в текущем виде не готов заменить set-строки.
|
||||
|
||||
**Другие направления.** Рассматривались отрицательный Bloom-подобный prefilter с обязательной точной проверкой, адаптивный индекс для больших `Provides`, Elias–Fano и глобальные идентификаторы символов. Общий вывод: (в это возможно стоит углубиться и сделать его)
|
||||
|
||||
## 7. Итого
|
||||
|
||||
Суть работы была в понимании "магии" кода. Сначала были восстановлены назначение, формат и реальные пути использования; создана документация и читаемая реализация; после этого каждая оптимизация проверялась на совместимость, безопасность и скорость.
|
||||
Эксперименты показали, что код содержит оправданные низкоуровневые решения, а локально более простой или быстрый формат не обязательно выигрывает на полном потоке RPM/APT (still check). Наиболее практичное продолжение - (что-то)
|
||||
@@ -0,0 +1,588 @@
|
||||
\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)$, не считая сохраняемого в процессе кэша первого операнда.
|
||||
|
||||
\section{Оптимизации с сохранением текущего формата}
|
||||
\label{COMPATIBLE_OPTIMIZATIONS}
|
||||
|
||||
Для проверки направлений оптимизации была создана экспериментальная реализация \texttt{set9.c} [\Ref{SET9}]. Она сохраняет Jenkins OAAT, формат заголовка, Golomb--Rice/Base62-представление и пять функций публичного API. Поэтому сформированные ею строки побайтово совместимы с исходной реализацией, а изменения относятся только к внутренним структурам и алгоритмам.
|
||||
|
||||
Помимо оптимизации сравнения, построение множества было переведено с отдельных \texttt{xstrdup()} для каждого имени на общую растущую строковую арену. В массиве элементов хранятся смещения, которые остаются корректными после \texttt{xrealloc()} арены. Кодирование и декодирование выполняются потоково: дельты, состояния Голомба--Райса и Base62 обрабатываются через 64-битный аккумулятор без отдельных массивов битов и дельт.
|
||||
|
||||
\subsection{Два bucketed LRU-кэша}
|
||||
|
||||
Вместо одного линейного кэша используются два независимых кэша --- для первого и второго операндов. Каждый содержит до 512 записей и таблицу из 1024 бакетов. Разделение предотвращает взаимное вытеснение часто повторяющихся \texttt{Provides} и \texttt{Requires}, а бакеты ограничивают область поиска записи.
|
||||
|
||||
Ключ включает короткий отпечаток исходной строки и целевую точность $b_*$. Совпадение, как и в исходном варианте, обязательно подтверждается полным \texttt{strcmp()}, поэтому ускоряющая структура не меняет семантику. Значение $b_*$ принципиально важно: одна строка может сравниваться с операндами разной точности и давать разные нормализованные массивы.
|
||||
|
||||
При промахе строка декодируется, сразу приводится к $b_*$ и в таком виде сохраняется. Повторное сравнение той же строки при той же точности не требует ни декодирования, ни цикла \texttt{downsample\_set()}. Порядок вытеснения поддерживается двусвязным списком: попадание переносит запись в начало за $O(1)$, а при заполнении удаляется самый старый элемент. В отличие от массивного кэша, полный сдвиг записей не требуется. При равномерном распределении отпечатков ожидаемая стоимость поиска близка к $O(1)$ плюс стоимость окончательной проверки строки.
|
||||
|
||||
\subsection{Radix sort для больших множеств}
|
||||
|
||||
При построении строки исходный \texttt{qsort()} требует $O(n\log n)$ вызовов функции сравнения. Для 32-битных целых ключей число разрядов заранее ограничено, поэтому в \texttt{set9.c} при $n\geq128$ применяется стабильная LSD radix sort по байтам хеша. Для меньших наборов сохраняется \texttt{qsort()}, поскольку подготовка таблиц и временного массива не окупается.
|
||||
|
||||
Число проходов определяется фактически используемой точностью:
|
||||
\[
|
||||
k=\left\lceil\frac b8\right\rceil,
|
||||
\qquad
|
||||
T_{\mathrm{radix}}=O(kn).
|
||||
\]
|
||||
На каждом проходе сначала подсчитываются 256 значений текущего байта, затем префиксные суммы преобразуют счётчики в позиции, после чего элементы стабильно распределяются во временный массив. Источник и приёмник меняются местами между проходами. Дополнительная память равна
|
||||
\[
|
||||
M_{\mathrm{radix}}=O(n+256).
|
||||
\]
|
||||
Так как $b\leq32$, выполняется не более четырёх линейных проходов.
|
||||
|
||||
\subsection{Адаптивная проверка включения}
|
||||
|
||||
Мощности массивов после нормализации позволяют заранее исключить часть отношений. Если $p=r$, строгого включения быть не может: равенство проверяется одним \texttt{memcmp()}, а несовпадение означает несравнимость. Если $p>r$, проверяется только $\widehat R\subseteq\widehat P$; обратное строгое включение невозможно. Случай $p<r$ обрабатывается симметрично. Тем самым устраняется одновременное ведение двух флагов и выбирается единственная содержательная проверка.
|
||||
|
||||
Функция \texttt{sorted\_subset()} использует отношение мощностей
|
||||
\[
|
||||
j=\left\lfloor\frac{n_{\mathrm{large}}}{n_{\mathrm{small}}}\right\rfloor.
|
||||
\]
|
||||
При $j<4$ выполняется обычное линейное слияние. Для разреженного случая поиск очередного элемента начинается от позиции предыдущего совпадения, делает шаг приблизительно $j$, а затем делит шаг пополам до нахождения нижней границы. Указатель по большому массиву движется только вперёд; при первом отсутствующем хеше функция немедленно возвращает отрицательный результат.
|
||||
|
||||
Для плотного случая сохраняется оценка $O(p+r)$. В разреженном потоке суммарное число крупных шагов имеет порядок $O(p/j)$, а уточнение границы требует до $O(\log j)$ проб на один элемент меньшего множества:
|
||||
\[
|
||||
T_{\mathrm{sparse}}=
|
||||
O\left(\frac pj+r\log j\right).
|
||||
\]
|
||||
При характерном $j\approx p/r$ первое слагаемое имеет порядок $r$. В отличие от фиксированного выбора между шагами 4 и 8, величина перехода адаптируется к наблюдаемому отношению мощностей.
|
||||
|
||||
\section{Результаты экспериментальной проверки}
|
||||
\label{RESULTS}
|
||||
|
||||
Проверка экспериментальной реализации проводилась в два этапа. Сначала проверялась корректность формата и операций над множествами, затем измерялась производительность в сценариях пакетного менеджера.
|
||||
|
||||
\subsection{Дифференциальное тестирование на синтетических множествах}
|
||||
|
||||
Для проверки совместимости был разработан дифференциальный тест, не использующий заранее подготовленные \texttt{set:}-строки. На каждой итерации он самостоятельно формировал исходное множество из случайных уникальных строк. Число символов выбиралось в диапазоне от 1 до 1000, длина каждого имени --- от 1 до 100 знаков, а параметр $bpp$ --- от 10 до 32. В алфавит входили латинские буквы, цифры и знаки, встречающиеся в именах экспортируемых символов: точка, знак \texttt{@} и подчёркивание.
|
||||
|
||||
Из исходного множества строилось второе множество с заранее известным отношением к первому. Тест охватывал четыре класса входов: равные множества, строгое включение, несравнимые множества и некорректные \texttt{set:}-строки. Для проверки строгого включения из первого множества удалялась случайная непустая часть элементов. В случае несравнимости после удаления добавлялись новые символы, отсутствующие в первом множестве. Некорректная строка генерировалась случайным набором символов, в таком случае был шанс получить корректную строку, но для нас остаётся важным одинаковый результат двух программ.
|
||||
|
||||
Один и тот же набор исходных символов независимо передавался построителю исходного \texttt{set.c} и построителю \texttt{set9.c}. Таким образом, каждая реализация сама выполняла хеширование, сортировку и кодирование, после чего её вариант \texttt{rpmsetcmp()} сравнивал полученную пару строк. Тест сопоставлял наблюдаемый результат публичного API: $1$ для строгого надмножества, $0$ для равенства, $-1$ для строгого подмножества, $-2$ для несравнимости и $-3$ или $-4$ для ошибки декодирования соответствующего операнда. Для случая включения операнды дополнительно менялись местами, что позволяло одновременно проверить результаты $1$ и $-1$.
|
||||
|
||||
Генерация и сравнение выполнялись циклически до ручной остановки теста. Такой подход проверял эквивалентность реализаций на широком диапазоне мощностей и точностей.
|
||||
|
||||
\subsection{Производительность в сценариях APT}
|
||||
|
||||
Для измерения времени использовались две локальные сборки \texttt{librpm}: с исходным \texttt{set.c} и с \texttt{set9.c}. На каждой сборке трижды выполнялись одинаковые успешно завершившиеся симуляции APT. В табл.~\Ref{APT_RESULTS} приведены средние значения процессорного времени пользователя и ядра. Время ожидания и ввода-вывода в эти величины не входит.
|
||||
|
||||
Коэффициент вычислялся по суммарному процессорному времени:
|
||||
\[
|
||||
K_{\mathrm{CPU}}=
|
||||
\frac{U_{\mathrm{original}}+S_{\mathrm{original}}}
|
||||
{U_{\mathrm{set9}}+S_{\mathrm{set9}}}.
|
||||
\]
|
||||
Значение $K_{\mathrm{CPU}}>1$ означает ускорение, а $K_{\mathrm{CPU}}<1$ --- замедление экспериментальной реализации.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{APT_RESULTS}Сравнение исходного \texttt{set.c} и \texttt{set9.c} в симуляциях APT}
|
||||
\scriptsize
|
||||
\begin{tabular}{|l|r|r|r|r|r|}
|
||||
\hline
|
||||
Сценарий & \multicolumn{2}{c|}{Исходный, с} & \multicolumn{2}{c|}{\texttt{set9.c}, с} & $K_{\mathrm{CPU}}$ \\
|
||||
\cline{2-5}
|
||||
& user & system & user & system & \\
|
||||
\hline
|
||||
\texttt{-s check} & 0,453 & 0,033 & 0,503 & 0,040 & 0,896 \\
|
||||
\hline
|
||||
\texttt{-s autoremove} & 0,763 & 0,040 & 0,810 & 0,040 & 0,945 \\
|
||||
\hline
|
||||
\texttt{-s install rpm-build} & 1,210 & 0,050 & 1,283 & 0,050 & 0,945 \\
|
||||
\hline
|
||||
\texttt{-s install openuds-server} & 3,747 & 0,080 & 3,897 & 0,083 & 0,961 \\
|
||||
\hline
|
||||
\texttt{-s install password-store} & 1,233 & 0,050 & 1,310 & 0,050 & 0,944 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Во всех пяти измеренных сценариях \texttt{set9.c} не превзошёл исходный вариант: суммарное процессорное время увеличилось примерно на 4,0--11,6\%. Это отрицательный результат, вызванный более простым потоковым декодером, который не превзошёл специализированного табличного декодера исходной реализации даже при условии остальных оптимизаций.
|
||||
|
||||
Для проверки этого объяснения исходный слитый декодер отдельно сравнивался с вариантом, в котором Base62, код Голомба--Райса и восстановление дельт выполнялись последовательными стадиями. В зависимости от сценария слитый путь сокращал суммарное процессорное время в 1,47--2,82 раза. Следовательно, благодаря табличной обработке Base62/Golomb получается измеримый выигрыш. Практическое направление дальнейшей работы может состоять в разработке нового формата хранения и обработки \texttt{set:}-строк.
|
||||
|
||||
\section{Варианты со сменой формата или API}
|
||||
\label{ALTERNATIVE_FORMATS}
|
||||
|
||||
Оптимизации разд.~\Ref{COMPATIBLE_OPTIMIZATIONS} ограничены требованием побайтовой совместимости. Параллельно исследовались представления, снимающие это ограничение. Их результаты нельзя напрямую переносить на существующие RPM-метаданные: новый формат требует повторного формирования зависимостей репозитория.
|
||||
|
||||
\subsection{Roaring Bitmap: отрицательный результат}
|
||||
|
||||
Roaring Bitmap [\Ref{ROARING}] предназначен прежде всего для множеств, содержащих плотные участки целочисленного пространства. Усечённые хеши, напротив, распределены по диапазону приблизительно равномерно. Поэтому контейнеры bitmap не получают длинных серий соседних значений, но сохраняют собственные заголовки и индексы.
|
||||
|
||||
В микротесте использовались 1000 предоставляемых и 500 требуемых символов при $b=32$. Результаты приведены в табл.~\Ref{ROARING_RESULTS}. Коэффициенты времени вычислены относительно \texttt{set9.c}; значение больше единицы означает замедление.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{ROARING_RESULTS}Сравнение Golomb/Base62 и Roaring Bitmap}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|r|}
|
||||
\hline
|
||||
Представление & Длина строки & Cold compare & Warm compare \\
|
||||
\hline
|
||||
Golomb/Base62 \texttt{set9} & 3\,994 & 1,00 & 1,00 \\
|
||||
\hline
|
||||
Roaring/hex & 19\,924 & 4,50 & 76,14 \\
|
||||
\hline
|
||||
Roaring+zstd & 14\,126 & 4,96 & 101,86 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Без дополнительного сжатия строка оказалась длиннее в 4,99 раза. Холодное сравнение заняло 1321,17 мкс вместо 293,87 мкс, а прогретое --- 399,20 мкс вместо 5,24 мкс. При этом построение bitmap занимало лишь $0,76$ времени \texttt{set\_fini()} варианта \texttt{set9}; ускорение генерации не компенсировало стоимость хранения и сравнения.
|
||||
|
||||
Сжатие zstd [\Ref{ZSTD}] уменьшило строку до 14\,126 символов, то есть до 3,54 длины \texttt{set9}, но добавило декомпрессию в горячий путь. В отдельном прогоне холодное и прогретое сравнения были медленнее соответственно в 4,96 и 101,86 раза. Таким образом, Roaring Bitmap для равномерных хешей является подтверждённым отрицательным вариантом: структура данных не соответствует распределению кодируемых значений.
|
||||
|
||||
\subsection{Прямые хеши в Base64}
|
||||
|
||||
Другой вариант, обозначенный как D1, исключает дельты и код Голомба--Райса. $n$ отсортированных уникальных хешей записываются подряд по $b$ бит, после чего битовый массив преобразуется в Base64-строку [\Ref{BASE64}]. Если
|
||||
\[
|
||||
Q=\left\lceil\frac{nb}{8}\right\rceil
|
||||
\]
|
||||
--- число байтов упакованного массива, то доминирующая часть длины payload равна
|
||||
\[
|
||||
L_{\mathrm{payload}}=
|
||||
\left\lceil\frac{4Q}{3}\right\rceil,
|
||||
\]
|
||||
а заголовок формата добавляет постоянное число символов. В отличие от Golomb--Rice, эта оценка почти не зависит от расстояний между соседними хешами: цена быстрого произвольного доступа --- увеличение метаданных.
|
||||
|
||||
Для тех же 1000/500 символов при $b=32$ длина строки выросла с 3994 до 5340 символов, то есть на 33,7\%. Результаты теста показаны в табл.~\Ref{DIRECT_RESULTS}; отношение меньше единицы означает, что D1 затратил меньшую долю времени \texttt{set9.c}.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{DIRECT_RESULTS}Тест прямого представления D1}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|r|}
|
||||
\hline
|
||||
Операция & \texttt{set9}, мкс & D1, мкс & D1/\texttt{set9} \\
|
||||
\hline
|
||||
\texttt{set\_fini()} & 147,45 & 105,09 & 0,71 \\
|
||||
\hline
|
||||
\texttt{rpmsetcmp()}, cold & 129,67 & 113,04 & 0,87 \\
|
||||
\hline
|
||||
\texttt{rpmsetcmp()}, warm & 2,99 & 1,04 & 0,35 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Для крупного требования прямое представление ускорило все три измеряемые операции, особенно попадание в кэш. Однако при одном требуемом символе холодное сравнение занимало 83,22 мкс ($0,88$ времени \texttt{set9}), а прогретое --- 0,86 мкс ($1,02$), то есть преимущество исчезало. Это подчёркивает зависимость результата от формы нагрузки: выигрыш одного сравнения не доказывает ускорение полного потока зависимостей APT. D1 представляет интерес как компромисс между размером и стоимостью декодирования, но требует отдельной сквозной проверки на преобразованных метаданных репозитория.
|
||||
|
||||
\subsection{Альтернативные хеш-функции}
|
||||
|
||||
Отдельно сравнивались Jenkins OAAT, xxHash32 [\Ref{XXHASH}] и CityHash32 [\Ref{CITYHASH}]. Сравнение было необходимо, так как xxHash и CityHash --- хеш-функции, разработанные значительно позже последнего обновления \texttt{set.c}. В каждом замере использовались три запуска; в табл.~\Ref{HASH_SPEED} приведены медианы. Для коротких строк указано время одного хеширования, для длинных --- пропускная способность.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{HASH_SPEED}Скорость 32-битных хеш-функций}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|}
|
||||
\hline
|
||||
Функция & 32 байта, нс/хеш & 1024 байта, ГиБ/с \\
|
||||
\hline
|
||||
Jenkins OAAT & 50,98 & 0,53 \\
|
||||
\hline
|
||||
xxHash32 & 11,56 & 5,18 \\
|
||||
\hline
|
||||
CityHash32 & 16,70 & 4,10 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
На строках длиной 32 байта xxHash32 был быстрее Jenkins примерно в 4,4 раза, CityHash32 --- в 3,1 раза. При 1024 байтах различие по пропускной способности достигало соответственно 9,8 и 7,7 раза. Следовательно, Jenkins OAAT не является оптимальным по скорости, особенно на длинных входах.
|
||||
|
||||
Для оценки качества использовался другой тест, в нём сравнивались Jenkins OAAT, 64-битный xxHash и \texttt{t1ha2\_atonce} [\Ref{T1HA}]. Поскольку формат \texttt{set:version} сохраняет не более 32 бит хеша, при сопоставлении учитывались младшие 32 выходных бита всех трёх функций.
|
||||
|
||||
Тест измерял лавинный эффект на реальном корпусе экспортируемых C++-символов ALT Linux p11. Утилита \texttt{provided\_symbols} извлекла из 259 ELF-файлов пяти пакетов 577\,509 уникальных имён. Пакеты выбирались среди имеющих наиболее длинные \texttt{Provides set:}-строки. Корпус специально является сложным для хеширования похожих строк: 99,60\% символов имеют с другим символом общий префикс длиной не менее 12 знаков, 95,05\% --- не менее 24 знаков, а медиана максимального общего префикса равна 61 знаку.
|
||||
|
||||
Для каждого бита вычислялась доля $q_i$ корпусных хешей, в которых этот бит равен единице. Идеальному распределению соответствует $q_i=0{,}5$. Результаты обоих тестов представлены в табл.~\Ref{HASH_QUALITY_EXOTIC}. Значения приведены в процентах от полной шкалы вероятности; последний столбец содержит максимальное среднее абсолютное отклонение младших 32 бит от $0{,}5$.
|
||||
|
||||
Среднее абсолютное отклонение по младшим 32 битам вычислялось как:
|
||||
\[
|
||||
\overline{\Delta}=\frac{1}{32}\sum_{i=0}^{31}|p_i-0{,}5|.
|
||||
\]
|
||||
Меньшее значение $\overline{\Delta}$ соответствует более равномерному лавинному эффекту.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{HASH_QUALITY_EXOTIC}Качество перемешивания младших 32 бит хеш-функций}
|
||||
\scriptsize
|
||||
\begin{tabular}{|l|r|r|r|r|}
|
||||
\hline
|
||||
Функция & \makecell{Корпус\\$\overline{\Delta}$, \%} & \makecell{Корпус\\$\Delta_{\max}$, \%} \\
|
||||
\hline
|
||||
Jenkins OAAT & 0,0475 & 0,167 \\
|
||||
\hline
|
||||
xxHash64 & 0,0431 & 0,122 \\
|
||||
\hline
|
||||
t1ha2 & 0,0615 & 0,188 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Все три функции продемонстрировали близкое к равномерному распределение. На реальном корпусе лучший результат получен для xxHash64, однако разность между функциями составляла сотые доли процента, а максимальное отклонение отдельного бита не превысило 0,188\%.
|
||||
|
||||
Таким образом, тестирование на похожих C++-символах не выявило недостаточного перемешивания Jenkins OAAT. xxHash64 и t1ha2 остаются кандидатами для отдельной оценки производительности, но качество распределения само по себе не обосновывает замену Jenkins.
|
||||
|
||||
\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}
|
||||
Jenkins B.
|
||||
\emph{A Hash Function for Hash Table Lookup}.
|
||||
1997, обновлено в 2013 г.
|
||||
[Электронный ресурс] URL: \url{https://burtleburtle.net/bob/hash/doobs.html}.
|
||||
|
||||
\item
|
||||
\Label{GOLOMB-RICE}
|
||||
Wikipedia contributors.
|
||||
\emph{Golomb coding: описание кодов Голомба и Райса}.
|
||||
[Электронный ресурс] URL: \url{https://en.wikipedia.org/wiki/Golomb_coding}.
|
||||
|
||||
\item
|
||||
\Label{SET9}
|
||||
Гудов Д.О.
|
||||
\emph{set9.c --- экспериментальная реализация алгоритмов set:version}.
|
||||
Исходный код на GitHub.
|
||||
[Электронный ресурс] URL: \url{https://github.com/kr0sh512/alt-rpm-set-version/blob/main/reimplement/set9.c}.
|
||||
|
||||
\item
|
||||
\Label{ROARING}
|
||||
RoaringBitmap.org.
|
||||
\emph{Roaring Bitmaps --- A better compressed bitset}.
|
||||
[Электронный ресурс] URL: \url{https://roaringbitmap.org/}.
|
||||
|
||||
\item
|
||||
\Label{BASE64}
|
||||
Josefsson S.
|
||||
\emph{The Base16, Base32, and Base64 Data Encodings}. RFC~4648, 2006.
|
||||
[Электронный ресурс] URL: \url{https://www.rfc-editor.org/info/rfc4648}.
|
||||
|
||||
\item
|
||||
\Label{ZSTD}
|
||||
Collet Y., Kucherawy M.
|
||||
\emph{Zstandard Compression and the application/zstd Media Type}. RFC~8878, 2021.
|
||||
[Электронный ресурс] URL: \url{https://www.rfc-editor.org/info/rfc8878}.
|
||||
|
||||
\item
|
||||
\Label{XXHASH}
|
||||
Collet Y.
|
||||
\emph{xxHash --- extremely fast non-cryptographic hash algorithm}.
|
||||
Исходный код и документация.
|
||||
[Электронный ресурс] URL: \url{https://github.com/Cyan4973/xxHash}.
|
||||
|
||||
\item
|
||||
\Label{CITYHASH}
|
||||
Google.
|
||||
\emph{CityHash --- a family of hash functions for strings}.
|
||||
Исходный код и документация.
|
||||
[Электронный ресурс] URL: \url{https://github.com/google/cityhash}.
|
||||
|
||||
\item
|
||||
\Label{T1HA}
|
||||
Erthink.
|
||||
\emph{t1ha --- Fast Positive Hash}.
|
||||
Исходный код и документация.
|
||||
[Электронный ресурс] URL: \url{https://gitflic.ru/project/erthink/t1ha}.
|
||||
|
||||
\end{enumerate}
|
||||
@@ -0,0 +1,141 @@
|
||||
%% ---------------------------------------------------------------------------
|
||||
\documentclass[openany, twoside, a4paper, 10pt]{extbook}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[english, russian]{babel}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{indentfirst}
|
||||
\frenchspacing
|
||||
\raggedbottom
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{url}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{ifpdf}
|
||||
\ifpdf
|
||||
\usepackage[pdftex]{graphicx}
|
||||
\usepackage{cmap}
|
||||
\pdfcompresslevel=9
|
||||
\DeclareGraphicsExtensions{.pdf,.png,.jpg}
|
||||
\else
|
||||
\usepackage[dvips]{graphicx}
|
||||
\DeclareGraphicsExtensions{.eps}
|
||||
\fi
|
||||
\graphicspath{ {./images/} }
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{float}
|
||||
\restylefloat{table}
|
||||
\usepackage{multirow}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{amscd}
|
||||
\usepackage{bm}
|
||||
\usepackage{caption}
|
||||
\usepackage{makecell}
|
||||
\usepackage{nameref}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{titlesec}
|
||||
\titleformat{\section}[block]{\rmfamily\Large\bfseries\filcenter}{\thesection.}{1em}{}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\sloppy
|
||||
%% ---------------------------------------------------------------------------
|
||||
% a5paper: 148 x 210
|
||||
% print area: 110 x 180
|
||||
% top = bottom = (210 - 180) / 2 = 15
|
||||
% left = right = (148 - 110) / 2 = 19
|
||||
\usepackage[%
|
||||
left=5cm,%
|
||||
top=5.85cm,%
|
||||
right=5cm,%
|
||||
bottom=5.85cm,%
|
||||
headsep=0.2cm,%
|
||||
footskip=0.5cm,%
|
||||
]{geometry}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{titlesec}
|
||||
\newpagestyle{lvkpagestyle}{
|
||||
\sethead{}{}{}
|
||||
\setfoot{}{\usepage}{}
|
||||
}
|
||||
\pagestyle{lvkpagestyle}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand\Title[2]{%
|
||||
%\par
|
||||
\begin{centering}
|
||||
\medskip
|
||||
{
|
||||
\textbf{#1} \\
|
||||
}
|
||||
{
|
||||
\Large
|
||||
\textbf{\textsc{#2}} \\
|
||||
}
|
||||
\bigskip
|
||||
\end{centering}
|
||||
\@afterheading
|
||||
}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand{\References}{
|
||||
\medskip
|
||||
\section*{Литература}
|
||||
\nopagebreak
|
||||
\medskip
|
||||
\@afterheading
|
||||
}
|
||||
\makeatother
|
||||
|
||||
\makeatletter
|
||||
\newcommand{\ReferencesEng}{
|
||||
\medskip
|
||||
\section*{References}
|
||||
\nopagebreak
|
||||
\medskip
|
||||
\@afterheading
|
||||
}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand{\unchapter}[1]{%
|
||||
\begingroup
|
||||
\let\@makechapterhead\@gobble % make \@makechapterhead do nothing
|
||||
\chapter{#1}
|
||||
\endgroup
|
||||
}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand{\startgroup}[1]{\addtocontents{toc}{\contentsline{chapter}{#1}{}}}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\renewcommand*\thesection{\arabic{section}}
|
||||
\newcommand{\AddRefPrefix}[1]{
|
||||
\renewcommand{\Pageref}[1]{\pageref{#1##1}}
|
||||
\renewcommand{\Ref}[1]{\ref{#1##1}}
|
||||
\renewcommand{\Label}[1]{\label{#1##1}}
|
||||
}
|
||||
%%\newcommand{\Ref}[1]{\ref{#1}}
|
||||
\newcommand{\Pageref}[1]{\pageref{#1}}
|
||||
\newcommand{\Label}[1]{\label{#1}}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\renewcommand{\thetable}{\arabic{table}}
|
||||
\renewcommand{\thefigure}{\arabic{figure}}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{pdfpages}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\begin{document}
|
||||
%% \setcounter{page}{3}
|
||||
\setcounter{tocdepth}{0}
|
||||
%% \tableofcontents
|
||||
%% ---------------------------------------------------------------------------
|
||||
|
||||
% \startgroup{Секция 1. Разные вещи}
|
||||
% \unchapter{\textnormal{\textit{Иванов И.И.}} Первая статья в сборнике}
|
||||
% \AddRefPrefix{th1}
|
||||
\input{article_hbd}
|
||||
|
||||
%% ---------------------------------------------------------------------------
|
||||
\end{document}
|
||||
%% ---------------------------------------------------------------------------
|
||||
@@ -0,0 +1,241 @@
|
||||
# Исследование алгоритма разрешения зависимостей в ALT RPM
|
||||
|
||||
## Замысел и структура статьи
|
||||
|
||||
Статья будет как решение одной формальной задачи. В ALT RPM пакет-потребитель задаёт множество
|
||||
требуемых символов $R$ (`Requires`), а пакет-поставщик - множество предоставляемых
|
||||
символов $P$ (`Provides`). Зависимость выполнима тогда и только тогда, когда
|
||||
|
||||
$$
|
||||
R \subseteq P.
|
||||
$$
|
||||
|
||||
Явное перечисление тысяч ELF-, Python- и файловых символов раздувает RPM-метаданные,
|
||||
поэтому множества представлены компактными set-строками.
|
||||
|
||||
## 1. Постановка задачи и данные ALT Linux
|
||||
|
||||
Ввести обозначения для bpp(b), множеств, хэш функций(H), длин строк и т.д.
|
||||
|
||||
Проверяемое программой условие имеет вид
|
||||
|
||||
$$
|
||||
H_b(R)\subseteq H_b(P).
|
||||
$$
|
||||
|
||||
Отмечаем вероятностную природу: из $R\subseteq P$ следует
|
||||
$H_b(R)\subseteq H_b(P)$, поэтому коллизия не создаёт ложного отказа. Обратное
|
||||
неверно: отсутствующий символ может совпасть по хешу с символом из $P$ и дать
|
||||
ложное принятие зависимости.
|
||||
|
||||
(Представить данные ниже табличкой)
|
||||
Привести срез Sisyphus `x86_64 + noarch`: 47 367 пакетов, 14 761 `Provides`-отношение и 68 857
|
||||
`Requires`-отношений. Для 68 198 реально сопоставимых пар медианы равны
|
||||
$p=480$, $r=13$, $p/r=28.1$; p75 - 1886, 37 и 80; p90 - 6219, 104 и 257.
|
||||
В 92.4% пар выполняется $p/r\ge 4$. Это к теме, почему рассматриваем разреженные множества.
|
||||
|
||||
## 2. Устройство существующей `set:`-строки
|
||||
|
||||
Даём схему формата:
|
||||
|
||||
```
|
||||
массив строк
|
||||
| (Jenkins OAAT)
|
||||
v
|
||||
массив хэшей
|
||||
| (qsort)
|
||||
v
|
||||
отсортированный массив хэшей
|
||||
| (вычисление разницы между элементами)
|
||||
v
|
||||
массив delta
|
||||
| (Rice-Golomb преобразование)
|
||||
v
|
||||
битовый массив
|
||||
| (base62 преобразование)
|
||||
v
|
||||
set-строка
|
||||
```
|
||||
|
||||
Нужно отдельно разобрать заголовок, диапазоны `bpp` и `Mshift`, внешний префикс
|
||||
`set:` и роль Base62: строка должна оставаться допустимым токеном версии RPM.
|
||||
Полезна небольшая иллюстрация на 5-8 символах: исходные имена, хеши,
|
||||
отсортированный массив, дельты, биты кода и итоговая строка.
|
||||
Но. Это много альт-специфики, может настолько глубоко не стоит (хотя и не очень глубоко)
|
||||
|
||||
### 2.1. Хеширование и вероятность коллизий
|
||||
|
||||
! Всё ещё нет нормального анализа коллизий
|
||||
|
||||
Текущий код использует 32-битный Jenkins OAAT и оставляет $b$ бит. Для модели
|
||||
равномерного независимого хеширования ожидаемое число столкнувшихся пар среди
|
||||
$n$ различных символов равно
|
||||
|
||||
(будет мудрёная формула, теория)
|
||||
|
||||
В статье формулы дополняем разными табличками по реальным данным из Sisyphus: распределения $p,r,b$, ожидаемые коллизии, реальные.
|
||||
|
||||
(Другие хэши идут далее)
|
||||
|
||||
### 2.2. Кодирование Golomb-Rice
|
||||
|
||||
Для отсортированных значений $0\le x_1<\dots<x_n<N$ определить
|
||||
$\delta_1=x_1$, $\delta_i=x_i-x_{i-1}$.
|
||||
|
||||
Занимаеи столько и столько бит. В реализации параметр выбирается из
|
||||
$b$ и $n$ приблизительно как
|
||||
|
||||
$$
|
||||
m=b-\lfloor\log_2 n\rfloor-1
|
||||
$$
|
||||
|
||||
с ограничением допустимым диапазоном формата. При средней дельте
|
||||
$\mathbb E\delta\approx 2^b/n$ это даёт ожидаемую длину порядка
|
||||
|
||||
...
|
||||
|
||||
### 2.3. Base62
|
||||
|
||||
Пояснить экзотичность Base62, его алфавит, принцип кодирования, Z-escape,
|
||||
|
||||
## 3. Построение `set:`-строк в `set.c`
|
||||
|
||||
(фактически его можно в 2 упихать)
|
||||
|
||||
Рассказать путь `set_new()` - `set_add()` - `set_fini()` и отдельно объяснить
|
||||
сложные участки:
|
||||
|
||||
(а тут и нет сложного, всё в cmp запихали)
|
||||
Можно рассказать о слитом энкодере, но это есть в переписанной части
|
||||
|
||||
В оригинальном варианте используется `qsort()`
|
||||
|
||||
Записать итоговую сложность построения:
|
||||
|
||||
...
|
||||
|
||||
И доп памяти:
|
||||
|
||||
...
|
||||
|
||||
## 4. Сравнение `set:`-строк
|
||||
|
||||
### 4.1. Обратное декодирование
|
||||
|
||||
Кратко пройти обратную цепочку:
|
||||
|
||||
```
|
||||
set-строка
|
||||
| (обратное base62 преобразование)
|
||||
v
|
||||
битовый массив
|
||||
| (обратное Rice-Golomb преобразование)
|
||||
v
|
||||
массив delta
|
||||
| (вычисление изначальных значений)
|
||||
v
|
||||
массив хэшей
|
||||
```
|
||||
|
||||
Если `bpp` различается, более точное множество проецируется на меньшее число бит,
|
||||
после чего снова удаляются дубликаты. (btw, тут есть сортировка слиянием, интересный момент)
|
||||
|
||||
### 4.2. Кэш декодированных множеств
|
||||
|
||||
В исходном `set.c` кэш содержит 256 записей (`CACHE_SIZE=256`), при вытеснении
|
||||
использует позицию 243 (`PIVOT_SIZE=243`), `free()` и два `memmove()`. Ключ включает
|
||||
короткий fingerprint, а окончательная проверка требует сравнения строки.
|
||||
|
||||
- что именно кэшируется;
|
||||
- стоимость линейного поиска, `strcmp`, перемещений и аллокаций;
|
||||
- слабое повторное использование при потоке десятков тысяч различных пар;
|
||||
|
||||
(всё надо сильно короче расписывать)
|
||||
|
||||
### 4.3. Проверка включения и «прыжок» по массивам
|
||||
|
||||
После декодирования задача сводится к двум возрастающим массивам. Базовый вариант
|
||||
|
||||
рассказ про IFLT4 и IFLT8
|
||||
|
||||
Худшая асимптотика остаётся
|
||||
|
||||
$$
|
||||
T_{\subseteq}(p,r)=O(p+r),
|
||||
$$
|
||||
|
||||
Полная сложность алгоритма холодного сравнения:
|
||||
|
||||
...
|
||||
|
||||
При попадании в кэш:
|
||||
|
||||
...
|
||||
|
||||
## 5. Оптимизации с сохранением текущего формата
|
||||
|
||||
### 5.1. Новый кэш
|
||||
|
||||
Заменить небольшой кэш на два раздельных bucketed LRU для `Provides` и `Requires`, увеличить вместимость. Хранить длину и более сильный fingerprint, избегать лишних
|
||||
`strlen`/`strcmp`, `free` и полных `memmove`. Отдельно убрать повторные `realloc` при построении входных наборов.
|
||||
Проверить cold, warm, поток меняющихся `Requires` к одному `Provides` и поток
|
||||
полностью уникальных пар.
|
||||
|
||||
### 5.2. Radix sort
|
||||
|
||||
Для больших наборов заменить $O(n\log n)$ `qsort()` стабильной LSD radix sort по
|
||||
байтам хеша. Число проходов
|
||||
|
||||
$$
|
||||
k=\left\lceil\frac b8\right\rceil,
|
||||
\qquad T_{\mathrm{radix}}=O(kn),
|
||||
$$
|
||||
|
||||
дополнительная память $O(n+256)$. Для малых $n$ оставить `qsort()`; в `set9.c`
|
||||
порог равен 128.
|
||||
|
||||
### 5.3. Оптимальный проход по массивам
|
||||
|
||||
Сделать адаптивный алгоритм:
|
||||
|
||||
- равные мощности - `memcmp` и быстрый вывод «равны/несравнимы»;
|
||||
- близкие мощности - обычное линейное слияние;
|
||||
- $p/r$ велико - galloping/jump search с монотонной нижней границей;
|
||||
- немедленный выход при первом отсутствующем хеше.
|
||||
|
||||
## 6. Варианты со сменой формата или API
|
||||
|
||||
### 6.1. Roaring Bitmap - отрицательный результат
|
||||
|
||||
Равномерно распределённые хеши не образуют плотных диапазонов, ради которых создан
|
||||
Roaring. Для 1000 символов при `bpp=32` получено:
|
||||
|
||||
| Представление | Длина строки | Cold compare | Warm compare |
|
||||
| -------------------- | -----------: | -----------: | -----------: |
|
||||
| Golomb/Base62 `set9` | 3994 | 293.87 мкс | 5.24 мкс |
|
||||
| raw Roaring/hex | 19 924 | 1321.17 мкс | 399.20 мкс |
|
||||
|
||||
То есть строка длиннее в 4.99 раза, cold-сравнение медленнее в 4.50 раза, warm -
|
||||
в 76.14 раза. Zstd уменьшал строку до 14 126 символов, но оставлял её в 3.54 раза
|
||||
длиннее `set9`; в соответствующем прогоне cold- и warm-сравнение проиграли в 4.96
|
||||
и 101.86 раза. Этот вариант нужен в статье как важный отрицательный эксперимент.
|
||||
|
||||
### 6.2. Прямые хеши и Base64
|
||||
|
||||
В формате D1 отсортированные усечённые хеши хранятся фиксированной шириной без
|
||||
дельт и Golomb-кода, затем упаковываются в Base64. Теоретическая длина payload:
|
||||
|
||||
...
|
||||
|
||||
Для 1000 символов и `bpp=32` строка выросла с 3994 до 5340
|
||||
символов (+33.7%). Приложить разные результаты тестов. (тесты бы прогнать снова)
|
||||
|
||||
Отдельно про увеличение размера и влияние на apt команды от этого
|
||||
|
||||
выводы насчёт формата
|
||||
|
||||
### 6.3. Альтернативные хэш функции
|
||||
|
||||
тут про разные хэши, их быстрдействие и тесты коллизий
|
||||
|
||||
## 8. Вывод
|
||||
Submodule
+1
Submodule hash_testing/old_testing/src/cityhash added at f5dc54147f
Submodule
+1
Submodule hash_testing/old_testing/src/xxHash added at e573d4d2aa
@@ -0,0 +1,31 @@
|
||||
В этой реализации хэши хранятся напрямую, без Golomb-Rice-кодирования.
|
||||
|
||||
Set-строка целиком декодируется и проверяется при первом обращении. Как и в
|
||||
`set9.c`, полностью декодированные массивы хэшей сохраняются в двух LRU-кэшах
|
||||
по 512 записей — отдельно для первого и второго операнда.
|
||||
|
||||
Для 32-битного формата Base64
|
||||
декодируется сразу в три `unsigned` за блок с одновременной проверкой порядка.
|
||||
Большое понижение BPP выполняется radix-сортировкой, а не отдельным проходом на
|
||||
каждый бит.
|
||||
|
||||
Последний запуск `taskset -c 2 python3 benchmark.py`:
|
||||
|
||||
```text
|
||||
symbols=1000 required=500 bpp=32
|
||||
implementation set_chars format
|
||||
set9 3994 golomb/base62
|
||||
direct 5340 D1/base64
|
||||
|
||||
operation set9 direct direct/set9
|
||||
set_fini only 147.45 us 105.09 us 0.71x
|
||||
new+add (ctypes) 611.28 us 608.24 us 1.00x
|
||||
new+add+fini (ctypes) 769.66 us 693.60 us 0.90x
|
||||
rpmsetcmp cold 129.67 us 113.04 us 0.87x
|
||||
rpmsetcmp warm 2.99 us 1.04 us 0.35x
|
||||
```
|
||||
|
||||
Для разреженного сравнения
|
||||
(`taskset -c 2 python3 benchmark.py --required 1`) получено
|
||||
`83.22 us` на холодном кэше и `0.86 us` на прогретом: соответственно `0.88x`
|
||||
и `1.02x` от времени `set9`.
|
||||
Executable
+304
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import ctypes
|
||||
import gc
|
||||
import os
|
||||
import shutil
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BUILD = HERE / "build"
|
||||
LIBC = ctypes.CDLL(None)
|
||||
LIBC.free.argtypes = [ctypes.c_void_p]
|
||||
TIME_COMMAND = os.environ.get("TIME_COMMAND") or shutil.which("time") or "/usr/bin/time"
|
||||
TIME_FORMAT = "%U\t%S"
|
||||
|
||||
|
||||
class SetAPI:
|
||||
def __init__(self, path: Path):
|
||||
self.lib = ctypes.CDLL(str(path))
|
||||
self.lib.set_new.restype = ctypes.c_void_p
|
||||
self.lib.set_add.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||||
self.lib.set_fini.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
self.lib.set_fini.restype = ctypes.c_void_p
|
||||
self.lib.set_free.argtypes = [ctypes.c_void_p]
|
||||
self.lib.set_free.restype = ctypes.c_void_p
|
||||
self.lib.rpmsetcmp.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
||||
self.lib.rpmsetcmp.restype = ctypes.c_int
|
||||
|
||||
def new_with_symbols(self, symbols):
|
||||
value = self.lib.set_new()
|
||||
if not value:
|
||||
raise RuntimeError("set_new returned NULL")
|
||||
for symbol in symbols:
|
||||
self.lib.set_add(value, symbol)
|
||||
return value
|
||||
|
||||
def release(self, value, result):
|
||||
if result:
|
||||
LIBC.free(result)
|
||||
self.lib.set_free(value)
|
||||
|
||||
def encode(self, symbols, bpp):
|
||||
value = self.new_with_symbols(symbols)
|
||||
result = self.lib.set_fini(value, bpp)
|
||||
if not result:
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
encoded = ctypes.string_at(result)
|
||||
self.release(value, result)
|
||||
return encoded
|
||||
|
||||
|
||||
def make_symbols(args):
|
||||
symbols = tuple(
|
||||
f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols)
|
||||
)
|
||||
required = (
|
||||
symbols[::2]
|
||||
if args.required is None
|
||||
else tuple(symbols[i * args.symbols // args.required] for i in range(args.required))
|
||||
)
|
||||
return symbols, required
|
||||
|
||||
|
||||
def api_for(name):
|
||||
if name == "set9":
|
||||
return SetAPI(BUILD / "libset9.so")
|
||||
if name == "direct":
|
||||
return SetAPI(BUILD / "libdirect-hash.so")
|
||||
raise ValueError(f"unknown implementation: {name}")
|
||||
|
||||
|
||||
def child_main(args):
|
||||
symbols, required = make_symbols(args)
|
||||
api = api_for(args.time_child_impl)
|
||||
checksum = 0
|
||||
|
||||
if args.time_child_operation == "fini":
|
||||
# Measured by /usr/bin/time around this child process. The repeated work
|
||||
# is set construction plus set_fini; keeping construction in the same
|
||||
# child avoids Python-side subsection timers while still reporting CPU
|
||||
# user/system time from the external time utility.
|
||||
for _ in range(args.fini_calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
result = api.lib.set_fini(value, args.bpp)
|
||||
if not result:
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
checksum ^= len(ctypes.string_at(result))
|
||||
api.release(value, result)
|
||||
elif args.time_child_operation == "add":
|
||||
for _ in range(args.fini_calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
checksum ^= int(value)
|
||||
api.lib.set_free(value)
|
||||
elif args.time_child_operation == "build":
|
||||
for _ in range(args.fini_calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
result = api.lib.set_fini(value, args.bpp)
|
||||
if not result:
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
checksum ^= len(ctypes.string_at(result))
|
||||
api.release(value, result)
|
||||
elif args.time_child_operation == "cmp-cold":
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
for _ in range(args.cold_calls):
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
result = api.lib.rpmsetcmp(provider, requirement)
|
||||
os._exit(0 if result == 1 else 1)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
if status != 0:
|
||||
raise RuntimeError(f"cold rpmsetcmp child failed: status={status}")
|
||||
checksum += 1
|
||||
elif args.time_child_operation == "cmp-warm":
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
expected = api.lib.rpmsetcmp(provider, requirement)
|
||||
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
|
||||
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
|
||||
for _ in range(100):
|
||||
api.lib.rpmsetcmp(provider, requirement)
|
||||
for _ in range(args.cmp_calls):
|
||||
checksum += api.lib.rpmsetcmp(provider, requirement)
|
||||
if checksum != args.cmp_calls:
|
||||
raise RuntimeError("rpmsetcmp result changed during benchmark")
|
||||
else:
|
||||
raise RuntimeError(f"unknown timed operation: {args.time_child_operation}")
|
||||
|
||||
# Keep a small observable side effect so timed loops are not optimized away
|
||||
# inside the C library or by future wrappers.
|
||||
print(checksum, file=sys.stderr)
|
||||
|
||||
|
||||
def measure_with_time(args, implementation, operation, calls):
|
||||
samples = []
|
||||
for _ in range(args.rounds):
|
||||
with tempfile.NamedTemporaryFile(prefix="arsv-time-", delete=False) as handle:
|
||||
time_path = Path(handle.name)
|
||||
command = [
|
||||
TIME_COMMAND,
|
||||
"-f",
|
||||
TIME_FORMAT,
|
||||
"-o",
|
||||
str(time_path),
|
||||
"--",
|
||||
sys.executable,
|
||||
str(Path(__file__).resolve()),
|
||||
"--skip-build",
|
||||
"--time-child",
|
||||
"--time-child-impl",
|
||||
implementation,
|
||||
"--time-child-operation",
|
||||
operation,
|
||||
"--symbols",
|
||||
str(args.symbols),
|
||||
"--bpp",
|
||||
str(args.bpp),
|
||||
"--rounds",
|
||||
"1",
|
||||
"--fini-calls",
|
||||
str(args.fini_calls),
|
||||
"--cmp-calls",
|
||||
str(args.cmp_calls),
|
||||
"--cold-calls",
|
||||
str(args.cold_calls),
|
||||
]
|
||||
if args.required is not None:
|
||||
command.extend(["--required", str(args.required)])
|
||||
try:
|
||||
subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
user_text, system_text = time_path.read_text().split()
|
||||
finally:
|
||||
time_path.unlink(missing_ok=True)
|
||||
samples.append((float(user_text) / calls, float(system_text) / calls))
|
||||
users = [sample[0] for sample in samples]
|
||||
systems = [sample[1] for sample in samples]
|
||||
return statistics.median(users), statistics.median(systems)
|
||||
|
||||
|
||||
def verify_complete_decoding(api, provider, requirement):
|
||||
if api.lib.rpmsetcmp(provider, requirement) != 1:
|
||||
raise RuntimeError("provider must contain requirement")
|
||||
if api.lib.rpmsetcmp(requirement, provider) != -1:
|
||||
raise RuntimeError("requirement must be contained by provider")
|
||||
|
||||
payload_position = 4 + (len(provider) - 5) * 3 // 4
|
||||
corrupted = provider[:payload_position] + b"!" + provider[payload_position + 1 :]
|
||||
if api.lib.rpmsetcmp(corrupted, requirement) != -3:
|
||||
raise RuntimeError("first operand was not decoded and validated completely")
|
||||
if api.lib.rpmsetcmp(requirement, corrupted) != -4:
|
||||
raise RuntimeError("second operand was not decoded and validated completely")
|
||||
|
||||
|
||||
def format_cpu_time(seconds):
|
||||
return f"{seconds * 1_000_000:.2f} us"
|
||||
|
||||
|
||||
def format_ratio(new, old):
|
||||
if old == 0:
|
||||
return "n/a"
|
||||
return f"{new / old:.2f}x"
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(description="Compare set9 and direct-hash set APIs")
|
||||
parser.add_argument("--symbols", type=int, default=1000)
|
||||
parser.add_argument(
|
||||
"--required",
|
||||
type=int,
|
||||
help="number of evenly distributed required symbols (default: every second symbol)",
|
||||
)
|
||||
parser.add_argument("--bpp", type=int, default=32)
|
||||
parser.add_argument("--rounds", type=int, default=7)
|
||||
parser.add_argument("--fini-calls", type=int, default=5)
|
||||
parser.add_argument("--cmp-calls", type=int, default=2000)
|
||||
parser.add_argument("--cold-calls", type=int, default=20)
|
||||
parser.add_argument("--skip-build", action="store_true")
|
||||
parser.add_argument("--time-child", action="store_true", help=argparse.SUPPRESS)
|
||||
parser.add_argument("--time-child-impl", choices=("set9", "direct"), help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--time-child-operation",
|
||||
choices=("fini", "add", "build", "cmp-cold", "cmp-warm"),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
if args.symbols < 2 or not 10 <= args.bpp <= 32:
|
||||
parser.error("symbols must be >= 2 and bpp must be in 10..32")
|
||||
if args.required is not None and not 1 <= args.required < args.symbols:
|
||||
parser.error("required must be in 1..symbols-1")
|
||||
if min(args.rounds, args.fini_calls, args.cmp_calls, args.cold_calls) < 1:
|
||||
parser.error("rounds and call counts must be positive")
|
||||
if args.time_child and not (args.time_child_impl and args.time_child_operation):
|
||||
parser.error("--time-child requires --time-child-impl and --time-child-operation")
|
||||
|
||||
if not args.skip_build:
|
||||
subprocess.run([str(HERE / "build.sh")], check=True)
|
||||
|
||||
if args.time_child:
|
||||
child_main(args)
|
||||
return
|
||||
|
||||
if not Path(TIME_COMMAND).is_file():
|
||||
raise RuntimeError(f"time executable not found: {TIME_COMMAND}")
|
||||
|
||||
symbols, required = make_symbols(args)
|
||||
apis = {
|
||||
"set9": api_for("set9"),
|
||||
"direct": api_for("direct"),
|
||||
}
|
||||
|
||||
gc.disable()
|
||||
try:
|
||||
lengths = {}
|
||||
encoded = {}
|
||||
for name, api in apis.items():
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
encoded[name] = (provider, requirement)
|
||||
wire_format = "D1/base64" if provider.startswith(b"D1") else "golomb/base62"
|
||||
lengths[name] = (len(provider), wire_format)
|
||||
|
||||
for name, api in apis.items():
|
||||
verify_complete_decoding(api, *encoded[name])
|
||||
|
||||
operations = (
|
||||
("set_fini child", "fini", args.fini_calls),
|
||||
("new+add child", "add", args.fini_calls),
|
||||
("new+add+fini child", "build", args.fini_calls),
|
||||
("rpmsetcmp cold", "cmp-cold", args.cold_calls),
|
||||
("rpmsetcmp warm", "cmp-warm", args.cmp_calls),
|
||||
)
|
||||
timings = {name: [] for name in apis}
|
||||
for label, operation, calls in operations:
|
||||
for name in apis:
|
||||
timings[name].append(measure_with_time(args, name, operation, calls))
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
print(f"symbols={args.symbols} required={len(required)} bpp={args.bpp}")
|
||||
print("implementation set_chars format")
|
||||
for name in apis:
|
||||
print(f"{name:<14} {lengths[name][0]:>9} {lengths[name][1]}")
|
||||
print("\noperation set9_user set9_sys direct_user direct_sys user_ratio sys_ratio")
|
||||
for index, (label, _operation, _calls) in enumerate(operations):
|
||||
old_user, old_sys = timings["set9"][index]
|
||||
new_user, new_sys = timings["direct"][index]
|
||||
print(
|
||||
f"{label:<22} {format_cpu_time(old_user):>10} {format_cpu_time(old_sys):>9} "
|
||||
f"{format_cpu_time(new_user):>11} {format_cpu_time(new_sys):>10} "
|
||||
f"{format_ratio(new_user, old_user):>10} {format_ratio(new_sys, old_sys):>9}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/../.." && pwd)
|
||||
BUILD="$HERE/build"
|
||||
mkdir -p "$BUILD"
|
||||
touch "$BUILD/rpmlib.h" "$BUILD/system.h"
|
||||
cp "$HERE/../roaring_bitmap/set.h" "$BUILD/set.h"
|
||||
|
||||
CFLAGS=(-O2 -std=gnu11 -D_GNU_SOURCE -Wall -Wextra -Werror -I"$BUILD")
|
||||
COMPAT=(-include "$ROOT/scripts/rpmsetcmp/newset_compat.h")
|
||||
|
||||
for tool in mkset setcmp; do
|
||||
cc "${CFLAGS[@]}" "${COMPAT[@]}" \
|
||||
"$ROOT/reimplement/set9.c" "$ROOT/scripts/rpmsetcmp/$tool.c" \
|
||||
-o "$BUILD/$tool-set9"
|
||||
cc "${CFLAGS[@]}" "${COMPAT[@]}" \
|
||||
"$HERE/hash_set.c" "$ROOT/scripts/rpmsetcmp/$tool.c" \
|
||||
-o "$BUILD/$tool-direct"
|
||||
done
|
||||
|
||||
cc "${CFLAGS[@]}" -fPIC -shared "${COMPAT[@]}" \
|
||||
"$ROOT/reimplement/set9.c" -o "$BUILD/libset9.so"
|
||||
cc "${CFLAGS[@]}" -fPIC -shared "${COMPAT[@]}" \
|
||||
"$HERE/hash_set.c" -o "$BUILD/libdirect-hash.so"
|
||||
|
||||
printf 'Built tools and benchmark libraries in %s\n' "$BUILD"
|
||||
@@ -0,0 +1,968 @@
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "rpmlib.h"
|
||||
#include "set.h"
|
||||
#include "system.h"
|
||||
|
||||
/*
|
||||
* This is intentionally a new set-string format. It is not compatible with
|
||||
* the Golomb-Rice/base62 strings produced by the original lib/set.c.
|
||||
*
|
||||
* D1<two decimal bpp digits><unpadded RFC 4648 base64 of packed hashes>
|
||||
*
|
||||
* Sorted unique hashes are packed least-significant bit first, using exactly
|
||||
* bpp bits per hash. Base64 is only a textual representation of those bytes;
|
||||
* there is no delta or Golomb-Rice coding.
|
||||
*/
|
||||
#define FORMAT_PREFIX "D1"
|
||||
#define FORMAT_HEADER_LEN 4
|
||||
|
||||
_Static_assert(CHAR_BIT == 8, "direct-hash format requires 8-bit bytes");
|
||||
|
||||
struct set {
|
||||
size_t cnt;
|
||||
size_t symbols_cap;
|
||||
size_t strings_len;
|
||||
size_t strings_cap;
|
||||
char* strings;
|
||||
struct symbols {
|
||||
size_t offset;
|
||||
unsigned full_hash;
|
||||
unsigned hash;
|
||||
}* symbols_v;
|
||||
};
|
||||
|
||||
struct decoded_set {
|
||||
unsigned* hashes;
|
||||
size_t count;
|
||||
unsigned bpp;
|
||||
};
|
||||
|
||||
enum {
|
||||
DECODED_CACHE_SIZE = 512,
|
||||
DECODED_CACHE_BUCKETS = 1024,
|
||||
PAIR_CACHE_SIZE = 16,
|
||||
};
|
||||
|
||||
struct pair_cache_entry {
|
||||
uint64_t other_identity;
|
||||
int result;
|
||||
};
|
||||
|
||||
struct decoded_cache_entry {
|
||||
struct decoded_cache_entry* bucket_next;
|
||||
struct decoded_cache_entry* newer;
|
||||
struct decoded_cache_entry* older;
|
||||
char* str;
|
||||
unsigned* hashes;
|
||||
size_t len;
|
||||
size_t count;
|
||||
uint64_t identity;
|
||||
uint32_t fingerprint;
|
||||
unsigned bucket;
|
||||
unsigned target_bpp;
|
||||
unsigned pair_next;
|
||||
struct pair_cache_entry pairs[PAIR_CACHE_SIZE];
|
||||
};
|
||||
|
||||
struct set_meta {
|
||||
const char* str;
|
||||
size_t len;
|
||||
unsigned bpp;
|
||||
};
|
||||
|
||||
static unsigned decoded_cache_count[2];
|
||||
static struct decoded_cache_entry* decoded_cache_buckets[2][DECODED_CACHE_BUCKETS];
|
||||
static struct decoded_cache_entry* decoded_cache_newest[2];
|
||||
static struct decoded_cache_entry* decoded_cache_oldest[2];
|
||||
static uint64_t decoded_cache_next_identity = 1;
|
||||
|
||||
static unsigned hash(const char* str);
|
||||
|
||||
struct set* set_new(void) {
|
||||
struct set* set = xmalloc(sizeof(*set));
|
||||
set->cnt = 0;
|
||||
set->symbols_cap = 0;
|
||||
set->strings_len = 0;
|
||||
set->strings_cap = 0;
|
||||
set->strings = NULL;
|
||||
set->symbols_v = NULL;
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
void set_add(struct set* set, const char* sym) {
|
||||
if (set->cnt == set->symbols_cap) {
|
||||
set->symbols_cap += 1024;
|
||||
set->symbols_v = xrealloc(set->symbols_v, sizeof(*set->symbols_v) * set->symbols_cap);
|
||||
}
|
||||
|
||||
size_t length = strlen(sym) + 1;
|
||||
size_t required = set->strings_len + length;
|
||||
if (required > set->strings_cap) {
|
||||
size_t capacity = set->strings_cap ? set->strings_cap : 4096;
|
||||
while (capacity < required) capacity *= 2;
|
||||
set->strings = xrealloc(set->strings, capacity);
|
||||
set->strings_cap = capacity;
|
||||
}
|
||||
|
||||
set->symbols_v[set->cnt].offset = set->strings_len;
|
||||
set->symbols_v[set->cnt].full_hash = hash(sym);
|
||||
set->symbols_v[set->cnt].hash = set->symbols_v[set->cnt].full_hash;
|
||||
memcpy(set->strings + set->strings_len, sym, length);
|
||||
set->strings_len = required;
|
||||
++set->cnt;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct set* set_free(struct set* set) {
|
||||
if (set) {
|
||||
_free(set->strings);
|
||||
_free(set->symbols_v);
|
||||
set = _free(set);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static unsigned hash(const char* str) {
|
||||
unsigned hash = UINT32_C(0x9e3779b9);
|
||||
const unsigned char* p = (const unsigned char*)str;
|
||||
|
||||
while (*p) {
|
||||
hash += *p++;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
}
|
||||
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 11;
|
||||
hash += hash << 15;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
static int compare_symbols(const void* arg1, const void* arg2) {
|
||||
const struct symbols* s1 = arg1;
|
||||
const struct symbols* s2 = arg2;
|
||||
|
||||
if (s1->hash > s2->hash) return 1;
|
||||
if (s1->hash < s2->hash) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sort_symbols(struct symbols* values, size_t count, unsigned bpp) {
|
||||
if (count < 128) {
|
||||
qsort(values, count, sizeof(*values), compare_symbols);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
struct symbols* temporary = xmalloc(count * sizeof(*temporary));
|
||||
struct symbols* source = values;
|
||||
struct symbols* destination = temporary;
|
||||
unsigned passes = (bpp + 7) / 8;
|
||||
|
||||
for (unsigned pass = 0; pass < passes; ++pass) {
|
||||
size_t offsets[256] = {0};
|
||||
unsigned shift = pass * 8;
|
||||
for (size_t i = 0; i < count; ++i) ++offsets[(source[i].hash >> shift) & 0xffu];
|
||||
|
||||
size_t position = 0;
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
size_t bucket_count = offsets[i];
|
||||
offsets[i] = position;
|
||||
position += bucket_count;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
unsigned bucket = (source[i].hash >> shift) & 0xffu;
|
||||
destination[offsets[bucket]++] = source[i];
|
||||
}
|
||||
|
||||
struct symbols* swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
}
|
||||
|
||||
if (source != values) memcpy(values, source, count * sizeof(*values));
|
||||
_free(temporary);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static const char base64_alphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static size_t base64_encoded_size(size_t byte_count) {
|
||||
if (byte_count > SIZE_MAX - 2) abort();
|
||||
size_t groups = (byte_count + 2) / 3;
|
||||
if (groups > (SIZE_MAX - FORMAT_HEADER_LEN - 1) / 4) abort();
|
||||
|
||||
size_t size = groups * 4;
|
||||
size_t remainder = byte_count % 3;
|
||||
if (remainder != 0) size -= 3 - remainder;
|
||||
return size;
|
||||
}
|
||||
|
||||
static void base64_encode(const unsigned char* input, size_t input_len, char* output) {
|
||||
while (input_len >= 3) {
|
||||
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8) | input[2];
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = base64_alphabet[(value >> 6) & 0x3f];
|
||||
output[3] = base64_alphabet[value & 0x3f];
|
||||
input += 3;
|
||||
input_len -= 3;
|
||||
output += 4;
|
||||
}
|
||||
|
||||
if (input_len == 1) {
|
||||
uint32_t value = (uint32_t)input[0] << 16;
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
} else if (input_len == 2) {
|
||||
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8);
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = base64_alphabet[(value >> 6) & 0x3f];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static size_t compact_unique_hashes(struct symbols* symbols, size_t count) {
|
||||
size_t unique_count = 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
while (i + 1 < count && symbols[i].hash == symbols[i + 1].hash) ++i;
|
||||
symbols[unique_count++].hash = symbols[i].hash;
|
||||
}
|
||||
|
||||
return unique_count;
|
||||
}
|
||||
|
||||
static unsigned char* pack_symbol_hashes(const struct symbols* symbols, size_t count,
|
||||
unsigned bpp, size_t* byte_count) {
|
||||
if (count > (SIZE_MAX - 7) / bpp) abort();
|
||||
size_t bit_count = count * bpp;
|
||||
*byte_count = (bit_count + 7) / 8;
|
||||
unsigned char* bytes = xmalloc(*byte_count);
|
||||
unsigned char* output = bytes;
|
||||
|
||||
#if UINT_MAX == UINT32_MAX && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \
|
||||
__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
if (bpp == 32 && sizeof(unsigned) == 4) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
unsigned hash = symbols[i].hash;
|
||||
memcpy(output, &hash, sizeof(hash));
|
||||
output += sizeof(hash);
|
||||
}
|
||||
assert((size_t)(output - bytes) == *byte_count);
|
||||
return bytes;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint64_t bits = 0;
|
||||
unsigned filled = 0;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
bits |= (uint64_t)symbols[i].hash << filled;
|
||||
filled += bpp;
|
||||
|
||||
while (filled >= 8) {
|
||||
*output++ = (unsigned char)bits;
|
||||
bits >>= 8;
|
||||
filled -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (filled) *output++ = (unsigned char)bits;
|
||||
assert((size_t)(output - bytes) == *byte_count);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const char* set_fini(struct set* set, int bpp) {
|
||||
assert(set != NULL);
|
||||
assert(set->cnt > 0);
|
||||
assert(bpp >= 10 && bpp <= 32);
|
||||
|
||||
unsigned mask = bpp < 32 ? (UINT32_C(1) << bpp) - 1 : UINT32_MAX;
|
||||
for (size_t i = 0; i < set->cnt; ++i) {
|
||||
set->symbols_v[i].hash = set->symbols_v[i].full_hash & mask;
|
||||
}
|
||||
sort_symbols(set->symbols_v, set->cnt, (unsigned)bpp);
|
||||
|
||||
for (size_t i = 0; i + 1 < set->cnt; ++i) {
|
||||
if (set->symbols_v[i].hash != set->symbols_v[i + 1].hash) continue;
|
||||
const char* left = set->strings + set->symbols_v[i].offset;
|
||||
const char* right = set->strings + set->symbols_v[i + 1].offset;
|
||||
if (strcmp(left, right) != 0) fprintf(stderr, "warning: hash collision: %s %s\n", left, right);
|
||||
}
|
||||
|
||||
size_t byte_count;
|
||||
size_t unique_count = compact_unique_hashes(set->symbols_v, set->cnt);
|
||||
unsigned char* bytes = pack_symbol_hashes(set->symbols_v, unique_count, (unsigned)bpp, &byte_count);
|
||||
size_t payload_len = base64_encoded_size(byte_count);
|
||||
char* output = xmalloc(FORMAT_HEADER_LEN + payload_len + 1);
|
||||
memcpy(output, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1);
|
||||
output[2] = (char)('0' + bpp / 10);
|
||||
output[3] = (char)('0' + bpp % 10);
|
||||
base64_encode(bytes, byte_count, output + FORMAT_HEADER_LEN);
|
||||
output[FORMAT_HEADER_LEN + payload_len] = '\0';
|
||||
|
||||
_free(bytes);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static const unsigned char base64_values[256] = {
|
||||
['A'] = 1, ['B'] = 2, ['C'] = 3, ['D'] = 4, ['E'] = 5, ['F'] = 6, ['G'] = 7, ['H'] = 8,
|
||||
['I'] = 9, ['J'] = 10, ['K'] = 11, ['L'] = 12, ['M'] = 13, ['N'] = 14, ['O'] = 15, ['P'] = 16,
|
||||
['Q'] = 17, ['R'] = 18, ['S'] = 19, ['T'] = 20, ['U'] = 21, ['V'] = 22, ['W'] = 23, ['X'] = 24,
|
||||
['Y'] = 25, ['Z'] = 26, ['a'] = 27, ['b'] = 28, ['c'] = 29, ['d'] = 30, ['e'] = 31, ['f'] = 32,
|
||||
['g'] = 33, ['h'] = 34, ['i'] = 35, ['j'] = 36, ['k'] = 37, ['l'] = 38, ['m'] = 39, ['n'] = 40,
|
||||
['o'] = 41, ['p'] = 42, ['q'] = 43, ['r'] = 44, ['s'] = 45, ['t'] = 46, ['u'] = 47, ['v'] = 48,
|
||||
['w'] = 49, ['x'] = 50, ['y'] = 51, ['z'] = 52, ['0'] = 53, ['1'] = 54, ['2'] = 55, ['3'] = 56,
|
||||
['4'] = 57, ['5'] = 58, ['6'] = 59, ['7'] = 60, ['8'] = 61, ['9'] = 62, ['+'] = 63, ['/'] = 64,
|
||||
};
|
||||
|
||||
static inline int base64_value(unsigned char c) { return (int)base64_values[c] - 1; }
|
||||
|
||||
static int base64_decoded_size(size_t input_len, size_t* byte_count) {
|
||||
size_t remainder = input_len % 4;
|
||||
if (input_len == 0 || remainder == 1) return -1;
|
||||
|
||||
size_t groups = input_len / 4;
|
||||
if (groups > SIZE_MAX / 3) return -1;
|
||||
size_t size = groups * 3;
|
||||
size_t tail_size = remainder == 0 ? 0 : remainder - 1;
|
||||
if (size > SIZE_MAX - tail_size) return -1;
|
||||
|
||||
*byte_count = size + tail_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int has_set_prefix(const char* str) {
|
||||
return str[0] == 's' && str[1] == 'e' && str[2] == 't' && str[3] == ':';
|
||||
}
|
||||
|
||||
static int set_meta_init(const char* source, struct set_meta* meta) {
|
||||
const char* str = source;
|
||||
if (has_set_prefix(str)) str += 4;
|
||||
if (has_set_prefix(str)) return -1;
|
||||
|
||||
/* With bpp >= 10, a valid direct set has at least three Base64 characters. */
|
||||
if (str[0] != FORMAT_PREFIX[0] || str[1] != FORMAT_PREFIX[1]) return -1;
|
||||
if (str[2] < '0' || str[2] > '9' || str[3] < '0' || str[3] > '9') return -1;
|
||||
if (str[4] == '\0' || str[5] == '\0' || str[6] == '\0') return -1;
|
||||
|
||||
unsigned bpp = (unsigned)(str[2] - '0') * 10 + (unsigned)(str[3] - '0');
|
||||
if (bpp < 10 || bpp > 32) return -1;
|
||||
|
||||
meta->str = str;
|
||||
meta->len = strlen(str);
|
||||
meta->bpp = bpp;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct decode_writer {
|
||||
unsigned* hashes;
|
||||
size_t capacity;
|
||||
size_t written;
|
||||
uint64_t bits;
|
||||
uint64_t mask;
|
||||
unsigned previous;
|
||||
unsigned filled;
|
||||
unsigned bpp;
|
||||
int has_previous;
|
||||
};
|
||||
|
||||
static inline int decode_writer_put(struct decode_writer* writer, uint32_t bytes,
|
||||
unsigned byte_count) {
|
||||
writer->bits |= (uint64_t)bytes << writer->filled;
|
||||
writer->filled += byte_count * 8;
|
||||
|
||||
while (writer->filled >= writer->bpp) {
|
||||
if (writer->written == writer->capacity) return -1;
|
||||
|
||||
unsigned current = (unsigned)(writer->bits & writer->mask);
|
||||
writer->bits >>= writer->bpp;
|
||||
writer->filled -= writer->bpp;
|
||||
|
||||
if (writer->has_previous && writer->previous >= current) return -1;
|
||||
if (writer->hashes) writer->hashes[writer->written] = current;
|
||||
writer->previous = current;
|
||||
writer->has_previous = 1;
|
||||
++writer->written;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_base64_bytes(const char* input, size_t input_len, unsigned char* output,
|
||||
size_t output_len) {
|
||||
unsigned char* const output_end = output + output_len;
|
||||
size_t offset = 0;
|
||||
while (input_len - offset >= 4) {
|
||||
int v0 = base64_value((unsigned char)input[offset]);
|
||||
int v1 = base64_value((unsigned char)input[offset + 1]);
|
||||
int v2 = base64_value((unsigned char)input[offset + 2]);
|
||||
int v3 = base64_value((unsigned char)input[offset + 3]);
|
||||
if ((v0 | v1 | v2 | v3) < 0 || output_end - output < 3) return -1;
|
||||
output[0] = (unsigned char)((v0 << 2) | (v1 >> 4));
|
||||
output[1] = (unsigned char)(((v1 & 0x0f) << 4) | (v2 >> 2));
|
||||
output[2] = (unsigned char)(((v2 & 0x03) << 6) | v3);
|
||||
output += 3;
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
size_t remainder = input_len - offset;
|
||||
if (remainder == 0) return output == output_end ? 0 : -1;
|
||||
if (remainder == 1) return -1;
|
||||
|
||||
int v0 = base64_value((unsigned char)input[offset]);
|
||||
int v1 = base64_value((unsigned char)input[offset + 1]);
|
||||
if ((v0 | v1) < 0 || output == output_end) return -1;
|
||||
*output++ = (unsigned char)((v0 << 2) | (v1 >> 4));
|
||||
|
||||
if (remainder == 2) return (v1 & 0x0f) == 0 && output == output_end ? 0 : -1;
|
||||
|
||||
int v2 = base64_value((unsigned char)input[offset + 2]);
|
||||
if (v2 < 0 || output == output_end) return -1;
|
||||
*output++ = (unsigned char)(((v1 & 0x0f) << 4) | (v2 >> 2));
|
||||
return (v2 & 0x03) == 0 && output == output_end ? 0 : -1;
|
||||
}
|
||||
|
||||
static inline int decode_base64_triplet(const char* input, uint32_t* triplet) {
|
||||
int v0 = base64_value((unsigned char)input[0]);
|
||||
int v1 = base64_value((unsigned char)input[1]);
|
||||
int v2 = base64_value((unsigned char)input[2]);
|
||||
int v3 = base64_value((unsigned char)input[3]);
|
||||
if ((v0 | v1 | v2 | v3) < 0) return -1;
|
||||
|
||||
*triplet = (uint32_t)((v0 << 2) | (v1 >> 4)) |
|
||||
(uint32_t)(((v1 & 0x0f) << 4) | (v2 >> 2)) << 8 |
|
||||
(uint32_t)(((v2 & 0x03) << 6) | v3) << 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_base64_u32(const char* input, size_t input_len, unsigned* hashes,
|
||||
size_t count) {
|
||||
size_t blocks = count / 3;
|
||||
size_t written = 0;
|
||||
unsigned previous = 0;
|
||||
int has_previous = 0;
|
||||
|
||||
for (size_t block = 0; block < blocks; ++block) {
|
||||
uint32_t t0, t1, t2, t3;
|
||||
if (decode_base64_triplet(input, &t0) < 0 ||
|
||||
decode_base64_triplet(input + 4, &t1) < 0 ||
|
||||
decode_base64_triplet(input + 8, &t2) < 0 ||
|
||||
decode_base64_triplet(input + 12, &t3) < 0)
|
||||
return -1;
|
||||
|
||||
unsigned value0 = (unsigned)(t0 | ((t1 & UINT32_C(0xff)) << 24));
|
||||
unsigned value1 = (unsigned)((t1 >> 8) | ((t2 & UINT32_C(0xffff)) << 16));
|
||||
unsigned value2 = (unsigned)((t2 >> 16) | (t3 << 8));
|
||||
if ((has_previous && previous >= value0) || value0 >= value1 || value1 >= value2) return -1;
|
||||
|
||||
hashes[written++] = value0;
|
||||
hashes[written++] = value1;
|
||||
hashes[written++] = value2;
|
||||
previous = value2;
|
||||
has_previous = 1;
|
||||
input += 16;
|
||||
input_len -= 16;
|
||||
}
|
||||
|
||||
size_t remaining = count - written;
|
||||
if (remaining != 0) {
|
||||
size_t byte_count = remaining * sizeof(*hashes);
|
||||
unsigned char tail[2 * sizeof(*hashes)];
|
||||
if (decode_base64_bytes(input, input_len, tail, byte_count) < 0) return -1;
|
||||
for (size_t i = 0; i < remaining; ++i, ++written) {
|
||||
const unsigned char* bytes = tail + i * 4;
|
||||
unsigned current = (unsigned)bytes[0] | ((unsigned)bytes[1] << 8) |
|
||||
((unsigned)bytes[2] << 16) | ((unsigned)bytes[3] << 24);
|
||||
if (has_previous && previous >= current) return -1;
|
||||
hashes[written] = current;
|
||||
previous = current;
|
||||
has_previous = 1;
|
||||
}
|
||||
} else if (input_len != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int decode_set_sized(const char* str, size_t str_len, struct decoded_set* decoded) {
|
||||
if (str_len == 0) str_len = strlen(str);
|
||||
if (str_len <= FORMAT_HEADER_LEN) return -1;
|
||||
if (strncmp(str, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1) != 0) return -1;
|
||||
if (str[2] < '0' || str[2] > '9' || str[3] < '0' || str[3] > '9') return -1;
|
||||
|
||||
unsigned bpp = (unsigned)(str[2] - '0') * 10 + (unsigned)(str[3] - '0');
|
||||
if (bpp < 10 || bpp > 32) return -1;
|
||||
|
||||
const char* input = str + FORMAT_HEADER_LEN;
|
||||
size_t input_len = str_len - FORMAT_HEADER_LEN;
|
||||
size_t byte_count;
|
||||
if (base64_decoded_size(input_len, &byte_count) < 0) return -1;
|
||||
if (byte_count > SIZE_MAX / 8) return -1;
|
||||
|
||||
size_t count = byte_count * 8 / bpp;
|
||||
if (count == 0 || count > (SIZE_MAX - 7) / bpp || count > SIZE_MAX / sizeof(unsigned) ||
|
||||
(count * bpp + 7) / 8 != byte_count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned* hashes = xmalloc(count * sizeof(*hashes));
|
||||
#if UINT_MAX == UINT32_MAX
|
||||
if (bpp == 32 && sizeof(unsigned) == 4) {
|
||||
if (decode_base64_u32(input, input_len, hashes, count) < 0) goto invalid;
|
||||
if (decoded) {
|
||||
decoded->hashes = hashes;
|
||||
decoded->count = count;
|
||||
decoded->bpp = bpp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Byte-aligned widths can bypass the generic bit reservoir. Decode the
|
||||
* Base64 payload into the front of the final allocation, then expand 16-
|
||||
* and 24-bit values backwards so unread packed bytes are never overwritten. */
|
||||
if ((bpp == 16 || bpp == 24) && sizeof(unsigned) == 4) {
|
||||
if (decode_base64_bytes(input, input_len, (unsigned char*)hashes, byte_count) < 0) goto invalid;
|
||||
if (bpp == 16) {
|
||||
for (size_t i = count; i > 0; --i) {
|
||||
const unsigned char* bytes = (const unsigned char*)hashes + (i - 1) * 2;
|
||||
hashes[i - 1] = (unsigned)bytes[0] | ((unsigned)bytes[1] << 8);
|
||||
}
|
||||
} else if (bpp == 24) {
|
||||
for (size_t i = count; i > 0; --i) {
|
||||
const unsigned char* bytes = (const unsigned char*)hashes + (i - 1) * 3;
|
||||
hashes[i - 1] = (unsigned)bytes[0] | ((unsigned)bytes[1] << 8) |
|
||||
((unsigned)bytes[2] << 16);
|
||||
}
|
||||
}
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
if (hashes[i - 1] >= hashes[i]) goto invalid;
|
||||
}
|
||||
if (decoded) {
|
||||
decoded->hashes = hashes;
|
||||
decoded->count = count;
|
||||
decoded->bpp = bpp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct decode_writer writer = {
|
||||
.hashes = hashes,
|
||||
.capacity = count,
|
||||
.mask = bpp < 32 ? (UINT64_C(1) << bpp) - 1 : UINT32_MAX,
|
||||
.bpp = bpp,
|
||||
};
|
||||
|
||||
size_t full_len = input_len - input_len % 4;
|
||||
for (size_t offset = 0; offset < full_len; offset += 4) {
|
||||
int v0 = base64_value((unsigned char)input[offset]);
|
||||
int v1 = base64_value((unsigned char)input[offset + 1]);
|
||||
int v2 = base64_value((unsigned char)input[offset + 2]);
|
||||
int v3 = base64_value((unsigned char)input[offset + 3]);
|
||||
if ((v0 | v1 | v2 | v3) < 0) goto invalid;
|
||||
|
||||
uint32_t bytes = (uint32_t)((v0 << 2) | (v1 >> 4)) |
|
||||
(uint32_t)(((v1 & 0x0f) << 4) | (v2 >> 2)) << 8 |
|
||||
(uint32_t)(((v2 & 0x03) << 6) | v3) << 16;
|
||||
if (decode_writer_put(&writer, bytes, 3) < 0) goto invalid;
|
||||
}
|
||||
|
||||
size_t remainder = input_len - full_len;
|
||||
if (remainder != 0) {
|
||||
int v0 = base64_value((unsigned char)input[full_len]);
|
||||
int v1 = base64_value((unsigned char)input[full_len + 1]);
|
||||
if ((v0 | v1) < 0) goto invalid;
|
||||
|
||||
uint32_t bytes = (uint32_t)((v0 << 2) | (v1 >> 4));
|
||||
if (remainder == 2) {
|
||||
if ((v1 & 0x0f) != 0 || decode_writer_put(&writer, bytes, 1) < 0) goto invalid;
|
||||
} else {
|
||||
int v2 = base64_value((unsigned char)input[full_len + 2]);
|
||||
if (v2 < 0 || (v2 & 0x03) != 0) goto invalid;
|
||||
bytes |= (uint32_t)(((v1 & 0x0f) << 4) | (v2 >> 2)) << 8;
|
||||
if (decode_writer_put(&writer, bytes, 2) < 0) goto invalid;
|
||||
}
|
||||
}
|
||||
|
||||
if (writer.written != count || writer.bits != 0) goto invalid;
|
||||
|
||||
if (decoded) {
|
||||
decoded->hashes = hashes;
|
||||
decoded->count = count;
|
||||
decoded->bpp = bpp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
invalid:
|
||||
_free(hashes);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Reduce a sorted set of (bpp + 1)-bit values to a sorted set of bpp-bit values. */
|
||||
static size_t downsample_set(size_t count, const unsigned* hashes, unsigned* result, unsigned bpp) {
|
||||
unsigned mask = (UINT32_C(1) << bpp) - 1;
|
||||
size_t lower = 0;
|
||||
size_t upper = count;
|
||||
|
||||
while (lower < upper) {
|
||||
size_t middle = lower + (upper - lower) / 2;
|
||||
if (hashes[middle] <= mask)
|
||||
lower = middle + 1;
|
||||
else
|
||||
upper = middle;
|
||||
}
|
||||
|
||||
unsigned* output = result;
|
||||
const unsigned* low = hashes;
|
||||
const unsigned* low_end = hashes + lower;
|
||||
const unsigned* high = hashes + lower;
|
||||
const unsigned* high_end = hashes + count;
|
||||
|
||||
while (low < low_end && high < high_end) {
|
||||
unsigned low_value = *low;
|
||||
unsigned high_value = *high & mask;
|
||||
if (low_value < high_value) {
|
||||
*output++ = low_value;
|
||||
++low;
|
||||
} else if (high_value < low_value) {
|
||||
*output++ = high_value;
|
||||
++high;
|
||||
} else {
|
||||
*output++ = low_value;
|
||||
++low;
|
||||
++high;
|
||||
}
|
||||
}
|
||||
while (low < low_end) *output++ = *low++;
|
||||
while (high < high_end) *output++ = *high++ & mask;
|
||||
|
||||
return (size_t)(output - result);
|
||||
}
|
||||
|
||||
static void downsample_radix_to(struct decoded_set* set, unsigned target_bpp) {
|
||||
unsigned* original = set->hashes;
|
||||
unsigned* scratch = xmalloc(set->count * sizeof(*scratch));
|
||||
unsigned mask = (UINT32_C(1) << target_bpp) - 1;
|
||||
for (size_t i = 0; i < set->count; ++i) original[i] &= mask;
|
||||
|
||||
unsigned* source = original;
|
||||
unsigned* destination = scratch;
|
||||
unsigned passes = (target_bpp + 7) / 8;
|
||||
for (unsigned pass = 0; pass < passes; ++pass) {
|
||||
size_t offsets[256] = {0};
|
||||
unsigned shift = pass * 8;
|
||||
for (size_t i = 0; i < set->count; ++i) ++offsets[(source[i] >> shift) & 0xffu];
|
||||
|
||||
size_t position = 0;
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
size_t bucket_count = offsets[i];
|
||||
offsets[i] = position;
|
||||
position += bucket_count;
|
||||
}
|
||||
for (size_t i = 0; i < set->count; ++i) {
|
||||
unsigned value = source[i];
|
||||
destination[offsets[(value >> shift) & 0xffu]++] = value;
|
||||
}
|
||||
|
||||
unsigned* swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
}
|
||||
|
||||
size_t unique_count = 1;
|
||||
for (size_t i = 1; i < set->count; ++i) {
|
||||
if (source[i] != source[unique_count - 1]) source[unique_count++] = source[i];
|
||||
}
|
||||
|
||||
if (source == original) {
|
||||
_free(scratch);
|
||||
} else {
|
||||
_free(original);
|
||||
set->hashes = scratch;
|
||||
}
|
||||
set->count = unique_count;
|
||||
set->bpp = target_bpp;
|
||||
}
|
||||
|
||||
static void downsample_to(struct decoded_set* set, unsigned target_bpp) {
|
||||
if (set->bpp == target_bpp) return;
|
||||
|
||||
unsigned passes = (target_bpp + 7) / 8;
|
||||
if (set->bpp - target_bpp > passes) {
|
||||
downsample_radix_to(set, target_bpp);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned* original = set->hashes;
|
||||
unsigned* scratch = xmalloc(set->count * sizeof(*scratch));
|
||||
unsigned* source = original;
|
||||
unsigned* destination = scratch;
|
||||
|
||||
while (set->bpp > target_bpp) {
|
||||
--set->bpp;
|
||||
set->count = downsample_set(set->count, source, destination, set->bpp);
|
||||
unsigned* swap = source;
|
||||
source = destination;
|
||||
destination = swap;
|
||||
}
|
||||
|
||||
if (source == original) {
|
||||
_free(scratch);
|
||||
} else {
|
||||
_free(original);
|
||||
set->hashes = scratch;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t decoded_cache_fingerprint(const struct set_meta* meta, unsigned target_bpp) {
|
||||
const unsigned char* str = (const unsigned char*)meta->str;
|
||||
uint32_t fingerprint = UINT32_C(2166136261);
|
||||
fingerprint = (fingerprint ^ (uint32_t)meta->len) * UINT32_C(16777619);
|
||||
fingerprint = (fingerprint ^ (meta->bpp * UINT32_C(0x27d4eb2d))) * UINT32_C(16777619);
|
||||
fingerprint = (fingerprint ^ (target_bpp * UINT32_C(0x85ebca6b))) * UINT32_C(16777619);
|
||||
|
||||
size_t prefix_len = meta->len < 8 ? meta->len : 8;
|
||||
for (size_t i = 0; i < prefix_len; ++i) {
|
||||
fingerprint = (fingerprint ^ str[i]) * UINT32_C(16777619);
|
||||
}
|
||||
size_t suffix_start = meta->len > 8 ? meta->len - 8 : prefix_len;
|
||||
for (size_t i = suffix_start; i < meta->len; ++i) {
|
||||
fingerprint = (fingerprint ^ str[i]) * UINT32_C(16777619);
|
||||
}
|
||||
|
||||
fingerprint ^= fingerprint >> 16;
|
||||
fingerprint *= UINT32_C(0x7feb352d);
|
||||
fingerprint ^= fingerprint >> 15;
|
||||
fingerprint *= UINT32_C(0x846ca68b);
|
||||
fingerprint ^= fingerprint >> 16;
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
static void decoded_cache_touch(struct decoded_cache_entry* entry, unsigned cache_id) {
|
||||
if (entry == decoded_cache_newest[cache_id]) return;
|
||||
|
||||
if (entry->newer) entry->newer->older = entry->older;
|
||||
if (entry->older) entry->older->newer = entry->newer;
|
||||
if (entry == decoded_cache_oldest[cache_id]) decoded_cache_oldest[cache_id] = entry->newer;
|
||||
|
||||
entry->newer = NULL;
|
||||
entry->older = decoded_cache_newest[cache_id];
|
||||
decoded_cache_newest[cache_id]->newer = entry;
|
||||
decoded_cache_newest[cache_id] = entry;
|
||||
}
|
||||
|
||||
static void decoded_cache_remove(struct decoded_cache_entry* victim, unsigned cache_id) {
|
||||
if (victim->newer)
|
||||
victim->newer->older = victim->older;
|
||||
else
|
||||
decoded_cache_newest[cache_id] = victim->older;
|
||||
if (victim->older)
|
||||
victim->older->newer = victim->newer;
|
||||
else
|
||||
decoded_cache_oldest[cache_id] = victim->newer;
|
||||
|
||||
struct decoded_cache_entry** link = &decoded_cache_buckets[cache_id][victim->bucket];
|
||||
while (*link && *link != victim) link = &(*link)->bucket_next;
|
||||
assert(*link == victim);
|
||||
*link = victim->bucket_next;
|
||||
assert(decoded_cache_count[cache_id] > 0);
|
||||
--decoded_cache_count[cache_id];
|
||||
|
||||
_free(victim->hashes);
|
||||
_free(victim);
|
||||
}
|
||||
|
||||
static int pair_cache_lookup(const struct decoded_cache_entry* first,
|
||||
const struct decoded_cache_entry* second, int* result) {
|
||||
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) {
|
||||
if (first->pairs[i].other_identity == second->identity) {
|
||||
*result = first->pairs[i].result;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void pair_cache_store(struct decoded_cache_entry* first,
|
||||
const struct decoded_cache_entry* second, int result) {
|
||||
unsigned slot = first->pair_next++ & (PAIR_CACHE_SIZE - 1);
|
||||
first->pairs[slot].other_identity = second->identity;
|
||||
first->pairs[slot].result = result;
|
||||
}
|
||||
|
||||
static int cache_decode_set(const struct set_meta* meta, unsigned target_bpp, unsigned cache_id,
|
||||
const unsigned** hashes, size_t* count,
|
||||
struct decoded_cache_entry** cache_entry) {
|
||||
assert(cache_id < 2);
|
||||
assert(target_bpp <= meta->bpp);
|
||||
|
||||
uint32_t fingerprint = decoded_cache_fingerprint(meta, target_bpp);
|
||||
unsigned bucket = fingerprint & (DECODED_CACHE_BUCKETS - 1);
|
||||
for (struct decoded_cache_entry* entry = decoded_cache_buckets[cache_id][bucket]; entry;
|
||||
entry = entry->bucket_next) {
|
||||
if (entry->fingerprint != fingerprint || entry->target_bpp != target_bpp ||
|
||||
entry->len != meta->len || memcmp(entry->str, meta->str, meta->len + 1) != 0)
|
||||
continue;
|
||||
|
||||
decoded_cache_touch(entry, cache_id);
|
||||
*hashes = entry->hashes;
|
||||
*count = entry->count;
|
||||
*cache_entry = entry;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct decoded_set decoded;
|
||||
if (decode_set_sized(meta->str, meta->len, &decoded) < 0) return -1;
|
||||
if (decoded.bpp != meta->bpp) {
|
||||
_free(decoded.hashes);
|
||||
return -1;
|
||||
}
|
||||
downsample_to(&decoded, target_bpp);
|
||||
|
||||
if (meta->len > SIZE_MAX - sizeof(struct decoded_cache_entry) - 1) {
|
||||
_free(decoded.hashes);
|
||||
return -1;
|
||||
}
|
||||
struct decoded_cache_entry* entry = xmalloc(sizeof(*entry) + meta->len + 1);
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->str = (char*)(entry + 1);
|
||||
memcpy(entry->str, meta->str, meta->len + 1);
|
||||
entry->hashes = decoded.hashes;
|
||||
entry->len = meta->len;
|
||||
entry->count = decoded.count;
|
||||
entry->identity = decoded_cache_next_identity++;
|
||||
if (entry->identity == 0) entry->identity = decoded_cache_next_identity++;
|
||||
entry->fingerprint = fingerprint;
|
||||
entry->bucket = bucket;
|
||||
entry->target_bpp = target_bpp;
|
||||
|
||||
if (decoded_cache_count[cache_id] == DECODED_CACHE_SIZE) {
|
||||
decoded_cache_remove(decoded_cache_oldest[cache_id], cache_id);
|
||||
}
|
||||
|
||||
entry->bucket_next = decoded_cache_buckets[cache_id][bucket];
|
||||
decoded_cache_buckets[cache_id][bucket] = entry;
|
||||
entry->older = decoded_cache_newest[cache_id];
|
||||
if (decoded_cache_newest[cache_id]) {
|
||||
decoded_cache_newest[cache_id]->newer = entry;
|
||||
} else {
|
||||
decoded_cache_oldest[cache_id] = entry;
|
||||
}
|
||||
decoded_cache_newest[cache_id] = entry;
|
||||
++decoded_cache_count[cache_id];
|
||||
|
||||
*hashes = entry->hashes;
|
||||
*count = entry->count;
|
||||
*cache_entry = entry;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const unsigned* step_lower_bound(const unsigned* first, const unsigned* last, unsigned value,
|
||||
size_t jump) {
|
||||
size_t count = (size_t)(last - first);
|
||||
if (count == 0 || first[0] >= value) return first;
|
||||
if (jump == 0) jump = 1;
|
||||
|
||||
size_t position = 0;
|
||||
size_t step = jump;
|
||||
while (step != 0) {
|
||||
if (step > count - position - 1) {
|
||||
step /= 2;
|
||||
continue;
|
||||
}
|
||||
size_t next = position + step;
|
||||
if (first[next] < value)
|
||||
position = next;
|
||||
else
|
||||
step /= 2;
|
||||
}
|
||||
|
||||
return first + position + 1;
|
||||
}
|
||||
|
||||
static int sorted_subset(const unsigned* small, size_t small_count, const unsigned* large,
|
||||
size_t large_count) {
|
||||
const unsigned* small_end = small + small_count;
|
||||
const unsigned* large_end = large + large_count;
|
||||
size_t jump = large_count / small_count;
|
||||
|
||||
if (jump < 4) {
|
||||
while (small < small_end) {
|
||||
unsigned value = *small++;
|
||||
while (large < large_end && *large < value) ++large;
|
||||
if (large == large_end || *large != value) return 0;
|
||||
++large;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (small < small_end) {
|
||||
unsigned value = *small++;
|
||||
large = step_lower_bound(large, large_end, value, jump);
|
||||
if (large == large_end || *large != value) return 0;
|
||||
++large;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rpmsetcmp(const char* str1, const char* str2) {
|
||||
struct set_meta meta1;
|
||||
if (set_meta_init(str1, &meta1) < 0) return -3;
|
||||
|
||||
struct set_meta meta2;
|
||||
if (set_meta_init(str2, &meta2) < 0) return -4;
|
||||
unsigned target_bpp = meta2.bpp < meta1.bpp ? meta2.bpp : meta1.bpp;
|
||||
|
||||
const unsigned* hashes1;
|
||||
size_t count1;
|
||||
struct decoded_cache_entry* entry1;
|
||||
if (cache_decode_set(&meta1, target_bpp, 0, &hashes1, &count1, &entry1) < 0) return -3;
|
||||
|
||||
if (meta1.len == meta2.len && memcmp(meta1.str, meta2.str, meta1.len + 1) == 0) return 0;
|
||||
|
||||
const unsigned* hashes2;
|
||||
size_t count2;
|
||||
struct decoded_cache_entry* entry2;
|
||||
if (cache_decode_set(&meta2, target_bpp, 1, &hashes2, &count2, &entry2) < 0) return -4;
|
||||
|
||||
int result;
|
||||
if (pair_cache_lookup(entry1, entry2, &result)) return result;
|
||||
|
||||
if (count1 == count2)
|
||||
result = memcmp(hashes1, hashes2, count1 * sizeof(*hashes1)) == 0 ? 0 : -2;
|
||||
else if (count1 > count2)
|
||||
result = sorted_subset(hashes2, count2, hashes1, count1) ? 1 : -2;
|
||||
else
|
||||
result = sorted_subset(hashes1, count1, hashes2, count2) ? -1 : -2;
|
||||
|
||||
pair_cache_store(entry1, entry2, result);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Другой дизайн
|
||||
|
||||
## Roaring map
|
||||
|
||||
- описание [здесь](new_version/roaring_bitmap/about.md)
|
||||
|
||||
## хранить сразу расшифрованный set
|
||||
|
||||
- WIP [здесь](new_version/direct_hash/about.md)
|
||||
- огромная часть ресурсов уходит не на просмотр включения множеств, а на декодирование set-строк
|
||||
- при возможности хранить больше данных за более дешёвое сравнение - прекрасно
|
||||
- тупо условный формат:
|
||||
- `<bpp> <последовательно упакованные bpp-битные хеши>`
|
||||
- Shannon–Fano–Elias coding как одна из идей, но надо глубже копать
|
||||
|
||||
## Группировать, а не хэшировать
|
||||
|
||||
- если была бы возможность точно делать соответствия между label и id, то provides стал бы в большинстве последовательным, а required было бы легко искать в P.
|
||||
|
||||
# Доработки на текущий дизайн
|
||||
|
||||
## битовый prefilter
|
||||
|
||||
- Позволяет отбросить заведомо ложные варианты
|
||||
- но так ли часто это будет срабатывать, но вызывает доп расходы при высчитывании
|
||||
- фильтр Блума как пример
|
||||
- В теории хэш можно заменить на него
|
||||
- необходимо знать, насколько больше будет ложноположительных срабатываний
|
||||
|
||||
## улучшить проход по массивам
|
||||
|
||||
- Можно provides хранить не в виде массива, а сразу как хэш-структурку
|
||||
- если provides строка кэшируется не так часто, смысла не будет
|
||||
|
||||
# Прочие улучшения
|
||||
|
||||
## улучшение работы с хэшем
|
||||
|
||||
- если условно "отсортировать" массив provides/requires, можно получить лучшую работу с кэшом
|
||||
- (надеюсь, что под капотом оно уже и так это делает, но проверить стоит)
|
||||
@@ -0,0 +1,39 @@
|
||||
попытка воссоздания алгоритма с roaring bitmap
|
||||
|
||||
Даже эффективней в создании, но проигрывает в дешифровке (сравнении) и занимаемому месту
|
||||
|
||||
Кратно выше становится сама длина строки
|
||||
|
||||
```text
|
||||
symbols=1000 required=500 bpp=32
|
||||
implementation set_chars format
|
||||
set9 3994 golomb
|
||||
bitmap 19924 R1
|
||||
```
|
||||
|
||||
```text
|
||||
operation set9 bitmap bitmap/set9
|
||||
set_fini only 266.24 us 202.89 us 0.76x
|
||||
new+add+fini 1696.05 us 1600.44 us 0.94x
|
||||
rpmsetcmp cold 293.87 us 1321.17 us 4.50x
|
||||
rpmsetcmp warm 5.24 us 399.20 us 76.14x
|
||||
```
|
||||
|
||||
### Использование zstd
|
||||
|
||||
```text
|
||||
symbols=1000 required=500 bpp=32
|
||||
implementation set_chars format
|
||||
set9 3994 golomb
|
||||
bitmap 14126 R2
|
||||
```
|
||||
|
||||
без zstd строка становится ~40% длинее
|
||||
|
||||
```text
|
||||
operation set9 bitmap bitmap/set9
|
||||
set_fini only 171.84 us 314.20 us 1.83x
|
||||
new+add+fini 1185.33 us 1726.32 us 1.46x
|
||||
rpmsetcmp cold 285.04 us 1412.84 us 4.96x
|
||||
rpmsetcmp warm 4.82 us 490.94 us 101.86x
|
||||
```
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import ctypes
|
||||
import gc
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BUILD = HERE / "build"
|
||||
LIBC = ctypes.CDLL(None)
|
||||
LIBC.free.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
class SetAPI:
|
||||
def __init__(self, path: Path, result_needs_free: bool):
|
||||
self.lib = ctypes.CDLL(str(path))
|
||||
self.result_needs_free = result_needs_free
|
||||
self.lib.set_new.restype = ctypes.c_void_p
|
||||
self.lib.set_add.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||||
self.lib.set_fini.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
self.lib.set_fini.restype = ctypes.c_void_p
|
||||
self.lib.set_free.argtypes = [ctypes.c_void_p]
|
||||
self.lib.set_free.restype = ctypes.c_void_p
|
||||
self.lib.rpmsetcmp.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
||||
self.lib.rpmsetcmp.restype = ctypes.c_int
|
||||
|
||||
def new_with_symbols(self, symbols):
|
||||
value = self.lib.set_new()
|
||||
if not value:
|
||||
raise RuntimeError("set_new returned NULL")
|
||||
for symbol in symbols:
|
||||
self.lib.set_add(value, symbol)
|
||||
return value
|
||||
|
||||
def release(self, value, result):
|
||||
if self.result_needs_free and result:
|
||||
LIBC.free(result)
|
||||
self.lib.set_free(value)
|
||||
|
||||
def encode(self, symbols, bpp):
|
||||
value = self.new_with_symbols(symbols)
|
||||
result = self.lib.set_fini(value, bpp)
|
||||
if not result:
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
encoded = ctypes.string_at(result)
|
||||
self.release(value, result)
|
||||
return encoded
|
||||
|
||||
|
||||
def median_fini(api, symbols, bpp, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
sets = [api.new_with_symbols(symbols) for _ in range(calls)]
|
||||
results = []
|
||||
start = time.perf_counter_ns()
|
||||
for value in sets:
|
||||
results.append(api.lib.set_fini(value, bpp))
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if not all(results):
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
encoded = [ctypes.string_at(result) for result in results]
|
||||
if len(set(encoded)) != 1:
|
||||
raise RuntimeError("set_fini is not deterministic")
|
||||
for value, result in zip(sets, results):
|
||||
api.release(value, result)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_build(api, symbols, bpp, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
sets = []
|
||||
results = []
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
result = api.lib.set_fini(value, bpp)
|
||||
sets.append(value)
|
||||
results.append(result)
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if not all(results):
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
for value, result in zip(sets, results):
|
||||
api.release(value, result)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_cmp(api, provider, requirement, calls, rounds):
|
||||
expected = api.lib.rpmsetcmp(provider, requirement)
|
||||
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
|
||||
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
|
||||
for _ in range(100):
|
||||
api.lib.rpmsetcmp(provider, requirement)
|
||||
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
checksum = 0
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
checksum += api.lib.rpmsetcmp(provider, requirement)
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if checksum != calls:
|
||||
raise RuntimeError("rpmsetcmp result changed during benchmark")
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def cold_cmp_once(api, provider, requirement):
|
||||
read_fd, write_fd = os.pipe()
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
os.close(read_fd)
|
||||
start = time.perf_counter_ns()
|
||||
result = api.lib.rpmsetcmp(provider, requirement)
|
||||
elapsed = time.perf_counter_ns() - start
|
||||
os.write(write_fd, f"{elapsed} {result}".encode())
|
||||
os.close(write_fd)
|
||||
os._exit(0)
|
||||
|
||||
os.close(write_fd)
|
||||
payload = b""
|
||||
while chunk := os.read(read_fd, 128):
|
||||
payload += chunk
|
||||
os.close(read_fd)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
if status != 0:
|
||||
raise RuntimeError(f"cold rpmsetcmp child failed: status={status}")
|
||||
elapsed, result = map(int, payload.split())
|
||||
if result != 1:
|
||||
raise RuntimeError(f"unexpected cold rpmsetcmp result: {result}")
|
||||
return elapsed
|
||||
|
||||
|
||||
def median_cmp_cold(api, provider, requirement, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
total = sum(cold_cmp_once(api, provider, requirement) for _ in range(calls))
|
||||
samples.append(total / calls)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def format_time(ns):
|
||||
return f"{ns / 1000:.2f} us"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare set9 and CRoaring set APIs")
|
||||
parser.add_argument("--symbols", type=int, default=1000)
|
||||
parser.add_argument("--bpp", type=int, default=32)
|
||||
parser.add_argument("--rounds", type=int, default=7)
|
||||
parser.add_argument("--fini-calls", type=int, default=5)
|
||||
parser.add_argument("--cmp-calls", type=int, default=2000)
|
||||
parser.add_argument("--cold-calls", type=int, default=20)
|
||||
parser.add_argument("--skip-build", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.symbols < 2 or not 10 <= args.bpp <= 32:
|
||||
parser.error("symbols must be >= 2 and bpp must be in 10..32")
|
||||
if min(args.rounds, args.fini_calls, args.cmp_calls, args.cold_calls) < 1:
|
||||
parser.error("rounds and call counts must be positive")
|
||||
|
||||
if not args.skip_build:
|
||||
subprocess.run([str(HERE / "build.sh")], check=True)
|
||||
|
||||
symbols = tuple(
|
||||
f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols)
|
||||
)
|
||||
required = symbols[::2]
|
||||
apis = {
|
||||
"set9": SetAPI(BUILD / "libset9.so", result_needs_free=True),
|
||||
"bitmap": SetAPI(BUILD / "libbitmap-set.so", result_needs_free=False),
|
||||
}
|
||||
|
||||
gc.disable()
|
||||
try:
|
||||
timings = {}
|
||||
lengths = {}
|
||||
for name, api in apis.items():
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
wire_format = provider[:2].decode() if provider.startswith(b"R1") else "golomb"
|
||||
lengths[name] = (len(provider), wire_format)
|
||||
timings[name] = (
|
||||
median_fini(api, symbols, args.bpp, args.fini_calls, args.rounds),
|
||||
median_build(api, symbols, args.bpp, args.fini_calls, args.rounds),
|
||||
median_cmp_cold(api, provider, requirement, args.cold_calls, args.rounds),
|
||||
median_cmp(api, provider, requirement, args.cmp_calls, args.rounds),
|
||||
)
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
print(f"symbols={args.symbols} required={len(required)} bpp={args.bpp}")
|
||||
print("implementation set_chars format")
|
||||
for name in apis:
|
||||
print(f"{name:<14} {lengths[name][0]:>9} {lengths[name][1]}")
|
||||
print("\noperation set9 bitmap bitmap/set9")
|
||||
labels = ("set_fini only", "new+add+fini", "rpmsetcmp cold", "rpmsetcmp warm")
|
||||
for index, label in enumerate(labels):
|
||||
old = timings["set9"][index]
|
||||
new = timings["bitmap"][index]
|
||||
print(f"{label:<22} {format_time(old):>10} {format_time(new):>10} {new / old:>12.2f}x")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,290 @@
|
||||
#include <roaring/roaring.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* This is intentionally a new set-string format. It is not compatible with
|
||||
* the Rice-Golomb/base62 strings produced by the original lib/set.c.
|
||||
*
|
||||
* R1<two decimal bpp digits><hex CRoaring portable serialization>
|
||||
*/
|
||||
#define FORMAT_PREFIX "R1"
|
||||
#define FORMAT_HEADER_LEN 4
|
||||
#define MAX_SERIALIZED_SIZE (64u * 1024u * 1024u)
|
||||
|
||||
struct set {
|
||||
roaring_bitmap_t* hashes;
|
||||
size_t added;
|
||||
char* encoded;
|
||||
};
|
||||
|
||||
static void* xmalloc(size_t size) {
|
||||
void* ptr = malloc(size);
|
||||
if (!ptr) abort();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static uint32_t hash_symbol(const char* str) {
|
||||
uint32_t hash = UINT32_C(0x9e3779b9);
|
||||
const unsigned char* p = (const unsigned char*)str;
|
||||
|
||||
while (*p) {
|
||||
hash += *p++;
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
}
|
||||
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 11;
|
||||
hash += hash << 15;
|
||||
return hash;
|
||||
}
|
||||
|
||||
struct set* set_new(void) {
|
||||
struct set* set = xmalloc(sizeof(*set));
|
||||
set->hashes = roaring_bitmap_create();
|
||||
if (!set->hashes) abort();
|
||||
set->added = 0;
|
||||
set->encoded = NULL;
|
||||
return set;
|
||||
}
|
||||
|
||||
void set_add(struct set* set, const char* sym) {
|
||||
if (!set || !sym) return;
|
||||
roaring_bitmap_add(set->hashes, hash_symbol(sym));
|
||||
set->added++;
|
||||
}
|
||||
|
||||
static roaring_bitmap_t* truncate_bitmap(const roaring_bitmap_t* source, unsigned bpp) {
|
||||
roaring_bitmap_t* result;
|
||||
roaring_uint32_iterator_t iterator;
|
||||
uint32_t mask;
|
||||
|
||||
if (bpp == 32) {
|
||||
result = roaring_bitmap_copy(source);
|
||||
if (!result) abort();
|
||||
return result;
|
||||
}
|
||||
|
||||
mask = (UINT32_C(1) << bpp) - 1;
|
||||
result = roaring_bitmap_create();
|
||||
if (!result) abort();
|
||||
|
||||
roaring_iterator_init(source, &iterator);
|
||||
while (iterator.has_value) {
|
||||
roaring_bitmap_add(result, iterator.current_value & mask);
|
||||
roaring_uint32_iterator_advance(&iterator);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static char hex_digit(unsigned value) {
|
||||
return (char)(value < 10 ? '0' + value : 'a' + value - 10);
|
||||
}
|
||||
|
||||
const char* set_fini(struct set* set, int bpp) {
|
||||
roaring_bitmap_t* truncated;
|
||||
size_t portable_size;
|
||||
size_t written;
|
||||
unsigned char* portable;
|
||||
char* output;
|
||||
|
||||
if (!set || set->added == 0 || bpp < 10 || bpp > 32) return NULL;
|
||||
|
||||
truncated = truncate_bitmap(set->hashes, (unsigned)bpp);
|
||||
if (roaring_bitmap_get_cardinality(truncated) < set->added)
|
||||
fprintf(stderr, "warning: hash collision\n");
|
||||
|
||||
roaring_bitmap_run_optimize(truncated);
|
||||
portable_size = roaring_bitmap_portable_size_in_bytes(truncated);
|
||||
if (portable_size > MAX_SERIALIZED_SIZE) {
|
||||
roaring_bitmap_free(truncated);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
portable = xmalloc(portable_size);
|
||||
written = roaring_bitmap_portable_serialize(truncated, (char*)portable);
|
||||
if (written != portable_size) abort();
|
||||
if (portable_size > (SIZE_MAX - FORMAT_HEADER_LEN - 1) / 2) abort();
|
||||
|
||||
output = xmalloc(FORMAT_HEADER_LEN + portable_size * 2 + 1);
|
||||
memcpy(output, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1);
|
||||
output[2] = (char)('0' + bpp / 10);
|
||||
output[3] = (char)('0' + bpp % 10);
|
||||
|
||||
for (size_t i = 0; i < portable_size; ++i) {
|
||||
output[FORMAT_HEADER_LEN + i * 2] = hex_digit(portable[i] >> 4);
|
||||
output[FORMAT_HEADER_LEN + i * 2 + 1] = hex_digit(portable[i] & 0x0f);
|
||||
}
|
||||
output[FORMAT_HEADER_LEN + portable_size * 2] = '\0';
|
||||
|
||||
free(portable);
|
||||
roaring_bitmap_free(truncated);
|
||||
free(set->encoded);
|
||||
set->encoded = output;
|
||||
return set->encoded;
|
||||
}
|
||||
|
||||
struct set* set_free(struct set* set) {
|
||||
if (set) {
|
||||
roaring_bitmap_free(set->hashes);
|
||||
free(set->encoded);
|
||||
free(set);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int hex_value(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static roaring_bitmap_t* decode_bitmap(const char* str, unsigned* bpp) {
|
||||
roaring_bitmap_t* bitmap;
|
||||
unsigned char* bytes;
|
||||
const char* hex;
|
||||
const char* reason = NULL;
|
||||
size_t hex_len;
|
||||
size_t str_len;
|
||||
size_t byte_count;
|
||||
size_t expected;
|
||||
|
||||
if (!str || !bpp) return NULL;
|
||||
if (strncmp(str, "set:", 4) == 0) str += 4;
|
||||
str_len = strlen(str);
|
||||
if (str_len < FORMAT_HEADER_LEN) return NULL;
|
||||
if (strncmp(str, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1) != 0) return NULL;
|
||||
if (str[2] < '0' || str[2] > '9' || str[3] < '0' || str[3] > '9') return NULL;
|
||||
|
||||
*bpp = (unsigned)(str[2] - '0') * 10u + (unsigned)(str[3] - '0');
|
||||
if (*bpp < 10 || *bpp > 32) return NULL;
|
||||
|
||||
hex = str + FORMAT_HEADER_LEN;
|
||||
hex_len = str_len - FORMAT_HEADER_LEN;
|
||||
if (hex_len == 0 || (hex_len & 1u) != 0) return NULL;
|
||||
byte_count = hex_len / 2;
|
||||
if (byte_count > MAX_SERIALIZED_SIZE) return NULL;
|
||||
bytes = xmalloc(byte_count);
|
||||
|
||||
for (size_t i = 0; i < byte_count; ++i) {
|
||||
int high = hex_value(hex[i * 2]);
|
||||
int low = hex_value(hex[i * 2 + 1]);
|
||||
if (high < 0 || low < 0) {
|
||||
free(bytes);
|
||||
return NULL;
|
||||
}
|
||||
bytes[i] = (unsigned char)((high << 4) | low);
|
||||
}
|
||||
|
||||
expected = roaring_bitmap_portable_deserialize_size((const char*)bytes, byte_count);
|
||||
if (expected != byte_count) {
|
||||
free(bytes);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bitmap = roaring_bitmap_portable_deserialize_safe((const char*)bytes, byte_count);
|
||||
free(bytes);
|
||||
if (!bitmap) return NULL;
|
||||
if (!roaring_bitmap_internal_validate(bitmap, &reason) || roaring_bitmap_is_empty(bitmap)) {
|
||||
roaring_bitmap_free(bitmap);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
int rpmsetcmp(const char* set1, const char* set2) {
|
||||
roaring_bitmap_t* bitmap1;
|
||||
roaring_bitmap_t* bitmap2;
|
||||
unsigned bpp1;
|
||||
unsigned bpp2;
|
||||
unsigned common_bpp;
|
||||
bool one_in_two;
|
||||
bool two_in_one;
|
||||
|
||||
bitmap1 = decode_bitmap(set1, &bpp1);
|
||||
if (!bitmap1) return -3;
|
||||
bitmap2 = decode_bitmap(set2, &bpp2);
|
||||
if (!bitmap2) {
|
||||
roaring_bitmap_free(bitmap1);
|
||||
return -4;
|
||||
}
|
||||
|
||||
common_bpp = bpp1 < bpp2 ? bpp1 : bpp2;
|
||||
if (bpp1 != common_bpp) {
|
||||
roaring_bitmap_t* truncated = truncate_bitmap(bitmap1, common_bpp);
|
||||
roaring_bitmap_free(bitmap1);
|
||||
bitmap1 = truncated;
|
||||
}
|
||||
if (bpp2 != common_bpp) {
|
||||
roaring_bitmap_t* truncated = truncate_bitmap(bitmap2, common_bpp);
|
||||
roaring_bitmap_free(bitmap2);
|
||||
bitmap2 = truncated;
|
||||
}
|
||||
|
||||
one_in_two = roaring_bitmap_is_subset(bitmap1, bitmap2);
|
||||
two_in_one = roaring_bitmap_is_subset(bitmap2, bitmap1);
|
||||
roaring_bitmap_free(bitmap1);
|
||||
roaring_bitmap_free(bitmap2);
|
||||
|
||||
if (one_in_two && two_in_one) return 0;
|
||||
if (two_in_one) return 1;
|
||||
if (one_in_two) return -1;
|
||||
return -2;
|
||||
}
|
||||
|
||||
#ifdef SELF_TEST
|
||||
#include <assert.h>
|
||||
|
||||
static char* make_set(const char* const* symbols, size_t count, int bpp) {
|
||||
struct set* set = set_new();
|
||||
const char* encoded;
|
||||
char* copy;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) set_add(set, symbols[i]);
|
||||
encoded = set_fini(set, bpp);
|
||||
assert(encoded);
|
||||
copy = xmalloc(strlen(encoded) + 1);
|
||||
strcpy(copy, encoded);
|
||||
set = set_free(set);
|
||||
assert(!set);
|
||||
return copy;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const char* const small[] = {"malloc", "printf"};
|
||||
static const char* const large[] = {"free", "malloc", "printf"};
|
||||
static const char* const other[] = {"calloc", "malloc"};
|
||||
char* small16 = make_set(small, 2, 16);
|
||||
char* small32 = make_set(small, 2, 32);
|
||||
char* large16 = make_set(large, 3, 16);
|
||||
char* other16 = make_set(other, 2, 16);
|
||||
char* prefixed = xmalloc(strlen(small16) + 5);
|
||||
|
||||
sprintf(prefixed, "set:%s", small16);
|
||||
assert(rpmsetcmp(small16, small16) == 0);
|
||||
assert(rpmsetcmp(prefixed, small16) == 0);
|
||||
assert(rpmsetcmp(small32, small16) == 0);
|
||||
assert(rpmsetcmp(large16, small16) == 1);
|
||||
assert(rpmsetcmp(small16, large16) == -1);
|
||||
assert(rpmsetcmp(small16, other16) == -2);
|
||||
assert(rpmsetcmp("bad", small16) == -3);
|
||||
assert(rpmsetcmp("R21600", small16) == -3);
|
||||
assert(rpmsetcmp(small16, "R116xyz") == -4);
|
||||
assert(rpmsetcmp("R", small16) == -3);
|
||||
|
||||
free(prefixed);
|
||||
free(other16);
|
||||
free(large16);
|
||||
free(small32);
|
||||
free(small16);
|
||||
puts("bitmap_set self-test: OK");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/../.." && pwd)
|
||||
BUILD="$HERE/build"
|
||||
mkdir -p "$BUILD"
|
||||
touch "$BUILD/rpmlib.h" "$BUILD/system.h"
|
||||
|
||||
if [[ -z ${ROARING_CFLAGS+x} || -z ${ROARING_LIBS+x} ]]; then
|
||||
if pkg-config --exists roaring; then
|
||||
ROARING_CFLAGS=$(pkg-config --cflags roaring)
|
||||
ROARING_LIBS=$(pkg-config --libs roaring)
|
||||
else
|
||||
CROARING_SRC="$BUILD/CRoaring"
|
||||
CROARING_BUILD="$BUILD/CRoaring-pic-build"
|
||||
if [[ ! -d $CROARING_SRC/.git ]]; then
|
||||
rm -rf "$CROARING_SRC"
|
||||
git clone --depth 1 https://github.com/RoaringBitmap/CRoaring.git "$CROARING_SRC"
|
||||
fi
|
||||
if [[ ! -f $CROARING_BUILD/src/libroaring.a ]]; then
|
||||
cmake -S "$CROARING_SRC" -B "$CROARING_BUILD" \
|
||||
-DROARING_BUILD_STATIC=ON -DENABLE_ROARING_TESTS=OFF \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
cmake --build "$CROARING_BUILD" --parallel
|
||||
fi
|
||||
ROARING_CFLAGS="-I$CROARING_SRC/include"
|
||||
ROARING_LIBS="$CROARING_BUILD/src/libroaring.a"
|
||||
fi
|
||||
fi
|
||||
read -r -a ROARING_CFLAGS_A <<<"$ROARING_CFLAGS"
|
||||
read -r -a ROARING_LIBS_A <<<"$ROARING_LIBS"
|
||||
CFLAGS=(-O2 -std=gnu11 -D_GNU_SOURCE -Wall -Wextra -I"$HERE" -I"$BUILD")
|
||||
|
||||
for tool in mkset setcmp; do
|
||||
cc "${CFLAGS[@]}" -include "$ROOT/scripts/rpmsetcmp/newset_compat.h" \
|
||||
"$ROOT/reimplement/set9.c" "$ROOT/scripts/rpmsetcmp/$tool.c" \
|
||||
-o "$BUILD/$tool-set9"
|
||||
cc "${CFLAGS[@]}" "${ROARING_CFLAGS_A[@]}" \
|
||||
"$HERE/bitmap_set.c" "$ROOT/scripts/rpmsetcmp/$tool.c" \
|
||||
"${ROARING_LIBS_A[@]}" -o "$BUILD/$tool-bitmap"
|
||||
done
|
||||
|
||||
cc "${CFLAGS[@]}" -fPIC -shared -include "$ROOT/scripts/rpmsetcmp/newset_compat.h" \
|
||||
"$ROOT/reimplement/set9.c" -o "$BUILD/libset9.so"
|
||||
cc "${CFLAGS[@]}" -fPIC -shared "${ROARING_CFLAGS_A[@]}" \
|
||||
"$HERE/bitmap_set.c" "${ROARING_LIBS_A[@]}" -o "$BUILD/libbitmap-set.so"
|
||||
|
||||
printf 'Built tools and benchmark libraries in %s\n' "$BUILD"
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef ARSV_SET_H
|
||||
#define ARSV_SET_H
|
||||
|
||||
struct set;
|
||||
|
||||
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);
|
||||
|
||||
#endif
|
||||
+520
-244
File diff suppressed because it is too large
Load Diff
+25
-18
@@ -16,6 +16,7 @@ PERF_EVENTS='task-clock,context-switches,cpu-migrations,page-faults,minor-faults
|
||||
PACKAGER='krosh <gudovdo@my.msu.ru>'
|
||||
APT_SOURCE=/etc/apt/sources.list.d/alt.list
|
||||
APT_GET=/usr/lib/apt/apt-get
|
||||
TIME_COMMAND=/usr/bin/time
|
||||
RPM_BUILD_GIT=https://git.altlinux.org/gears/r/rpm-build.git
|
||||
RPM_GIT=https://git.altlinux.org/gears/r/rpm.git
|
||||
|
||||
@@ -93,7 +94,7 @@ operation_label()
|
||||
|
||||
run_once()
|
||||
{
|
||||
local operation=$1 variant=$2 run=$3 start end status perf_stat
|
||||
local operation=$1 variant=$2 run=$3 status perf_stat time_file
|
||||
local libdir="$variant/lib/usr/lib64"
|
||||
local root="$COMMON/root"
|
||||
local raw="$variant/raw"
|
||||
@@ -117,8 +118,9 @@ run_once()
|
||||
)
|
||||
fi
|
||||
|
||||
start=$(date +%s%N)
|
||||
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \
|
||||
time_file="$raw/$operation.$run.time.tsv"
|
||||
if "$TIME_COMMAND" -f $'%U\t%S' -o "$time_file" -- \
|
||||
env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \
|
||||
LD_LIBRARY_PATH="$libdir" \
|
||||
taskset -c "$CPU" "${command[@]}" \
|
||||
>"$raw/$operation.$run.stdout" \
|
||||
@@ -127,10 +129,7 @@ run_once()
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
end=$(date +%s%N)
|
||||
|
||||
RUN_TIME=$(awk -v start="$start" -v end="$end" \
|
||||
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
|
||||
read -r RUN_USER_TIME RUN_SYSTEM_TIME <"$time_file"
|
||||
RUN_STATUS=$status
|
||||
}
|
||||
|
||||
@@ -228,8 +227,8 @@ benchmark_variant()
|
||||
{
|
||||
local variant=$1 result=$2 debug_file=$3 debuginfo_rpm=$4
|
||||
local runtime_file=$5 dso_name=$6
|
||||
local operation run average label status_text perf_result perf_dir
|
||||
local -a times statuses
|
||||
local operation run average_user average_system label status_text perf_result perf_dir
|
||||
local -a user_times system_times statuses
|
||||
local -a operations=(
|
||||
check
|
||||
autoremove
|
||||
@@ -241,7 +240,7 @@ benchmark_variant()
|
||||
)
|
||||
|
||||
mkdir -p "$variant/raw/perf-stat"
|
||||
printf 'command\taverage_seconds\trun1_seconds\trun2_seconds\trun3_seconds\texit_status\n' \
|
||||
printf 'command\taverage_user_seconds\taverage_system_seconds\trun1_user_seconds\trun1_system_seconds\trun2_user_seconds\trun2_system_seconds\trun3_user_seconds\trun3_system_seconds\texit_status\n' \
|
||||
>"$result"
|
||||
perf_result=${result%.tsv}.perf-stat.tsv
|
||||
perf_dir=${result%.tsv}.perf
|
||||
@@ -257,22 +256,27 @@ benchmark_variant()
|
||||
fi
|
||||
|
||||
for operation in "${operations[@]}"; do
|
||||
times=()
|
||||
user_times=()
|
||||
system_times=()
|
||||
statuses=()
|
||||
|
||||
for ((run = 1; run <= RUNS; ++run)); do
|
||||
run_once "$operation" "$variant" "$run"
|
||||
times+=("$RUN_TIME")
|
||||
user_times+=("$RUN_USER_TIME")
|
||||
system_times+=("$RUN_SYSTEM_TIME")
|
||||
statuses+=("$RUN_STATUS")
|
||||
if ((COLLECT_PERF)); then
|
||||
append_perf_stat "$operation" "$run" \
|
||||
"$variant/raw/perf-stat/$operation.$run.tsv" "$perf_result"
|
||||
fi
|
||||
printf '%s: run %d/%d: %ss, status=%s\n' \
|
||||
"$operation" "$run" "$RUNS" "$RUN_TIME" "$RUN_STATUS"
|
||||
printf '%s: run %d/%d: user=%ss system=%ss status=%s\n' \
|
||||
"$operation" "$run" "$RUNS" \
|
||||
"$RUN_USER_TIME" "$RUN_SYSTEM_TIME" "$RUN_STATUS"
|
||||
done
|
||||
|
||||
average=$(printf '%s\n' "${times[@]}" | \
|
||||
average_user=$(printf '%s\n' "${user_times[@]}" | \
|
||||
awk '{ total += $1 } END { printf "%.6f", total / NR }')
|
||||
average_system=$(printf '%s\n' "${system_times[@]}" | \
|
||||
awk '{ total += $1 } END { printf "%.6f", total / NR }')
|
||||
|
||||
status_text=${statuses[0]}
|
||||
@@ -284,9 +288,11 @@ benchmark_variant()
|
||||
done
|
||||
|
||||
label=$(operation_label "$operation")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$label" "$average" \
|
||||
"${times[0]}" "${times[1]}" "${times[2]}" "$status_text" \
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$label" "$average_user" "$average_system" \
|
||||
"${user_times[0]}" "${system_times[0]}" \
|
||||
"${user_times[1]}" "${system_times[1]}" \
|
||||
"${user_times[2]}" "${system_times[2]}" "$status_text" \
|
||||
>>"$result"
|
||||
|
||||
if ((COLLECT_PERF && PERF_RECORD)); then
|
||||
@@ -300,6 +306,7 @@ for command in git gear-hsh hsh rpm rpmquery rpm2cpio cpio apt-get apt-cache \
|
||||
taskset awk sed date sha256sum ldd readelf readlink eu-unstrip; do
|
||||
command -v "$command" >/dev/null || fail "required command not found: $command"
|
||||
done
|
||||
[[ -x $TIME_COMMAND ]] || fail "time executable not found: $TIME_COMMAND"
|
||||
if ((COLLECT_PERF)); then
|
||||
command -v perf >/dev/null || \
|
||||
fail "perf is not installed; install the ALT package: apt-get install perf"
|
||||
|
||||
Reference in New Issue
Block a user