Compare commits

...
9 Commits
Author SHA1 Message Date
krosh b1806cda5e update test result 2026-08-24 05:53:12 +03:00
krosh e7836fdf31 article for asvk 2026-08-21 04:44:49 +03:00
krosh 45033e7f6d perf: measure set benchmarks with user and system time 2026-08-20 13:31:12 +03:00
krosh 8c25773fcc WIP alt article 2026-08-19 05:38:09 +03:00
krosh 7b5fa1864b WIP article for alt article 2026-08-18 15:51:05 +03:00
krosh be1fcd06b5 return cache for pairs 2026-08-18 15:50:52 +03:00
krosh 9f3b01f445 WIP article for alt article 2026-08-18 13:14:15 +03:00
krosh 74e3c9d6d3 WIP article for alt conf 2026-08-18 05:59:20 +03:00
krosh 4eb56ae891 some upd on speed hash_set 2026-08-18 03:45:51 +03:00
9 changed files with 768 additions and 268 deletions
+28 -28
View File
@@ -1,3 +1,7 @@
# Результаты APT-RPM тестов
Коэффициент считается как отношение времени базовой реализации к времени проверяемой реализации. Значение больше `1×` означает ускорение, меньше `1×` — замедление.
## original set.c w/out optimizations ## original set.c w/out optimizations
функция `decode_set` идёт по пути функций: функция `decode_set` идёт по пути функций:
@@ -6,15 +10,13 @@
2. `decode_golomb` 2. `decode_golomb`
3. `decode_delta` 3. `decode_delta`
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status | | command | user, с | system, с | user+system, с |
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: | | :-------------------------- | -------: | --------: | -------------: |
| -s check | 4.217343 | 4.310882 | 4.179343 | 4.161804 | 0 | | `-s check` | 1.320000 | 0.050000 | 1.370000 |
| -s autoremove | 4.884413 | 4.876135 | 4.882347 | 4.894757 | 0 | | `-s autoremove` | 1.626667 | 0.050000 | 1.676667 |
| -s install rpm-build | 4.634789 | 4.652499 | 4.623077 | 4.628792 | 0 | | `-s install rpm-build` | 2.913333 | 0.070000 | 2.983333 |
| -s install openuds-server | 12.704538 | 12.640266 | 12.975482 | 12.497867 | 0 | | `-s install openuds-server` | 5.526667 | 0.100000 | 5.626667 |
| -s install password-store | 8.560463 | 8.561515 | 8.557596 | 8.562277 | 0 | | `-s install password-store` | 2.940000 | 0.070000 | 3.010000 |
| --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 |
## original set.c ## original set.c
@@ -23,26 +25,24 @@
1. `decode_base62_golomb` 1. `decode_base62_golomb`
2. `decode_delta` 2. `decode_delta`
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status | Коэффициенты посчитаны относительно `original set.c w/out optimizations`.
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
| -s check | 1.570747 | 1.621496 | 1.545001 | 1.545744 | 0 | | command | user, с | system, с | user+system, с | коэфф. user | коэфф. user+system |
| -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 check` | 0.453333 | 0.033333 | 0.486666 | 2.912× | 2.815× |
| -s install openuds-server | 8.028098 | 8.147214 | 8.001140 | 7.935940 | 0 | | `-s autoremove` | 0.763333 | 0.040000 | 0.803333 | 2.131× | 2.087× |
| -s install password-store | 3.373377 | 3.396943 | 3.361290 | 3.361899 | 0 | | `-s install rpm-build` | 1.210000 | 0.050000 | 1.260000 | 2.408× | 2.368× |
| --enable-upgrade -s upgrade | 33.915979 | 33.895262 | 33.770192 | 34.082484 | 100 | | `-s install openuds-server` | 3.746667 | 0.080000 | 3.826667 | 1.475× | 1.470× |
| -s dist-upgrade | 88.722136 | 95.484250 | 85.808085 | 84.874073 | 100 | | `-s install password-store` | 1.233333 | 0.050000 | 1.283333 | 2.384× | 2.345× |
## set9.c ## set9.c
Переписанная реализация `set.c` Переписанная реализация `set.c`. Коэффициенты посчитаны относительно `original set.c`.
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status | | command | user, с | system, с | user+system, с | коэфф. user | коэфф. user+system |
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: | | :-------------------------- | -------: | --------: | -------------: | ----------: | -----------------: |
| -s check | 1.550342 | 1.596307 | 1.532768 | 1.521950 | 0 | | `-s check` | 0.503333 | 0.040000 | 0.543333 | 0.901× | 0.896× |
| -s autoremove | 2.200188 | 2.237645 | 2.185378 | 2.177540 | 0 | | `-s autoremove` | 0.810000 | 0.040000 | 0.850000 | 0.942× | 0.945× |
| -s install rpm-build | 2.031055 | 2.045757 | 2.023173 | 2.024234 | 0 | | `-s install rpm-build` | 1.283333 | 0.050000 | 1.333333 | 0.943× | 0.945× |
| -s install openuds-server | 7.580058 | 7.572994 | 7.582557 | 7.584622 | 0 | | `-s install openuds-server` | 3.896667 | 0.083333 | 3.980000 | 0.962× | 0.961× |
| -s install password-store | 3.337012 | 3.367696 | 3.309826 | 3.333514 | 0 | | `-s install password-store` | 1.310000 | 0.050000 | 1.360000 | 0.941× | 0.944× |
| --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 |
+189
View File
@@ -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. Итоги
(нужны ли..)
+119
View File
@@ -0,0 +1,119 @@
\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$ значений. Тогда математическое ожидание числа столкнувшихся пар равно
\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}
\end{enumerate}
+141
View File
@@ -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}
%% ---------------------------------------------------------------------------
Submodule hash_testing/old_testing/src/cityhash added at f5dc54147f
Submodule hash_testing/old_testing/src/xxHash added at e573d4d2aa
+168 -135
View File
@@ -3,15 +3,19 @@ import argparse
import ctypes import ctypes
import gc import gc
import os import os
import shutil
import statistics import statistics
import subprocess import subprocess
import time import sys
import tempfile
from pathlib import Path from pathlib import Path
HERE = Path(__file__).resolve().parent HERE = Path(__file__).resolve().parent
BUILD = HERE / "build" BUILD = HERE / "build"
LIBC = ctypes.CDLL(None) LIBC = ctypes.CDLL(None)
LIBC.free.argtypes = [ctypes.c_void_p] 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: class SetAPI:
@@ -49,108 +53,132 @@ class SetAPI:
return encoded return encoded
def median_fini(api, symbols, bpp, calls, rounds): def make_symbols(args):
samples = [] symbols = tuple(
for _ in range(rounds): f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols)
sets = [api.new_with_symbols(symbols) for _ in range(calls)] )
results = [] required = (
start = time.perf_counter_ns() symbols[::2]
for value in sets: if args.required is None
results.append(api.lib.set_fini(value, bpp)) else tuple(symbols[i * args.symbols // args.required] for i in range(args.required))
samples.append((time.perf_counter_ns() - start) / calls) )
if not all(results): return symbols, required
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): def api_for(name):
samples = [] if name == "set9":
for _ in range(rounds): return SetAPI(BUILD / "libset9.so")
sets = [] if name == "direct":
results = [] return SetAPI(BUILD / "libdirect-hash.so")
start = time.perf_counter_ns() raise ValueError(f"unknown implementation: {name}")
for _ in range(calls):
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) value = api.new_with_symbols(symbols)
result = api.lib.set_fini(value, bpp) result = api.lib.set_fini(value, args.bpp)
sets.append(value) if not result:
results.append(result)
samples.append((time.perf_counter_ns() - start) / calls)
if not all(results):
raise RuntimeError("set_fini returned NULL") raise RuntimeError("set_fini returned NULL")
for value, result in zip(sets, results): checksum ^= len(ctypes.string_at(result))
api.release(value, result) api.release(value, result)
return statistics.median(samples) elif args.time_child_operation == "add":
for _ in range(args.fini_calls):
value = api.new_with_symbols(symbols)
def median_add(api, symbols, calls, rounds): checksum ^= int(value)
samples = []
for _ in range(rounds):
values = []
start = time.perf_counter_ns()
for _ in range(calls):
values.append(api.new_with_symbols(symbols))
samples.append((time.perf_counter_ns() - start) / calls)
for value in values:
api.lib.set_free(value) api.lib.set_free(value)
return statistics.median(samples) elif args.time_child_operation == "build":
for _ in range(args.fini_calls):
value = api.new_with_symbols(symbols)
def median_cmp(api, provider, requirement, calls, rounds): 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) expected = api.lib.rpmsetcmp(provider, requirement)
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0: if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}") raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
for _ in range(100): for _ in range(100):
api.lib.rpmsetcmp(provider, requirement) api.lib.rpmsetcmp(provider, requirement)
for _ in range(args.cmp_calls):
samples = []
for _ in range(rounds):
checksum = 0
start = time.perf_counter_ns()
for _ in range(calls):
checksum += api.lib.rpmsetcmp(provider, requirement) checksum += api.lib.rpmsetcmp(provider, requirement)
samples.append((time.perf_counter_ns() - start) / calls) if checksum != args.cmp_calls:
if checksum != calls:
raise RuntimeError("rpmsetcmp result changed during benchmark") raise RuntimeError("rpmsetcmp result changed during benchmark")
return statistics.median(samples) 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 cold_cmp_once(api, provider, requirement): def measure_with_time(args, implementation, operation, calls):
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 = [] samples = []
for _ in range(rounds): for _ in range(args.rounds):
total = sum(cold_cmp_once(api, provider, requirement) for _ in range(calls)) with tempfile.NamedTemporaryFile(prefix="arsv-time-", delete=False) as handle:
samples.append(total / calls) time_path = Path(handle.name)
return statistics.median(samples) 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): def verify_complete_decoding(api, provider, requirement):
@@ -167,11 +195,17 @@ def verify_complete_decoding(api, provider, requirement):
raise RuntimeError("second operand was not decoded and validated completely") raise RuntimeError("second operand was not decoded and validated completely")
def format_time(ns): def format_cpu_time(seconds):
return f"{ns / 1000:.2f} us" return f"{seconds * 1_000_000:.2f} us"
def main(): 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 = argparse.ArgumentParser(description="Compare set9 and direct-hash set APIs")
parser.add_argument("--symbols", type=int, default=1000) parser.add_argument("--symbols", type=int, default=1000)
parser.add_argument( parser.add_argument(
@@ -185,6 +219,18 @@ def main():
parser.add_argument("--cmp-calls", type=int, default=2000) parser.add_argument("--cmp-calls", type=int, default=2000)
parser.add_argument("--cold-calls", type=int, default=20) parser.add_argument("--cold-calls", type=int, default=20)
parser.add_argument("--skip-build", action="store_true") 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() args = parser.parse_args()
if args.symbols < 2 or not 10 <= args.bpp <= 32: if args.symbols < 2 or not 10 <= args.bpp <= 32:
parser.error("symbols must be >= 2 and bpp must be in 10..32") parser.error("symbols must be >= 2 and bpp must be in 10..32")
@@ -192,26 +238,27 @@ def main():
parser.error("required must be in 1..symbols-1") parser.error("required must be in 1..symbols-1")
if min(args.rounds, args.fini_calls, args.cmp_calls, args.cold_calls) < 1: if min(args.rounds, args.fini_calls, args.cmp_calls, args.cold_calls) < 1:
parser.error("rounds and call counts must be positive") 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: if not args.skip_build:
subprocess.run([str(HERE / "build.sh")], check=True) subprocess.run([str(HERE / "build.sh")], check=True)
symbols = tuple( if args.time_child:
f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols) child_main(args)
) return
required = (
symbols[::2] if not Path(TIME_COMMAND).is_file():
if args.required is None raise RuntimeError(f"time executable not found: {TIME_COMMAND}")
else tuple(symbols[i * args.symbols // args.required] for i in range(args.required))
) symbols, required = make_symbols(args)
apis = { apis = {
"set9": SetAPI(BUILD / "libset9.so"), "set9": api_for("set9"),
"direct": SetAPI(BUILD / "libdirect-hash.so"), "direct": api_for("direct"),
} }
gc.disable() gc.disable()
try: try:
timings = {name: [[] for _ in range(5)] for name in apis}
lengths = {} lengths = {}
encoded = {} encoded = {}
for name, api in apis.items(): for name, api in apis.items():
@@ -221,31 +268,20 @@ def main():
wire_format = "D1/base64" if provider.startswith(b"D1") else "golomb/base62" wire_format = "D1/base64" if provider.startswith(b"D1") else "golomb/base62"
lengths[name] = (len(provider), wire_format) lengths[name] = (len(provider), wire_format)
operations = (
lambda name, api: median_fini(api, symbols, args.bpp, args.fini_calls, 1),
lambda name, api: median_add(api, symbols, args.fini_calls, 1),
lambda name, api: median_build(api, symbols, args.bpp, args.fini_calls, 1),
lambda name, api: median_cmp_cold(
api, encoded[name][0], encoded[name][1], args.cold_calls, 1
),
lambda name, api: median_cmp(
api, encoded[name][0], encoded[name][1], args.cmp_calls, 1
),
)
names = tuple(apis)
for operation_index, operation in enumerate(operations):
for round_index in range(args.rounds):
order = names if round_index % 2 == 0 else tuple(reversed(names))
for name in order:
timings[name][operation_index].append(operation(name, apis[name]))
timings = {
name: tuple(statistics.median(samples) for samples in operation_samples)
for name, operation_samples in timings.items()
}
# Run validation after timing: forked cold samples must inherit an empty
# decoded-set cache from the parent process.
for name, api in apis.items(): for name, api in apis.items():
verify_complete_decoding(api, *encoded[name]) 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: finally:
gc.enable() gc.enable()
@@ -253,18 +289,15 @@ def main():
print("implementation set_chars format") print("implementation set_chars format")
for name in apis: for name in apis:
print(f"{name:<14} {lengths[name][0]:>9} {lengths[name][1]}") print(f"{name:<14} {lengths[name][0]:>9} {lengths[name][1]}")
print("\noperation set9 direct direct/set9") print("\noperation set9_user set9_sys direct_user direct_sys user_ratio sys_ratio")
labels = ( for index, (label, _operation, _calls) in enumerate(operations):
"set_fini only", old_user, old_sys = timings["set9"][index]
"new+add (ctypes)", new_user, new_sys = timings["direct"][index]
"new+add+fini (ctypes)", print(
"rpmsetcmp cold", f"{label:<22} {format_cpu_time(old_user):>10} {format_cpu_time(old_sys):>9} "
"rpmsetcmp warm", 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}"
) )
for index, label in enumerate(labels):
old = timings["set9"][index]
new = timings["direct"][index]
print(f"{label:<22} {format_time(old):>10} {format_time(new):>10} {new / old:>12.2f}x")
if __name__ == "__main__": if __name__ == "__main__":
+89 -80
View File
@@ -1,6 +1,5 @@
#include <assert.h> #include <assert.h>
#include <limits.h> #include <limits.h>
#include <stdatomic.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -34,6 +33,7 @@ struct set {
char* strings; char* strings;
struct symbols { struct symbols {
size_t offset; size_t offset;
unsigned full_hash;
unsigned hash; unsigned hash;
}* symbols_v; }* symbols_v;
}; };
@@ -47,11 +47,9 @@ struct decoded_set {
enum { enum {
DECODED_CACHE_SIZE = 512, DECODED_CACHE_SIZE = 512,
DECODED_CACHE_BUCKETS = 1024, DECODED_CACHE_BUCKETS = 1024,
PAIR_CACHE_SIZE = 4, PAIR_CACHE_SIZE = 16,
}; };
struct decoded_cache_entry;
struct pair_cache_entry { struct pair_cache_entry {
uint64_t other_identity; uint64_t other_identity;
int result; int result;
@@ -65,8 +63,8 @@ struct decoded_cache_entry {
unsigned* hashes; unsigned* hashes;
size_t len; size_t len;
size_t count; size_t count;
uint32_t fingerprint;
uint64_t identity; uint64_t identity;
uint32_t fingerprint;
unsigned bucket; unsigned bucket;
unsigned target_bpp; unsigned target_bpp;
unsigned pair_next; unsigned pair_next;
@@ -84,9 +82,8 @@ static struct decoded_cache_entry* decoded_cache_buckets[2][DECODED_CACHE_BUCKET
static struct decoded_cache_entry* decoded_cache_newest[2]; static struct decoded_cache_entry* decoded_cache_newest[2];
static struct decoded_cache_entry* decoded_cache_oldest[2]; static struct decoded_cache_entry* decoded_cache_oldest[2];
static uint64_t decoded_cache_next_identity = 1; static uint64_t decoded_cache_next_identity = 1;
/* Cached arrays remain in use until comparison completes, so lookup, eviction,
* and comparison share one lock. */ static unsigned hash(const char* str);
static atomic_flag decoded_cache_lock = ATOMIC_FLAG_INIT;
struct set* set_new(void) { struct set* set_new(void) {
struct set* set = xmalloc(sizeof(*set)); struct set* set = xmalloc(sizeof(*set));
@@ -116,7 +113,8 @@ void set_add(struct set* set, const char* sym) {
} }
set->symbols_v[set->cnt].offset = set->strings_len; set->symbols_v[set->cnt].offset = set->strings_len;
set->symbols_v[set->cnt].hash = 0; 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); memcpy(set->strings + set->strings_len, sym, length);
set->strings_len = required; set->strings_len = required;
++set->cnt; ++set->cnt;
@@ -241,18 +239,42 @@ static void base64_encode(const unsigned char* input, size_t input_len, char* ou
return; return;
} }
static unsigned char* pack_hashes(const unsigned* hashes, size_t count, unsigned bpp, static size_t compact_unique_hashes(struct symbols* symbols, size_t count) {
size_t* byte_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(); if (count > (SIZE_MAX - 7) / bpp) abort();
size_t bit_count = count * bpp; size_t bit_count = count * bpp;
*byte_count = (bit_count + 7) / 8; *byte_count = (bit_count + 7) / 8;
unsigned char* bytes = xmalloc(*byte_count); unsigned char* bytes = xmalloc(*byte_count);
unsigned char* output = bytes; 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; uint64_t bits = 0;
unsigned filled = 0; unsigned filled = 0;
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
bits |= (uint64_t)hashes[i] << filled; bits |= (uint64_t)symbols[i].hash << filled;
filled += bpp; filled += bpp;
while (filled >= 8) { while (filled >= 8) {
@@ -275,7 +297,7 @@ const char* set_fini(struct set* set, int bpp) {
unsigned mask = bpp < 32 ? (UINT32_C(1) << bpp) - 1 : UINT32_MAX; unsigned mask = bpp < 32 ? (UINT32_C(1) << bpp) - 1 : UINT32_MAX;
for (size_t i = 0; i < set->cnt; ++i) { for (size_t i = 0; i < set->cnt; ++i) {
set->symbols_v[i].hash = hash(set->strings + set->symbols_v[i].offset) & mask; set->symbols_v[i].hash = set->symbols_v[i].full_hash & mask;
} }
sort_symbols(set->symbols_v, set->cnt, (unsigned)bpp); sort_symbols(set->symbols_v, set->cnt, (unsigned)bpp);
@@ -286,27 +308,9 @@ const char* set_fini(struct set* set, int bpp) {
if (strcmp(left, right) != 0) fprintf(stderr, "warning: hash collision: %s %s\n", left, right); if (strcmp(left, right) != 0) fprintf(stderr, "warning: hash collision: %s %s\n", left, right);
} }
unsigned* unique_hashes = xmalloc(set->cnt * sizeof(*unique_hashes));
size_t unique_count = 0;
for (size_t i = 0; i < set->cnt; ++i) {
while (i + 1 < set->cnt && set->symbols_v[i].hash == set->symbols_v[i + 1].hash) ++i;
unique_hashes[unique_count++] = set->symbols_v[i].hash;
}
size_t byte_count; size_t byte_count;
unsigned char* allocated_bytes = NULL; size_t unique_count = compact_unique_hashes(set->symbols_v, set->cnt);
const unsigned char* bytes; unsigned char* bytes = pack_symbol_hashes(set->symbols_v, unique_count, (unsigned)bpp, &byte_count);
#if UINT_MAX == UINT32_MAX && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \
__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
if (bpp == 32 && sizeof(unsigned) == 4) {
byte_count = unique_count * sizeof(*unique_hashes);
bytes = (const unsigned char*)unique_hashes;
} else
#endif
{
allocated_bytes = pack_hashes(unique_hashes, unique_count, (unsigned)bpp, &byte_count);
bytes = allocated_bytes;
}
size_t payload_len = base64_encoded_size(byte_count); size_t payload_len = base64_encoded_size(byte_count);
char* output = xmalloc(FORMAT_HEADER_LEN + payload_len + 1); char* output = xmalloc(FORMAT_HEADER_LEN + payload_len + 1);
memcpy(output, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1); memcpy(output, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1);
@@ -315,8 +319,7 @@ const char* set_fini(struct set* set, int bpp) {
base64_encode(bytes, byte_count, output + FORMAT_HEADER_LEN); base64_encode(bytes, byte_count, output + FORMAT_HEADER_LEN);
output[FORMAT_HEADER_LEN + payload_len] = '\0'; output[FORMAT_HEADER_LEN + payload_len] = '\0';
_free(allocated_bytes); _free(bytes);
_free(unique_hashes);
return output; return output;
} }
@@ -357,8 +360,7 @@ static int set_meta_init(const char* source, struct set_meta* meta) {
if (has_set_prefix(str)) str += 4; if (has_set_prefix(str)) str += 4;
if (has_set_prefix(str)) return -1; if (has_set_prefix(str)) return -1;
/* With bpp >= 10, a valid direct set has at least three Base64 characters. /* With bpp >= 10, a valid direct set has at least three Base64 characters. */
* Checking this fixed prefix makes cache hits independent of total key length. */
if (str[0] != FORMAT_PREFIX[0] || str[1] != FORMAT_PREFIX[1]) return -1; 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[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; if (str[4] == '\0' || str[5] == '\0' || str[6] == '\0') return -1;
@@ -367,7 +369,7 @@ static int set_meta_init(const char* source, struct set_meta* meta) {
if (bpp < 10 || bpp > 32) return -1; if (bpp < 10 || bpp > 32) return -1;
meta->str = str; meta->str = str;
meta->len = 0; meta->len = strlen(str);
meta->bpp = bpp; meta->bpp = bpp;
return 0; return 0;
} }
@@ -739,12 +741,24 @@ static void downsample_to(struct decoded_set* set, unsigned target_bpp) {
static uint32_t decoded_cache_fingerprint(const struct set_meta* meta, unsigned target_bpp) { static uint32_t decoded_cache_fingerprint(const struct set_meta* meta, unsigned target_bpp) {
const unsigned char* str = (const unsigned char*)meta->str; const unsigned char* str = (const unsigned char*)meta->str;
uint32_t fingerprint = (uint32_t)str[4] | ((uint32_t)str[5] << 8) | uint32_t fingerprint = UINT32_C(2166136261);
((uint32_t)str[6] << 16) | ((uint32_t)str[7] << 24); fingerprint = (fingerprint ^ (uint32_t)meta->len) * UINT32_C(16777619);
fingerprint ^= meta->bpp * UINT32_C(0x27d4eb2d); fingerprint = (fingerprint ^ (meta->bpp * UINT32_C(0x27d4eb2d))) * UINT32_C(16777619);
fingerprint ^= target_bpp * UINT32_C(0x85ebca6b); fingerprint = (fingerprint ^ (target_bpp * UINT32_C(0x85ebca6b))) * UINT32_C(16777619);
fingerprint ^= fingerprint >> 11;
fingerprint *= UINT32_C(0x9e3779b1); 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; fingerprint ^= fingerprint >> 16;
return fingerprint; return fingerprint;
} }
@@ -783,14 +797,23 @@ static void decoded_cache_remove(struct decoded_cache_entry* victim, unsigned ca
_free(victim); _free(victim);
} }
static void decoded_cache_reset_pair_identities(void) { static int pair_cache_lookup(const struct decoded_cache_entry* first,
for (unsigned bucket = 0; bucket < DECODED_CACHE_BUCKETS; ++bucket) { const struct decoded_cache_entry* second, int* result) {
for (struct decoded_cache_entry* provider = decoded_cache_buckets[0][bucket]; provider; for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) {
provider = provider->bucket_next) { if (first->pairs[i].other_identity == second->identity) {
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) provider->pairs[i].other_identity = 0; *result = first->pairs[i].result;
return 1;
} }
} }
decoded_cache_next_identity = 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, static int cache_decode_set(const struct set_meta* meta, unsigned target_bpp, unsigned cache_id,
@@ -804,7 +827,7 @@ static int cache_decode_set(const struct set_meta* meta, unsigned target_bpp, un
for (struct decoded_cache_entry* entry = decoded_cache_buckets[cache_id][bucket]; entry; for (struct decoded_cache_entry* entry = decoded_cache_buckets[cache_id][bucket]; entry;
entry = entry->bucket_next) { entry = entry->bucket_next) {
if (entry->fingerprint != fingerprint || entry->target_bpp != target_bpp || if (entry->fingerprint != fingerprint || entry->target_bpp != target_bpp ||
strcmp(entry->str, meta->str) != 0) entry->len != meta->len || memcmp(entry->str, meta->str, meta->len + 1) != 0)
continue; continue;
decoded_cache_touch(entry, cache_id); decoded_cache_touch(entry, cache_id);
@@ -814,30 +837,28 @@ static int cache_decode_set(const struct set_meta* meta, unsigned target_bpp, un
return 0; return 0;
} }
size_t len = strlen(meta->str);
struct decoded_set decoded; struct decoded_set decoded;
if (decode_set_sized(meta->str, len, &decoded) < 0) return -1; if (decode_set_sized(meta->str, meta->len, &decoded) < 0) return -1;
if (decoded.bpp != meta->bpp) { if (decoded.bpp != meta->bpp) {
_free(decoded.hashes); _free(decoded.hashes);
return -1; return -1;
} }
downsample_to(&decoded, target_bpp); downsample_to(&decoded, target_bpp);
if (len > SIZE_MAX - sizeof(struct decoded_cache_entry) - 1) { if (meta->len > SIZE_MAX - sizeof(struct decoded_cache_entry) - 1) {
_free(decoded.hashes); _free(decoded.hashes);
return -1; return -1;
} }
struct decoded_cache_entry* entry = xmalloc(sizeof(*entry) + len + 1); struct decoded_cache_entry* entry = xmalloc(sizeof(*entry) + meta->len + 1);
memset(entry, 0, sizeof(*entry)); memset(entry, 0, sizeof(*entry));
entry->str = (char*)(entry + 1); entry->str = (char*)(entry + 1);
memcpy(entry->str, meta->str, len + 1); memcpy(entry->str, meta->str, meta->len + 1);
entry->hashes = decoded.hashes; entry->hashes = decoded.hashes;
entry->len = len; entry->len = meta->len;
entry->count = decoded.count; entry->count = decoded.count;
entry->fingerprint = fingerprint;
if (decoded_cache_next_identity == 0) decoded_cache_reset_pair_identities();
entry->identity = decoded_cache_next_identity++; entry->identity = decoded_cache_next_identity++;
if (entry->identity == 0) entry->identity = decoded_cache_next_identity++;
entry->fingerprint = fingerprint;
entry->bucket = bucket; entry->bucket = bucket;
entry->target_bpp = target_bpp; entry->target_bpp = target_bpp;
@@ -912,31 +933,29 @@ static int sorted_subset(const unsigned* small, size_t small_count, const unsign
return 1; return 1;
} }
static int rpmsetcmp_locked(const char* str1, const char* str2) { int rpmsetcmp(const char* str1, const char* str2) {
struct set_meta meta1; struct set_meta meta1;
if (set_meta_init(str1, &meta1) < 0) return -3; if (set_meta_init(str1, &meta1) < 0) return -3;
struct set_meta meta2; struct set_meta meta2;
int meta2_status = set_meta_init(str2, &meta2); if (set_meta_init(str2, &meta2) < 0) return -4;
unsigned target_bpp = unsigned target_bpp = meta2.bpp < meta1.bpp ? meta2.bpp : meta1.bpp;
meta2_status == 0 && meta2.bpp < meta1.bpp ? meta2.bpp : meta1.bpp;
const unsigned* hashes1; const unsigned* hashes1;
size_t count1; size_t count1;
struct decoded_cache_entry* entry1; struct decoded_cache_entry* entry1;
if (cache_decode_set(&meta1, target_bpp, 0, &hashes1, &count1, &entry1) < 0) return -3; if (cache_decode_set(&meta1, target_bpp, 0, &hashes1, &count1, &entry1) < 0) return -3;
if (meta2_status < 0) return -4;
if (meta1.len == meta2.len && memcmp(meta1.str, meta2.str, meta1.len + 1) == 0) return 0;
const unsigned* hashes2; const unsigned* hashes2;
size_t count2; size_t count2;
struct decoded_cache_entry* entry2; struct decoded_cache_entry* entry2;
if (cache_decode_set(&meta2, target_bpp, 1, &hashes2, &count2, &entry2) < 0) return -4; if (cache_decode_set(&meta2, target_bpp, 1, &hashes2, &count2, &entry2) < 0) return -4;
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) {
if (entry1->pairs[i].other_identity == entry2->identity) return entry1->pairs[i].result;
}
int result; int result;
if (pair_cache_lookup(entry1, entry2, &result)) return result;
if (count1 == count2) if (count1 == count2)
result = memcmp(hashes1, hashes2, count1 * sizeof(*hashes1)) == 0 ? 0 : -2; result = memcmp(hashes1, hashes2, count1 * sizeof(*hashes1)) == 0 ? 0 : -2;
else if (count1 > count2) else if (count1 > count2)
@@ -944,16 +963,6 @@ static int rpmsetcmp_locked(const char* str1, const char* str2) {
else else
result = sorted_subset(hashes1, count1, hashes2, count2) ? -1 : -2; result = sorted_subset(hashes1, count1, hashes2, count2) ? -1 : -2;
struct pair_cache_entry* pair = &entry1->pairs[entry1->pair_next++ % PAIR_CACHE_SIZE]; pair_cache_store(entry1, entry2, result);
pair->other_identity = entry2->identity;
pair->result = result;
return result;
}
int rpmsetcmp(const char* str1, const char* str2) {
while (atomic_flag_test_and_set_explicit(&decoded_cache_lock, memory_order_acquire)) {
}
int result = rpmsetcmp_locked(str1, str2);
atomic_flag_clear_explicit(&decoded_cache_lock, memory_order_release);
return result; return result;
} }
+25 -18
View File
@@ -16,6 +16,7 @@ PERF_EVENTS='task-clock,context-switches,cpu-migrations,page-faults,minor-faults
PACKAGER='krosh <gudovdo@my.msu.ru>' PACKAGER='krosh <gudovdo@my.msu.ru>'
APT_SOURCE=/etc/apt/sources.list.d/alt.list APT_SOURCE=/etc/apt/sources.list.d/alt.list
APT_GET=/usr/lib/apt/apt-get 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_BUILD_GIT=https://git.altlinux.org/gears/r/rpm-build.git
RPM_GIT=https://git.altlinux.org/gears/r/rpm.git RPM_GIT=https://git.altlinux.org/gears/r/rpm.git
@@ -93,7 +94,7 @@ operation_label()
run_once() 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 libdir="$variant/lib/usr/lib64"
local root="$COMMON/root" local root="$COMMON/root"
local raw="$variant/raw" local raw="$variant/raw"
@@ -117,8 +118,9 @@ run_once()
) )
fi fi
start=$(date +%s%N) time_file="$raw/$operation.$run.time.tsv"
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \ if "$TIME_COMMAND" -f $'%U\t%S' -o "$time_file" -- \
env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \
LD_LIBRARY_PATH="$libdir" \ LD_LIBRARY_PATH="$libdir" \
taskset -c "$CPU" "${command[@]}" \ taskset -c "$CPU" "${command[@]}" \
>"$raw/$operation.$run.stdout" \ >"$raw/$operation.$run.stdout" \
@@ -127,10 +129,7 @@ run_once()
else else
status=$? status=$?
fi fi
end=$(date +%s%N) read -r RUN_USER_TIME RUN_SYSTEM_TIME <"$time_file"
RUN_TIME=$(awk -v start="$start" -v end="$end" \
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
RUN_STATUS=$status RUN_STATUS=$status
} }
@@ -228,8 +227,8 @@ benchmark_variant()
{ {
local variant=$1 result=$2 debug_file=$3 debuginfo_rpm=$4 local variant=$1 result=$2 debug_file=$3 debuginfo_rpm=$4
local runtime_file=$5 dso_name=$6 local runtime_file=$5 dso_name=$6
local operation run average label status_text perf_result perf_dir local operation run average_user average_system label status_text perf_result perf_dir
local -a times statuses local -a user_times system_times statuses
local -a operations=( local -a operations=(
check check
autoremove autoremove
@@ -241,7 +240,7 @@ benchmark_variant()
) )
mkdir -p "$variant/raw/perf-stat" 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" >"$result"
perf_result=${result%.tsv}.perf-stat.tsv perf_result=${result%.tsv}.perf-stat.tsv
perf_dir=${result%.tsv}.perf perf_dir=${result%.tsv}.perf
@@ -257,22 +256,27 @@ benchmark_variant()
fi fi
for operation in "${operations[@]}"; do for operation in "${operations[@]}"; do
times=() user_times=()
system_times=()
statuses=() statuses=()
for ((run = 1; run <= RUNS; ++run)); do for ((run = 1; run <= RUNS; ++run)); do
run_once "$operation" "$variant" "$run" run_once "$operation" "$variant" "$run"
times+=("$RUN_TIME") user_times+=("$RUN_USER_TIME")
system_times+=("$RUN_SYSTEM_TIME")
statuses+=("$RUN_STATUS") statuses+=("$RUN_STATUS")
if ((COLLECT_PERF)); then if ((COLLECT_PERF)); then
append_perf_stat "$operation" "$run" \ append_perf_stat "$operation" "$run" \
"$variant/raw/perf-stat/$operation.$run.tsv" "$perf_result" "$variant/raw/perf-stat/$operation.$run.tsv" "$perf_result"
fi fi
printf '%s: run %d/%d: %ss, status=%s\n' \ printf '%s: run %d/%d: user=%ss system=%ss status=%s\n' \
"$operation" "$run" "$RUNS" "$RUN_TIME" "$RUN_STATUS" "$operation" "$run" "$RUNS" \
"$RUN_USER_TIME" "$RUN_SYSTEM_TIME" "$RUN_STATUS"
done 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 }') awk '{ total += $1 } END { printf "%.6f", total / NR }')
status_text=${statuses[0]} status_text=${statuses[0]}
@@ -284,9 +288,11 @@ benchmark_variant()
done done
label=$(operation_label "$operation") label=$(operation_label "$operation")
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$label" "$average" \ "$label" "$average_user" "$average_system" \
"${times[0]}" "${times[1]}" "${times[2]}" "$status_text" \ "${user_times[0]}" "${system_times[0]}" \
"${user_times[1]}" "${system_times[1]}" \
"${user_times[2]}" "${system_times[2]}" "$status_text" \
>>"$result" >>"$result"
if ((COLLECT_PERF && PERF_RECORD)); then 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 taskset awk sed date sha256sum ldd readelf readlink eu-unstrip; do
command -v "$command" >/dev/null || fail "required command not found: $command" command -v "$command" >/dev/null || fail "required command not found: $command"
done done
[[ -x $TIME_COMMAND ]] || fail "time executable not found: $TIME_COMMAND"
if ((COLLECT_PERF)); then if ((COLLECT_PERF)); then
command -v perf >/dev/null || \ command -v perf >/dev/null || \
fail "perf is not installed; install the ALT package: apt-get install perf" fail "perf is not installed; install the ALT package: apt-get install perf"