Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548159e5ea | ||
|
|
b1806cda5e | ||
|
|
e7836fdf31 | ||
|
|
45033e7f6d | ||
|
|
8c25773fcc | ||
|
|
7b5fa1864b | ||
|
|
be1fcd06b5 | ||
|
|
9f3b01f445 | ||
|
|
74e3c9d6d3 | ||
|
|
4eb56ae891 |
+28
-28
@@ -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 |
|
|
||||||
|
|||||||
@@ -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,204 @@
|
|||||||
|
\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}. Пример показывает, что строка содержит достаточно данных для восстановления упорядоченного множества усечённых хешей, но не исходных имён символов.
|
||||||
|
|
||||||
|
\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}
|
||||||
|
|
||||||
|
\item
|
||||||
|
\Label{GOLOMB-RICE}
|
||||||
|
|
||||||
|
\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}
|
||||||
|
%% ---------------------------------------------------------------------------
|
||||||
Submodule
+1
Submodule hash_testing/old_testing/src/cityhash added at f5dc54147f
Submodule
+1
Submodule hash_testing/old_testing/src/xxHash added at e573d4d2aa
@@ -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)
|
raise RuntimeError("set_fini returned NULL")
|
||||||
samples.append((time.perf_counter_ns() - start) / calls)
|
checksum ^= len(ctypes.string_at(result))
|
||||||
if not all(results):
|
|
||||||
raise RuntimeError("set_fini returned NULL")
|
|
||||||
for value, result in zip(sets, results):
|
|
||||||
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)
|
||||||
expected = api.lib.rpmsetcmp(provider, requirement)
|
if not result:
|
||||||
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
|
raise RuntimeError("set_fini returned NULL")
|
||||||
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
|
checksum ^= len(ctypes.string_at(result))
|
||||||
for _ in range(100):
|
api.release(value, result)
|
||||||
api.lib.rpmsetcmp(provider, requirement)
|
elif args.time_child_operation == "cmp-cold":
|
||||||
|
provider = api.encode(symbols, args.bpp)
|
||||||
samples = []
|
requirement = api.encode(required, args.bpp)
|
||||||
for _ in range(rounds):
|
for _ in range(args.cold_calls):
|
||||||
checksum = 0
|
pid = os.fork()
|
||||||
start = time.perf_counter_ns()
|
if pid == 0:
|
||||||
for _ in range(calls):
|
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)
|
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__":
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user