Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e38d1365cd | ||
|
|
9e9c99fc2b | ||
|
|
87b588f63e | ||
|
|
8a53f2186b | ||
|
|
1fac5b277d | ||
|
|
7106d0c9ea | ||
|
|
f86537626a | ||
|
|
548159e5ea | ||
|
|
b1806cda5e | ||
|
|
e7836fdf31 | ||
|
|
45033e7f6d | ||
|
|
8c25773fcc | ||
|
|
7b5fa1864b | ||
|
|
be1fcd06b5 | ||
|
|
9f3b01f445 | ||
|
|
74e3c9d6d3 | ||
|
|
4eb56ae891 |
@@ -1,3 +1,10 @@
|
||||
# Результаты APT-RPM тестов
|
||||
|
||||
Коэффициент считается как отношение времени базовой реализации к времени проверяемой реализации. Значение больше `1×` означает ускорение, меньше `1×` — замедление.
|
||||
|
||||
- `original set.c` - код текущего `lib/set.c` в `rpm`/`rpm-build`
|
||||
- `set9.c` - drop-in замена оригинального `lib/set.c`.
|
||||
|
||||
## original set.c w/out optimizations
|
||||
|
||||
функция `decode_set` идёт по пути функций:
|
||||
@@ -6,15 +13,13 @@
|
||||
2. `decode_golomb`
|
||||
3. `decode_delta`
|
||||
|
||||
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status |
|
||||
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
|
||||
| -s check | 4.217343 | 4.310882 | 4.179343 | 4.161804 | 0 |
|
||||
| -s autoremove | 4.884413 | 4.876135 | 4.882347 | 4.894757 | 0 |
|
||||
| -s install rpm-build | 4.634789 | 4.652499 | 4.623077 | 4.628792 | 0 |
|
||||
| -s install openuds-server | 12.704538 | 12.640266 | 12.975482 | 12.497867 | 0 |
|
||||
| -s install password-store | 8.560463 | 8.561515 | 8.557596 | 8.562277 | 0 |
|
||||
| --enable-upgrade -s upgrade | 36.271838 | 35.511443 | 35.908641 | 37.395430 | 100 |
|
||||
| -s dist-upgrade | 90.550086 | 94.702198 | 92.174160 | 84.773901 | 100 |
|
||||
| command | user, с | system, с | user+system, с |
|
||||
| :-------------------------- | -------: | --------: | -------------: |
|
||||
| `-s check` | 1.320000 | 0.050000 | 1.370000 |
|
||||
| `-s autoremove` | 1.626667 | 0.050000 | 1.676667 |
|
||||
| `-s install rpm-build` | 2.913333 | 0.070000 | 2.983333 |
|
||||
| `-s install openuds-server` | 5.526667 | 0.100000 | 5.626667 |
|
||||
| `-s install password-store` | 2.940000 | 0.070000 | 3.010000 |
|
||||
|
||||
## original set.c
|
||||
|
||||
@@ -23,26 +28,24 @@
|
||||
1. `decode_base62_golomb`
|
||||
2. `decode_delta`
|
||||
|
||||
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status |
|
||||
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
|
||||
| -s check | 1.570747 | 1.621496 | 1.545001 | 1.545744 | 0 |
|
||||
| -s autoremove | 2.294502 | 2.312574 | 2.287133 | 2.283798 | 0 |
|
||||
| -s install rpm-build | 2.108121 | 2.142784 | 2.093474 | 2.088105 | 0 |
|
||||
| -s install openuds-server | 8.028098 | 8.147214 | 8.001140 | 7.935940 | 0 |
|
||||
| -s install password-store | 3.373377 | 3.396943 | 3.361290 | 3.361899 | 0 |
|
||||
| --enable-upgrade -s upgrade | 33.915979 | 33.895262 | 33.770192 | 34.082484 | 100 |
|
||||
| -s dist-upgrade | 88.722136 | 95.484250 | 85.808085 | 84.874073 | 100 |
|
||||
Коэффициенты посчитаны относительно `original set.c w/out optimizations`.
|
||||
|
||||
| command | user, с | system, с | user+system, с | коэфф. user | коэфф. user+system |
|
||||
| :-------------------------- | -------: | --------: | -------------: | ----------: | -----------------: |
|
||||
| `-s check` | 0.453333 | 0.033333 | 0.486666 | 2.912× | 2.815× |
|
||||
| `-s autoremove` | 0.763333 | 0.040000 | 0.803333 | 2.131× | 2.087× |
|
||||
| `-s install rpm-build` | 1.210000 | 0.050000 | 1.260000 | 2.408× | 2.368× |
|
||||
| `-s install openuds-server` | 3.746667 | 0.080000 | 3.826667 | 1.475× | 1.470× |
|
||||
| `-s install password-store` | 1.233333 | 0.050000 | 1.283333 | 2.384× | 2.345× |
|
||||
|
||||
## set9.c
|
||||
|
||||
Переписанная реализация `set.c`
|
||||
Переписанная реализация `set.c`. Коэффициенты посчитаны относительно `original set.c`.
|
||||
|
||||
| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status |
|
||||
| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: |
|
||||
| -s check | 1.550342 | 1.596307 | 1.532768 | 1.521950 | 0 |
|
||||
| -s autoremove | 2.200188 | 2.237645 | 2.185378 | 2.177540 | 0 |
|
||||
| -s install rpm-build | 2.031055 | 2.045757 | 2.023173 | 2.024234 | 0 |
|
||||
| -s install openuds-server | 7.580058 | 7.572994 | 7.582557 | 7.584622 | 0 |
|
||||
| -s install password-store | 3.337012 | 3.367696 | 3.309826 | 3.333514 | 0 |
|
||||
| --enable-upgrade -s upgrade | 31.494775 | 31.535714 | 31.585522 | 31.363090 | 100 |
|
||||
| -s dist-upgrade | 85.326580 | 85.389775 | 85.604566 | 84.985398 | 100 |
|
||||
| command | user, с | system, с | user+system, с | коэфф. user | коэфф. user+system |
|
||||
| :-------------------------- | -------: | --------: | -------------: | ----------: | -----------------: |
|
||||
| `-s check` | 0.503333 | 0.040000 | 0.543333 | 0.901× | 0.896× |
|
||||
| `-s autoremove` | 0.810000 | 0.040000 | 0.850000 | 0.942× | 0.945× |
|
||||
| `-s install rpm-build` | 1.283333 | 0.050000 | 1.333333 | 0.943× | 0.945× |
|
||||
| `-s install openuds-server` | 3.896667 | 0.083333 | 3.980000 | 0.962× | 0.961× |
|
||||
| `-s install password-store` | 1.310000 | 0.050000 | 1.360000 | 0.941× | 0.944× |
|
||||
|
||||
@@ -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,588 @@
|
||||
\Title{Гудов Д.О.}{Исследование алгоритма разрешения зависимостей в ALT RPM}
|
||||
|
||||
\section*{Введение}
|
||||
|
||||
Одной из задач пакетного менеджера является проверка совместимости устанавливаемого программного обеспечения с уже имеющимися или одновременно устанавливаемыми библиотеками. Традиционная зависимость от имени библиотеки и номера её версии не всегда достаточна: отдельный экспортируемый символ может быть удалён без изменения SONAME, а библиотеки с одинаковым SONAME могут предоставлять различные программные интерфейсы. В результате формально удовлетворённая зависимость от версии ещё не гарантирует, что динамический загрузчик найдёт все символы, необходимые программе.
|
||||
|
||||
В ALT RPM эта задача решается при помощи специальных версий зависимостей вида \texttt{set:<encoded-set>} [\Ref{SETC}]. Для отношения \texttt{Provides} такая строка описывает множество символов, экспортируемых библиотекой, а для отношения \texttt{Requires} --- символы, требуемые программой от конкретной библиотеки. При проверке зависимости обычное сравнение версий заменяется проверкой включения множеств. Полные имена символов при этом не сохраняются: они заменяются усечёнными хеш-значениями, сортируются и кодируются в компактную строку, допустимую как версия RPM.
|
||||
|
||||
Цель настоящей работы --- исследовать алгоритм построения и сравнения \texttt{set:}-строк, оценить вероятностные свойства хэша и вычислительную сложность алгоритма, а также определить направления оптимизации реализации.
|
||||
|
||||
%% В первых разделах формализуется решаемая задача, рассматриваются реальные данные репозитория Sisyphus, формат строки и путь её построения в исходной реализации \texttt{lib/set.c}.
|
||||
|
||||
\section{Постановка задачи и данные ALT Linux}
|
||||
\label{PROBLEM}
|
||||
|
||||
Пусть $P$ --- множество символов, предоставляемых библиотекой, а $R$ --- множество символов, требуемых от неё программой. Обозначим $p=|P|$ и $r=|R|$. Зависимость выполнима тогда и только тогда, когда
|
||||
\[
|
||||
R\subseteq P.
|
||||
\]
|
||||
|
||||
Списки символов формируются механизмом автоматического определения зависимостей \texttt{rpm-build}. Для \texttt{Provides} из динамической таблицы библиотеки выбираются доступные извне определённые символы. Для \texttt{Requires} утилита \texttt{ldd --bindings} устанавливает соответствие между требуемым символом и конкретной библиотекой-поставщиком; слабые неопределённые символы исключаются. Таким образом, одна строка \texttt{Requires} содержит только множество, связанное с данным поставщиком.
|
||||
|
||||
Явное хранение имён увеличивало бы RPM-метаданные пропорционально их суммарной длине. Поэтому используется усечённая хеш-функция c целым $b$, называемого в реализации \texttt{bpp},
|
||||
\[
|
||||
h_b(x)=h_{32}(x)\bmod 2^b,
|
||||
\qquad
|
||||
H_b(S)=\{h_b(x)\mid x\in S\},
|
||||
\]
|
||||
где $h_{32}$ --- 32-битная функция Jenkins one-at-a-time. Фактически \texttt{lib/set.c} проверяет условие
|
||||
\[
|
||||
H_b(R)\subseteq H_b(P).
|
||||
\]
|
||||
|
||||
Такое представление сохраняет включение: из $R\subseteq P$ следует $H_b(R)\subseteq H_b(P)$. Следовательно, коллизия хешей не создаёт ложного отказа для корректной зависимости. Обратное утверждение неверно: отсутствующий символ из $R\setminus P$ может получить то же усечённое значение, что и один из символов $P$, и привести к ложному принятию зависимости. Тем самым предоставляемая гарантия имеет вероятностный характер.
|
||||
|
||||
Для оценки реальной нагрузки был исследован срез репозитория Sisyphus для архитектур \texttt{x86\_64} и \texttt{noarch}. Объём рассмотренных метаданных на момент 2026-08-20 приведён в табл.~\Ref{CORPUS}.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{CORPUS}Объём исследованного среза Sisyphus}
|
||||
\small
|
||||
\begin{tabular}{|l|r|}
|
||||
\hline
|
||||
Объект & Количество \\
|
||||
\hline
|
||||
Пакеты & 47\,654 \\
|
||||
\hline
|
||||
Отношения \texttt{Provides} с \texttt{set:}-версией & 14\,859 \\
|
||||
\hline
|
||||
Отношения \texttt{Requires} с \texttt{set:}-версией & 69\,153 \\
|
||||
\hline
|
||||
Сопоставленные пары $P,R$ & 68\,492 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
661 отношений удовлетворены обычным неверсионированным \texttt{Provides}.
|
||||
|
||||
Для каждой сопоставленной пары декодировались мощности множеств $p$ и $r$, а также вычислялось индивидуальное отношение $p/r$. Квантили этих величин показаны в табл.~\Ref{CARDINALITIES}. Квантили отношения вычислялись непосредственно по парам.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{CARDINALITIES}Мощности множеств в парах \texttt{Provides}/\texttt{Requires}}
|
||||
\small
|
||||
\begin{tabular}{|c|r|r|r|}
|
||||
\hline
|
||||
Квантиль & $p$ & $r$ & $p/r$ \\
|
||||
\hline
|
||||
0,50 & 480 & 13 & 28,1 \\
|
||||
\hline
|
||||
0,75 & 1\,886 & 37 & 80 \\
|
||||
\hline
|
||||
0,90 & 6\,219 & 104 & 257 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
В 92,4\% сопоставленных пар выполняется $p/r\geq 4$. Следовательно, типичный проверяемый набор требований существенно разрежен относительно множества предоставляемых символов. Это наблюдение важно для алгоритма сравнения: последовательный симметричный просмотр двух массивов не всегда использует характерное различие их мощностей.
|
||||
|
||||
\section{Устройство существующей \texttt{set:}-строки}
|
||||
\label{FORMAT}
|
||||
|
||||
Построение \texttt{set:}-строки выполняется как последовательность преобразований
|
||||
\[
|
||||
\begin{aligned}
|
||||
\text{имена символов}
|
||||
&\longrightarrow \text{усечённые хеши}
|
||||
\longrightarrow \text{сортировка},\\
|
||||
&\longrightarrow \text{удаление повторов и вычисление дельт},\\
|
||||
&\longrightarrow \text{код Голомба--Райса}
|
||||
\longrightarrow \text{Base62-представление}.
|
||||
\end{aligned}
|
||||
\]
|
||||
Итоговая версия имеет вид
|
||||
\[
|
||||
\texttt{set:}\langle b\rangle\langle m\rangle\langle payload\rangle.
|
||||
\]
|
||||
Префикс \texttt{set:} распознаётся RPM как признак специальной версии. Следующие два символа кодируют параметры $b=\texttt{bpp}$ и $m=\texttt{Mshift}$ по правилу $c=v-7+\texttt{'a'}$. Допустимы $10\leq b\leq32$, $7\leq m\leq31$ и $m<b$. Оставшаяся часть строки содержит закодированный в \texttt{Base62} битовый поток.
|
||||
|
||||
\subsection{Хеширование и вероятность коллизий}
|
||||
|
||||
Для каждого имени вычисляется 32-битный Jenkins one-at-a-time [\Ref{JENKINS}], после чего сохраняются только $b$ младших бит. Пусть $n$ различных имён независимо и равномерно отображаются в пространство из $N=2^b$ значений. Тогда математическое ожидание числа столкнувшихся пар равно
|
||||
|
||||
\[
|
||||
\mathbb{E}C=\binom{n}{2}\frac{1}{2^b}
|
||||
=\frac{n(n-1)}{2^{b+1}},
|
||||
\]
|
||||
а вероятность хотя бы одной коллизии имеет вид
|
||||
\[
|
||||
\P(C>0)=1-\prod_{i=0}^{n-1}\left(1-\frac{i}{2^b}\right)
|
||||
\approx 1-\exp\left(-\frac{n(n-1)}{2^{b+1}}\right).
|
||||
\]
|
||||
Эти выражения являются модельными: Jenkins OAAT не является случайным оракулом, поэтому окончательная оценка должна дополняться измерением коллизий на реальном корпусе символов.
|
||||
|
||||
Эвристическую верхнюю оценку ложного принятия при $k=|R\setminus P|$ отсутствующих символов можно записать как
|
||||
\[
|
||||
\Pr\bigl(H_b(R)\subseteq H_b(P)\mid R\nsubseteq P\bigr)
|
||||
\lesssim
|
||||
\left(\frac{|H_b(P)|}{2^b}\right)^k.
|
||||
\]
|
||||
Увеличение $b$ снижает вероятность ошибки, но увеличивает кодируемые значения и длину строки. Поэтому выбор \texttt{bpp} представляет собой компромисс между компактностью метаданных и риском коллизий; при формировании \texttt{set:}-строки для \texttt{Requires} набора используется точность, определённая по числу символов соответствующего \texttt{Provides}, а не по обычно намного меньшему числу требований.
|
||||
|
||||
\subsection{Кодирование Голомба--Райса}
|
||||
|
||||
После сортировки уникальных значений
|
||||
\[
|
||||
0\leq x_1<x_2<\dots<x_n<2^b
|
||||
\]
|
||||
они заменяются дельтами
|
||||
\[
|
||||
\delta_1=x_1,
|
||||
\qquad
|
||||
\delta_i=x_i-x_{i-1},\quad i=2,\dots,n.
|
||||
\]
|
||||
Для равномерных хешей средний промежуток имеет порядок $2^b/n$, поэтому дельты значительно меньше абсолютных значений и хорошо подходят для кодирования Голомба--Райса [\Ref{GOLOMB-RICE}].
|
||||
|
||||
В реализации модуль $M$ равен степени двойки $M=2^m$. Для каждой дельты вычисляются
|
||||
\[
|
||||
q_i=\left\lfloor\frac{\delta_i}{M}\right\rfloor,
|
||||
\qquad
|
||||
r_i=\delta_i\bmod M.
|
||||
\]
|
||||
Частное $q_i$ записывается унарно как $q_i$ нулей и завершающая единица, после которой следуют $m$ младших бит остатка $r_i$. Длина кода одного значения равна
|
||||
\[
|
||||
\ell_i=q_i+1+m.
|
||||
\]
|
||||
Параметр выбирается приближённо как
|
||||
\[
|
||||
m=b-\lfloor\log_2 n\rfloor-1
|
||||
\]
|
||||
и ограничивается диапазоном формата $7\leq m\leq31$. При принятой модели суммарная длина битового потока оценивается как
|
||||
\[
|
||||
B=\sum_{i=1}^{n}\ell_i
|
||||
=O\!\left(n\left(1+\log_2\frac{2^b}{n}\right)\right).
|
||||
\]
|
||||
|
||||
\subsection{Base62-представление}
|
||||
|
||||
Битовый поток нельзя непосредственно поместить в поле версии RPM: необходима строка из допустимых символов. Исходная реализация использует алфавит \texttt{0--9}, \texttt{a--z}, \texttt{A--Z}. Значения от 0 до 60 записываются обычным символом, а \texttt{Z} служит escape-символом и позволяет представить также шестибитные значения 61, 62 и 63. Их различают два старших бита следующего символа: \texttt{00}, \texttt{01} или \texttt{10}. Комбинация \texttt{11} не используется, поэтому escape-последовательности не могут образовать неоднозначную цепочку. Вследствие этого один символ несёт от пяти до шести бит, и длина текстовой части приближённо пропорциональна $B/6$.
|
||||
|
||||
Рассмотрим небольшой пример с $b=10$. Для пяти имён Jenkins OAAT после усечения даёт значения, приведённые в табл.~\Ref{ENCODING_EXAMPLE}.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{ENCODING_EXAMPLE}Пример хеширования символов при $b=10$}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|}
|
||||
\hline
|
||||
Символ & 32-битный хеш & $h_{10}$ \\
|
||||
\hline
|
||||
\texttt{fclose} & \texttt{0x6743def6} & 758 \\
|
||||
\hline
|
||||
\texttt{fopen} & \texttt{0x0e984918} & 280 \\
|
||||
\hline
|
||||
\texttt{free} & \texttt{0xa5bbbac7} & 711 \\
|
||||
\hline
|
||||
\texttt{malloc} & \texttt{0x07c1b8ff} & 255 \\
|
||||
\hline
|
||||
\texttt{printf} & \texttt{0xfd1ad5cc} & 460 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
После сортировки получается массив $(255,280,460,711,758)$, а после вычисления разностей --- $(255,25,180,251,47)$. Для $n=5$ формула даёт $m=7$; полученный 43-битный поток преобразуется в payload \texttt{ZvpACXZy}. Параметрам $b=10$ и $m=7$ соответствуют заголовочные символы \texttt{d} и \texttt{a}, поэтому итоговая строка равна \texttt{set:daZvpACXZy}.
|
||||
|
||||
\section{Построение \texttt{set:}-строк в \texttt{set.c}}
|
||||
\label{CONSTRUCTION}
|
||||
|
||||
Благодаря сценариям автозависимостей \texttt{rpm-build} утилита \texttt{mkset} получает символы из ELF-файлов и далее использует \texttt{lib/set.c}, вызывая интерфейс построения множества:
|
||||
\[
|
||||
\texttt{set\_new()}\ \longrightarrow\
|
||||
\texttt{set\_add()}\ \longrightarrow\
|
||||
\texttt{set\_fini()}.
|
||||
\]
|
||||
|
||||
Функция \texttt{set\_new()} создаёт пустую структуру \texttt{struct set}. В исходной реализации она представляет собой растущий массив пар «указатель на строку --- хеш». Функция \texttt{set\_add()} увеличивает ёмкость массива блоками по 1024 элемента и копирует каждое имя отдельным вызовом \texttt{xstrdup()}. На этом этапе хеши ещё не вычисляются, поэтому один объект можно финализировать с заданным значением \texttt{bpp}.
|
||||
|
||||
Основную работу выполняет \texttt{set\_fini()}. Её действия следуют в фиксированном порядке:
|
||||
\begin{enumerate}
|
||||
\item проверяется непустота множества и условие $10\leq b\leq32$;
|
||||
\item для каждого имени вычисляется Jenkins OAAT и применяется маска из $b$ младших бит;
|
||||
\item массив пар сортируется стандартной функцией \texttt{qsort()} по хеш-значению;
|
||||
\item для равных хешей различных имён выводится предупреждение о коллизии;
|
||||
\item хеши копируются в числовой массив, а повторяющиеся значения удаляются;
|
||||
\item массив кодируется последовательно как дельты, код Голомба--Райса и Base62;
|
||||
\item сформированная строка копируется в динамическую память и возвращается вызывающей стороне.
|
||||
\end{enumerate}
|
||||
|
||||
Удаление повторов необходимо в двух случаях. Во-первых, одно имя может несколько раз попасть во входной поток. Во-вторых, различные имена могут совпасть после усечения хеша. Для семантики $H_b(S)$ оба случая означают один элемент множества. При этом предупреждение о коллизии формируется до удаления повторов, пока реализации ещё доступны исходные строки и можно различить совпадение имён от совпадения их хешей.
|
||||
|
||||
Оценим вычислительную сложность. Пусть $L$ --- суммарная длина всех входных имён, $n$ --- их количество, а $B$ --- длина потока Голомба--Райса в битах. Хеширование требует $O(L)$ операций, сортировка --- $O(n\log n)$ сравнений, линейные проходы сортированного массива --- $O(n)$, кодирование --- $O(B)$. Поэтому для исходного варианта
|
||||
\[
|
||||
T_{\mathrm{build}}=O(L)+O(n\log n)+O(B).
|
||||
\]
|
||||
Хранимые копии строк, массив пар, массив уникальных хешей, битовый буфер и результирующая строка дают дополнительную память порядка
|
||||
\[
|
||||
M_{\mathrm{build}}=O(L+n+B).
|
||||
\]
|
||||
Главным асимптотическим слагаемым для больших наборов остаётся \texttt{qsort()}, тогда как большое число отдельных копирований строк и наличие промежуточного битового массива влияют на постоянные затраты. Это определяет два независимых направления дальнейшего улучшения: замена сортировки для крупных множеств и потоковое кодирование без промежуточных представлений. При этом любые изменения должны сохранять байтовую совместимость формата.
|
||||
|
||||
\section{Сравнение \texttt{set:}-строк}
|
||||
\label{COMPARISON}
|
||||
|
||||
Функция \texttt{rpmsetcmp()} получает две строки и определяет отношение включения между закодированными множествами. В штатном пути RPM первым операндом передаётся \texttt{Provides}, а вторым --- \texttt{Requires}. Возвращаемые значения различают четыре результата: 1, если первое множество строго содержит второе; 0 при равенстве; $-1$, если первое множество строго содержится во втором; $-2$, если множества несравнимы по включению. Ошибки декодирования первого и второго операндов отображаются соответственно в коды $-3$ и $-4$.
|
||||
|
||||
\subsection{Обратное декодирование и нормализация точности}
|
||||
|
||||
После удаления необязательного префикса \texttt{set:} проверяются параметры $b$ и $m$, а затем выполняется обратная цепочка преобразований
|
||||
\[
|
||||
\begin{aligned}
|
||||
\text{Base62-строка}
|
||||
&\longrightarrow \text{битовый поток}
|
||||
\longrightarrow \text{дельты Голомба--Райса},\\
|
||||
&\longrightarrow \text{возрастающий массив хешей}.
|
||||
\end{aligned}
|
||||
\]
|
||||
Логически это преобразование обратно рассмотренному в разд.~\Ref{FORMAT}. Однако исходный \texttt{lib/set.c} не создаёт отдельный битовый массив: функция \texttt{decode\_base62\_golomb()} объединяет первые две стадии. Она считывает обычные символы попарно, преобразует их предварительно вычисленной таблицей и обрабатывает блоки до 24 бит. Частное и остаток кода Голомба--Райса восстанавливаются непосредственно из этих блоков. После этого единственный линейный проход суммирует дельты и получает исходные усечённые хеши.
|
||||
|
||||
Две строки могут иметь разные значения \texttt{bpp}. Сравнение выполняется при общей точности
|
||||
\[
|
||||
b_* = \min(b_P,b_R).
|
||||
\]
|
||||
Для этого более точный массив последовательно проецируется на один бит вниз. На одном шаге применяется отображение
|
||||
\[
|
||||
\pi_t(x)=x\bmod 2^t.
|
||||
\]
|
||||
После удаления старшего бита исходный возрастающий массив распадается на две возрастающие части. Обе части сливаются с сохранением возрастания элементов, а появившиеся совпадения удаляются. Поэтому после понижения точности сохраняются одновременно сортировка и представление множества без повторов. При разности точностей $d=|b_P-b_R|$ слияние повторяется $d$ раз.
|
||||
|
||||
В результате сравниваются два возрастающих массива
|
||||
\[
|
||||
\widehat P=H_{b_*}(P),\qquad
|
||||
\widehat R=H_{b_*}(R).
|
||||
\]
|
||||
|
||||
Понижение \texttt{bpp} необходимо, так как сравнение множеств хэшей отличающейся точности невозможно.
|
||||
|
||||
\subsection{Кэш декодированных множеств}
|
||||
|
||||
Декодирование длинной строки существенно дороже проверки нескольких уже готовых целых значений. Кроме того, один поставщик обычно сопоставляется с требованиями нескольких пакетов. Поэтому исходная реализация кэширует декодированный первый операнд, то есть в обычном вызове множество \texttt{Provides}.
|
||||
|
||||
Кэш состоит из двух массивов по 256 элементов: коротких отпечатков и указателей на записи. Отпечаток формируется из трёх байтов строки и служит только предварительным фильтром. Корректность попадания подтверждается полным сравнением исходной строки. Запись одним выделением памяти хранит декодированный массив, его длину и копию строки.
|
||||
|
||||
Поиск выполняется линейно. При попадании запись перемещается в начало двумя вызовами \texttt{memmove()}. Если кэш заполнен, последняя запись освобождается, а новый элемент вставляется на позицию 243 (\texttt{PIVOT\_SIZE}), после чего хвост обоих массивов также сдвигается. Такая схема приближает LRU-политику, но не является строгим LRU: новая, ещё не подтвердившая полезность запись помещается около конца кэша, тогда как повторно использованная попадает в начало.
|
||||
|
||||
Второй операнд исходная реализация каждый раз декодирует во временный массив на стеке. Кроме того, кэш хранит результат при исходном \texttt{bpp}; если для конкретного сравнения его требуется понизить, проекция вычисляется заново. Следовательно, кэш особенно полезен для потока сравнений одного \texttt{Provides} с разными \texttt{Requires} одинаковой точности, но не устраняет стоимость декодирования требований и нормализации.
|
||||
|
||||
\subsection{Проверка включения возрастающих массивов}
|
||||
|
||||
Наивная проверка $\widehat R\subseteq\widehat P$ последовательно продвигает указатель по $\widehat P$ до очередного требуемого значения. Исходная реализация одновременно вычисляет оба отношения включения с помощью флагов \texttt{ge} и \texttt{le}. Это позволяет одним проходом получить все четыре значения публичного API, а не только ответ на типичный для RPM вопрос $\widehat R\subseteq\widehat P$.
|
||||
|
||||
Для ускорения поиска используются макросы \texttt{IFLT4} и \texttt{IFLT8}. Первый перемещает указатель по массиву первого операнда блоками по четыре элемента, после превышения искомого значения возвращается на два элемента и уточняет позицию единичными шагами. Второй выполняет ту же схему с начальным шагом восемь и уточнениями 4, 2 и 1. В конец массива добавляются восемь значений-сентинелов $\mathtt{UINT\_MAX}$, благодаря чему пробный прыжок за последний настоящий элемент остаётся допустимым обращением к памяти. Макрос \texttt{IFLT8} выбирается при $p\geq16r$, в остальных случаях используется \texttt{IFLT4}.
|
||||
|
||||
Такой выбор учитывает разреженность требований, установленную в разд.~\Ref{PROBLEM}, но лишь двумя фиксированными режимами. При медианном $p/r=28{,}1$ шаг восемь уже применим, тогда как при $p/r=257$ он всё ещё остаётся равным восьми и не отражает фактическое среднее расстояние между требуемыми значениями.
|
||||
|
||||
Пусть $s_P$ и $s_R$ --- длины payload двух строк, а $d=|b_P-b_R|$. Холодное сравнение имеет оценку
|
||||
\[
|
||||
T_{\mathrm{cold}}=
|
||||
O(s_P+s_R)+O\bigl(d\max(p,r)\bigr)+O(p+r).
|
||||
\]
|
||||
Поскольку формат ограничивает $10\leq b\leq32$, величина $d$ ограничена константой, и итоговая асимптотика линейна по размеру входных строк и декодированных множеств. При попадании первого операнда в кэш исчезает его декодирование, но остаются линейный поиск по кэшу, проверка строки, декодирование второго операнда, возможная нормализация и проход по массивам:
|
||||
\[
|
||||
T_{\mathrm{hit}}=
|
||||
O(C+s_P+s_R)+O\bigl(d\max(p,r)\bigr)+O(p+r),
|
||||
\qquad C=256.
|
||||
\]
|
||||
Здесь член $s_P$ отражает окончательную проверку ключа; на практике она выполняется только для записей с совпавшим коротким отпечатком. Дополнительная память одного холодного вызова составляет $O(p+r)$, не считая сохраняемого в процессе кэша первого операнда.
|
||||
|
||||
\section{Оптимизации с сохранением текущего формата}
|
||||
\label{COMPATIBLE_OPTIMIZATIONS}
|
||||
|
||||
Для проверки направлений оптимизации была создана экспериментальная реализация \texttt{set9.c} [\Ref{SET9}]. Она сохраняет Jenkins OAAT, формат заголовка, Golomb--Rice/Base62-представление и пять функций публичного API. Поэтому сформированные ею строки побайтово совместимы с исходной реализацией, а изменения относятся только к внутренним структурам и алгоритмам.
|
||||
|
||||
Помимо оптимизации сравнения, построение множества было переведено с отдельных \texttt{xstrdup()} для каждого имени на общую растущую строковую арену. В массиве элементов хранятся смещения, которые остаются корректными после \texttt{xrealloc()} арены. Кодирование и декодирование выполняются потоково: дельты, состояния Голомба--Райса и Base62 обрабатываются через 64-битный аккумулятор без отдельных массивов битов и дельт.
|
||||
|
||||
\subsection{Два bucketed LRU-кэша}
|
||||
|
||||
Вместо одного линейного кэша используются два независимых кэша --- для первого и второго операндов. Каждый содержит до 512 записей и таблицу из 1024 бакетов. Разделение предотвращает взаимное вытеснение часто повторяющихся \texttt{Provides} и \texttt{Requires}, а бакеты ограничивают область поиска записи.
|
||||
|
||||
Ключ включает короткий отпечаток исходной строки и целевую точность $b_*$. Совпадение, как и в исходном варианте, обязательно подтверждается полным \texttt{strcmp()}, поэтому ускоряющая структура не меняет семантику. Значение $b_*$ принципиально важно: одна строка может сравниваться с операндами разной точности и давать разные нормализованные массивы.
|
||||
|
||||
При промахе строка декодируется, сразу приводится к $b_*$ и в таком виде сохраняется. Повторное сравнение той же строки при той же точности не требует ни декодирования, ни цикла \texttt{downsample\_set()}. Порядок вытеснения поддерживается двусвязным списком: попадание переносит запись в начало за $O(1)$, а при заполнении удаляется самый старый элемент. В отличие от массивного кэша, полный сдвиг записей не требуется. При равномерном распределении отпечатков ожидаемая стоимость поиска близка к $O(1)$ плюс стоимость окончательной проверки строки.
|
||||
|
||||
\subsection{Radix sort для больших множеств}
|
||||
|
||||
При построении строки исходный \texttt{qsort()} требует $O(n\log n)$ вызовов функции сравнения. Для 32-битных целых ключей число разрядов заранее ограничено, поэтому в \texttt{set9.c} при $n\geq128$ применяется стабильная LSD radix sort по байтам хеша. Для меньших наборов сохраняется \texttt{qsort()}, поскольку подготовка таблиц и временного массива не окупается.
|
||||
|
||||
Число проходов определяется фактически используемой точностью:
|
||||
\[
|
||||
k=\left\lceil\frac b8\right\rceil,
|
||||
\qquad
|
||||
T_{\mathrm{radix}}=O(kn).
|
||||
\]
|
||||
На каждом проходе сначала подсчитываются 256 значений текущего байта, затем префиксные суммы преобразуют счётчики в позиции, после чего элементы стабильно распределяются во временный массив. Источник и приёмник меняются местами между проходами. Дополнительная память равна
|
||||
\[
|
||||
M_{\mathrm{radix}}=O(n+256).
|
||||
\]
|
||||
Так как $b\leq32$, выполняется не более четырёх линейных проходов.
|
||||
|
||||
\subsection{Адаптивная проверка включения}
|
||||
|
||||
Мощности массивов после нормализации позволяют заранее исключить часть отношений. Если $p=r$, строгого включения быть не может: равенство проверяется одним \texttt{memcmp()}, а несовпадение означает несравнимость. Если $p>r$, проверяется только $\widehat R\subseteq\widehat P$; обратное строгое включение невозможно. Случай $p<r$ обрабатывается симметрично. Тем самым устраняется одновременное ведение двух флагов и выбирается единственная содержательная проверка.
|
||||
|
||||
Функция \texttt{sorted\_subset()} использует отношение мощностей
|
||||
\[
|
||||
j=\left\lfloor\frac{n_{\mathrm{large}}}{n_{\mathrm{small}}}\right\rfloor.
|
||||
\]
|
||||
При $j<4$ выполняется обычное линейное слияние. Для разреженного случая поиск очередного элемента начинается от позиции предыдущего совпадения, делает шаг приблизительно $j$, а затем делит шаг пополам до нахождения нижней границы. Указатель по большому массиву движется только вперёд; при первом отсутствующем хеше функция немедленно возвращает отрицательный результат.
|
||||
|
||||
Для плотного случая сохраняется оценка $O(p+r)$. В разреженном потоке суммарное число крупных шагов имеет порядок $O(p/j)$, а уточнение границы требует до $O(\log j)$ проб на один элемент меньшего множества:
|
||||
\[
|
||||
T_{\mathrm{sparse}}=
|
||||
O\left(\frac pj+r\log j\right).
|
||||
\]
|
||||
При характерном $j\approx p/r$ первое слагаемое имеет порядок $r$. В отличие от фиксированного выбора между шагами 4 и 8, величина перехода адаптируется к наблюдаемому отношению мощностей.
|
||||
|
||||
\section{Результаты экспериментальной проверки}
|
||||
\label{RESULTS}
|
||||
|
||||
Проверка экспериментальной реализации проводилась в два этапа. Сначала проверялась корректность формата и операций над множествами, затем измерялась производительность в сценариях пакетного менеджера.
|
||||
|
||||
\subsection{Дифференциальное тестирование на синтетических множествах}
|
||||
|
||||
Для проверки совместимости был разработан дифференциальный тест, не использующий заранее подготовленные \texttt{set:}-строки. На каждой итерации он самостоятельно формировал исходное множество из случайных уникальных строк. Число символов выбиралось в диапазоне от 1 до 1000, длина каждого имени --- от 1 до 100 знаков, а параметр $bpp$ --- от 10 до 32. В алфавит входили латинские буквы, цифры и знаки, встречающиеся в именах экспортируемых символов: точка, знак \texttt{@} и подчёркивание.
|
||||
|
||||
Из исходного множества строилось второе множество с заранее известным отношением к первому. Тест охватывал четыре класса входов: равные множества, строгое включение, несравнимые множества и некорректные \texttt{set:}-строки. Для проверки строгого включения из первого множества удалялась случайная непустая часть элементов. В случае несравнимости после удаления добавлялись новые символы, отсутствующие в первом множестве. Некорректная строка генерировалась случайным набором символов, в таком случае был шанс получить корректную строку, но для нас остаётся важным одинаковый результат двух программ.
|
||||
|
||||
Один и тот же набор исходных символов независимо передавался построителю исходного \texttt{set.c} и построителю \texttt{set9.c}. Таким образом, каждая реализация сама выполняла хеширование, сортировку и кодирование, после чего её вариант \texttt{rpmsetcmp()} сравнивал полученную пару строк. Тест сопоставлял наблюдаемый результат публичного API: $1$ для строгого надмножества, $0$ для равенства, $-1$ для строгого подмножества, $-2$ для несравнимости и $-3$ или $-4$ для ошибки декодирования соответствующего операнда. Для случая включения операнды дополнительно менялись местами, что позволяло одновременно проверить результаты $1$ и $-1$.
|
||||
|
||||
Генерация и сравнение выполнялись циклически до ручной остановки теста. Такой подход проверял эквивалентность реализаций на широком диапазоне мощностей и точностей.
|
||||
|
||||
\subsection{Производительность в сценариях APT}
|
||||
|
||||
Для измерения времени использовались две локальные сборки \texttt{librpm}: с исходным \texttt{set.c} и с \texttt{set9.c}. На каждой сборке трижды выполнялись одинаковые успешно завершившиеся симуляции APT. В табл.~\Ref{APT_RESULTS} приведены средние значения процессорного времени пользователя и ядра. Время ожидания и ввода-вывода в эти величины не входит.
|
||||
|
||||
Коэффициент вычислялся по суммарному процессорному времени:
|
||||
\[
|
||||
K_{\mathrm{CPU}}=
|
||||
\frac{U_{\mathrm{original}}+S_{\mathrm{original}}}
|
||||
{U_{\mathrm{set9}}+S_{\mathrm{set9}}}.
|
||||
\]
|
||||
Значение $K_{\mathrm{CPU}}>1$ означает ускорение, а $K_{\mathrm{CPU}}<1$ --- замедление экспериментальной реализации.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{APT_RESULTS}Сравнение исходного \texttt{set.c} и \texttt{set9.c} в симуляциях APT}
|
||||
\scriptsize
|
||||
\begin{tabular}{|l|r|r|r|r|r|}
|
||||
\hline
|
||||
Сценарий & \multicolumn{2}{c|}{Исходный, с} & \multicolumn{2}{c|}{\texttt{set9.c}, с} & $K_{\mathrm{CPU}}$ \\
|
||||
\cline{2-5}
|
||||
& user & system & user & system & \\
|
||||
\hline
|
||||
\texttt{-s check} & 0,453 & 0,033 & 0,503 & 0,040 & 0,896 \\
|
||||
\hline
|
||||
\texttt{-s autoremove} & 0,763 & 0,040 & 0,810 & 0,040 & 0,945 \\
|
||||
\hline
|
||||
\texttt{-s install rpm-build} & 1,210 & 0,050 & 1,283 & 0,050 & 0,945 \\
|
||||
\hline
|
||||
\texttt{-s install openuds-server} & 3,747 & 0,080 & 3,897 & 0,083 & 0,961 \\
|
||||
\hline
|
||||
\texttt{-s install password-store} & 1,233 & 0,050 & 1,310 & 0,050 & 0,944 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Во всех пяти измеренных сценариях \texttt{set9.c} не превзошёл исходный вариант: суммарное процессорное время увеличилось примерно на 4,0--11,6\%. Это отрицательный результат, вызванный более простым потоковым декодером, который не превзошёл специализированного табличного декодера исходной реализации даже при условии остальных оптимизаций.
|
||||
|
||||
Для проверки этого объяснения исходный слитый декодер отдельно сравнивался с вариантом, в котором Base62, код Голомба--Райса и восстановление дельт выполнялись последовательными стадиями. В зависимости от сценария слитый путь сокращал суммарное процессорное время в 1,47--2,82 раза. Следовательно, благодаря табличной обработке Base62/Golomb получается измеримый выигрыш. Практическое направление дальнейшей работы может состоять в разработке нового формата хранения и обработки \texttt{set:}-строк.
|
||||
|
||||
\section{Варианты со сменой формата или API}
|
||||
\label{ALTERNATIVE_FORMATS}
|
||||
|
||||
Оптимизации разд.~\Ref{COMPATIBLE_OPTIMIZATIONS} ограничены требованием побайтовой совместимости. Параллельно исследовались представления, снимающие это ограничение. Их результаты нельзя напрямую переносить на существующие RPM-метаданные: новый формат требует повторного формирования зависимостей репозитория.
|
||||
|
||||
\subsection{Roaring Bitmap: отрицательный результат}
|
||||
|
||||
Roaring Bitmap [\Ref{ROARING}] предназначен прежде всего для множеств, содержащих плотные участки целочисленного пространства. Усечённые хеши, напротив, распределены по диапазону приблизительно равномерно. Поэтому контейнеры bitmap не получают длинных серий соседних значений, но сохраняют собственные заголовки и индексы.
|
||||
|
||||
В микротесте использовались 1000 предоставляемых и 500 требуемых символов при $b=32$. Результаты приведены в табл.~\Ref{ROARING_RESULTS}. Коэффициенты времени вычислены относительно \texttt{set9.c}; значение больше единицы означает замедление.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{ROARING_RESULTS}Сравнение Golomb/Base62 и Roaring Bitmap}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|r|}
|
||||
\hline
|
||||
Представление & Длина строки & Cold compare & Warm compare \\
|
||||
\hline
|
||||
Golomb/Base62 \texttt{set9} & 3\,994 & 1,00 & 1,00 \\
|
||||
\hline
|
||||
Roaring/hex & 19\,924 & 4,50 & 76,14 \\
|
||||
\hline
|
||||
Roaring+zstd & 14\,126 & 4,96 & 101,86 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Без дополнительного сжатия строка оказалась длиннее в 4,99 раза. Холодное сравнение заняло 1321,17 мкс вместо 293,87 мкс, а прогретое --- 399,20 мкс вместо 5,24 мкс. При этом построение bitmap занимало лишь $0,76$ времени \texttt{set\_fini()} варианта \texttt{set9}; ускорение генерации не компенсировало стоимость хранения и сравнения.
|
||||
|
||||
Сжатие zstd [\Ref{ZSTD}] уменьшило строку до 14\,126 символов, то есть до 3,54 длины \texttt{set9}, но добавило декомпрессию в горячий путь. В отдельном прогоне холодное и прогретое сравнения были медленнее соответственно в 4,96 и 101,86 раза. Таким образом, Roaring Bitmap для равномерных хешей является подтверждённым отрицательным вариантом: структура данных не соответствует распределению кодируемых значений.
|
||||
|
||||
\subsection{Прямые хеши в Base64}
|
||||
|
||||
Другой вариант, обозначенный как D1, исключает дельты и код Голомба--Райса. $n$ отсортированных уникальных хешей записываются подряд по $b$ бит, после чего битовый массив преобразуется в Base64-строку [\Ref{BASE64}]. Если
|
||||
\[
|
||||
Q=\left\lceil\frac{nb}{8}\right\rceil
|
||||
\]
|
||||
--- число байтов упакованного массива, то доминирующая часть длины payload равна
|
||||
\[
|
||||
L_{\mathrm{payload}}=
|
||||
\left\lceil\frac{4Q}{3}\right\rceil,
|
||||
\]
|
||||
а заголовок формата добавляет постоянное число символов. В отличие от Golomb--Rice, эта оценка почти не зависит от расстояний между соседними хешами: цена быстрого произвольного доступа --- увеличение метаданных.
|
||||
|
||||
Для тех же 1000/500 символов при $b=32$ длина строки выросла с 3994 до 5340 символов, то есть на 33,7\%. Результаты теста показаны в табл.~\Ref{DIRECT_RESULTS}; отношение меньше единицы означает, что D1 затратил меньшую долю времени \texttt{set9.c}.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{DIRECT_RESULTS}Тест прямого представления D1}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|r|}
|
||||
\hline
|
||||
Операция & \texttt{set9}, мкс & D1, мкс & D1/\texttt{set9} \\
|
||||
\hline
|
||||
\texttt{set\_fini()} & 147,45 & 105,09 & 0,71 \\
|
||||
\hline
|
||||
\texttt{rpmsetcmp()}, cold & 129,67 & 113,04 & 0,87 \\
|
||||
\hline
|
||||
\texttt{rpmsetcmp()}, warm & 2,99 & 1,04 & 0,35 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Для крупного требования прямое представление ускорило все три измеряемые операции, особенно попадание в кэш. Однако при одном требуемом символе холодное сравнение занимало 83,22 мкс ($0,88$ времени \texttt{set9}), а прогретое --- 0,86 мкс ($1,02$), то есть преимущество исчезало. Это подчёркивает зависимость результата от формы нагрузки: выигрыш одного сравнения не доказывает ускорение полного потока зависимостей APT. D1 представляет интерес как компромисс между размером и стоимостью декодирования, но требует отдельной сквозной проверки на преобразованных метаданных репозитория.
|
||||
|
||||
\subsection{Альтернативные хеш-функции}
|
||||
|
||||
Отдельно сравнивались Jenkins OAAT, xxHash32 [\Ref{XXHASH}] и CityHash32 [\Ref{CITYHASH}]. Сравнение было необходимо, так как xxHash и CityHash --- хеш-функции, разработанные значительно позже последнего обновления \texttt{set.c}. В каждом замере использовались три запуска; в табл.~\Ref{HASH_SPEED} приведены медианы. Для коротких строк указано время одного хеширования, для длинных --- пропускная способность.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{HASH_SPEED}Скорость 32-битных хеш-функций}
|
||||
\small
|
||||
\begin{tabular}{|l|r|r|}
|
||||
\hline
|
||||
Функция & 32 байта, нс/хеш & 1024 байта, ГиБ/с \\
|
||||
\hline
|
||||
Jenkins OAAT & 50,98 & 0,53 \\
|
||||
\hline
|
||||
xxHash32 & 11,56 & 5,18 \\
|
||||
\hline
|
||||
CityHash32 & 16,70 & 4,10 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
На строках длиной 32 байта xxHash32 был быстрее Jenkins примерно в 4,4 раза, CityHash32 --- в 3,1 раза. При 1024 байтах различие по пропускной способности достигало соответственно 9,8 и 7,7 раза. Следовательно, Jenkins OAAT не является оптимальным по скорости, особенно на длинных входах.
|
||||
|
||||
Для оценки качества использовался другой тест, в нём сравнивались Jenkins OAAT, 64-битный xxHash и \texttt{t1ha2\_atonce} [\Ref{T1HA}]. Поскольку формат \texttt{set:version} сохраняет не более 32 бит хеша, при сопоставлении учитывались младшие 32 выходных бита всех трёх функций.
|
||||
|
||||
Тест измерял лавинный эффект на реальном корпусе экспортируемых C++-символов ALT Linux p11. Утилита \texttt{provided\_symbols} извлекла из 259 ELF-файлов пяти пакетов 577\,509 уникальных имён. Пакеты выбирались среди имеющих наиболее длинные \texttt{Provides set:}-строки. Корпус специально является сложным для хеширования похожих строк: 99,60\% символов имеют с другим символом общий префикс длиной не менее 12 знаков, 95,05\% --- не менее 24 знаков, а медиана максимального общего префикса равна 61 знаку.
|
||||
|
||||
Для каждого бита вычислялась доля $q_i$ корпусных хешей, в которых этот бит равен единице. Идеальному распределению соответствует $q_i=0{,}5$. Результаты обоих тестов представлены в табл.~\Ref{HASH_QUALITY_EXOTIC}. Значения приведены в процентах от полной шкалы вероятности; последний столбец содержит максимальное среднее абсолютное отклонение младших 32 бит от $0{,}5$.
|
||||
|
||||
Среднее абсолютное отклонение по младшим 32 битам вычислялось как:
|
||||
\[
|
||||
\overline{\Delta}=\frac{1}{32}\sum_{i=0}^{31}|p_i-0{,}5|.
|
||||
\]
|
||||
Меньшее значение $\overline{\Delta}$ соответствует более равномерному лавинному эффекту.
|
||||
|
||||
\begin{table}[H]
|
||||
\begin{center}
|
||||
\caption{\label{HASH_QUALITY_EXOTIC}Качество перемешивания младших 32 бит хеш-функций}
|
||||
\scriptsize
|
||||
\begin{tabular}{|l|r|r|r|r|}
|
||||
\hline
|
||||
Функция & \makecell{Корпус\\$\overline{\Delta}$, \%} & \makecell{Корпус\\$\Delta_{\max}$, \%} \\
|
||||
\hline
|
||||
Jenkins OAAT & 0,0475 & 0,167 \\
|
||||
\hline
|
||||
xxHash64 & 0,0431 & 0,122 \\
|
||||
\hline
|
||||
t1ha2 & 0,0615 & 0,188 \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{table}
|
||||
|
||||
Все три функции продемонстрировали близкое к равномерному распределение. На реальном корпусе лучший результат получен для xxHash64, однако разность между функциями составляла сотые доли процента, а максимальное отклонение отдельного бита не превысило 0,188\%.
|
||||
|
||||
Таким образом, тестирование на похожих C++-символах не выявило недостаточного перемешивания Jenkins OAAT. xxHash64 и t1ha2 остаются кандидатами для отдельной оценки производительности, но качество распределения само по себе не обосновывает замену Jenkins.
|
||||
|
||||
\References
|
||||
\begin{enumerate}
|
||||
\item
|
||||
\Label{SETC}
|
||||
Tourbin A.
|
||||
\emph{set.c --- base62, Golomb and set-string routines}.
|
||||
ALT RPM source code, 2010--2012.
|
||||
[Электронный ресурс] URL: \url{https://git.altlinux.org/gears/r/rpm.git?a=blob;f=lib/set.c}.
|
||||
|
||||
\item
|
||||
\Label{JENKINS}
|
||||
Jenkins B.
|
||||
\emph{A Hash Function for Hash Table Lookup}.
|
||||
1997, обновлено в 2013 г.
|
||||
[Электронный ресурс] URL: \url{https://burtleburtle.net/bob/hash/doobs.html}.
|
||||
|
||||
\item
|
||||
\Label{GOLOMB-RICE}
|
||||
Wikipedia contributors.
|
||||
\emph{Golomb coding: описание кодов Голомба и Райса}.
|
||||
[Электронный ресурс] URL: \url{https://en.wikipedia.org/wiki/Golomb_coding}.
|
||||
|
||||
\item
|
||||
\Label{SET9}
|
||||
Гудов Д.О.
|
||||
\emph{set9.c --- экспериментальная реализация алгоритмов set:version}.
|
||||
Исходный код на GitHub.
|
||||
[Электронный ресурс] URL: \url{https://github.com/kr0sh512/alt-rpm-set-version/blob/main/reimplement/set9.c}.
|
||||
|
||||
\item
|
||||
\Label{ROARING}
|
||||
RoaringBitmap.org.
|
||||
\emph{Roaring Bitmaps --- A better compressed bitset}.
|
||||
[Электронный ресурс] URL: \url{https://roaringbitmap.org/}.
|
||||
|
||||
\item
|
||||
\Label{BASE64}
|
||||
Josefsson S.
|
||||
\emph{The Base16, Base32, and Base64 Data Encodings}. RFC~4648, 2006.
|
||||
[Электронный ресурс] URL: \url{https://www.rfc-editor.org/info/rfc4648}.
|
||||
|
||||
\item
|
||||
\Label{ZSTD}
|
||||
Collet Y., Kucherawy M.
|
||||
\emph{Zstandard Compression and the application/zstd Media Type}. RFC~8878, 2021.
|
||||
[Электронный ресурс] URL: \url{https://www.rfc-editor.org/info/rfc8878}.
|
||||
|
||||
\item
|
||||
\Label{XXHASH}
|
||||
Collet Y.
|
||||
\emph{xxHash --- extremely fast non-cryptographic hash algorithm}.
|
||||
Исходный код и документация.
|
||||
[Электронный ресурс] URL: \url{https://github.com/Cyan4973/xxHash}.
|
||||
|
||||
\item
|
||||
\Label{CITYHASH}
|
||||
Google.
|
||||
\emph{CityHash --- a family of hash functions for strings}.
|
||||
Исходный код и документация.
|
||||
[Электронный ресурс] URL: \url{https://github.com/google/cityhash}.
|
||||
|
||||
\item
|
||||
\Label{T1HA}
|
||||
Erthink.
|
||||
\emph{t1ha --- Fast Positive Hash}.
|
||||
Исходный код и документация.
|
||||
[Электронный ресурс] URL: \url{https://gitflic.ru/project/erthink/t1ha}.
|
||||
|
||||
\end{enumerate}
|
||||
@@ -0,0 +1,141 @@
|
||||
%% ---------------------------------------------------------------------------
|
||||
\documentclass[openany, twoside, a4paper, 10pt]{extbook}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[english, russian]{babel}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{indentfirst}
|
||||
\frenchspacing
|
||||
\raggedbottom
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{url}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{ifpdf}
|
||||
\ifpdf
|
||||
\usepackage[pdftex]{graphicx}
|
||||
\usepackage{cmap}
|
||||
\pdfcompresslevel=9
|
||||
\DeclareGraphicsExtensions{.pdf,.png,.jpg}
|
||||
\else
|
||||
\usepackage[dvips]{graphicx}
|
||||
\DeclareGraphicsExtensions{.eps}
|
||||
\fi
|
||||
\graphicspath{ {./images/} }
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{float}
|
||||
\restylefloat{table}
|
||||
\usepackage{multirow}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{amscd}
|
||||
\usepackage{bm}
|
||||
\usepackage{caption}
|
||||
\usepackage{makecell}
|
||||
\usepackage{nameref}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{titlesec}
|
||||
\titleformat{\section}[block]{\rmfamily\Large\bfseries\filcenter}{\thesection.}{1em}{}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\sloppy
|
||||
%% ---------------------------------------------------------------------------
|
||||
% a5paper: 148 x 210
|
||||
% print area: 110 x 180
|
||||
% top = bottom = (210 - 180) / 2 = 15
|
||||
% left = right = (148 - 110) / 2 = 19
|
||||
\usepackage[%
|
||||
left=5cm,%
|
||||
top=5.85cm,%
|
||||
right=5cm,%
|
||||
bottom=5.85cm,%
|
||||
headsep=0.2cm,%
|
||||
footskip=0.5cm,%
|
||||
]{geometry}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{titlesec}
|
||||
\newpagestyle{lvkpagestyle}{
|
||||
\sethead{}{}{}
|
||||
\setfoot{}{\usepage}{}
|
||||
}
|
||||
\pagestyle{lvkpagestyle}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand\Title[2]{%
|
||||
%\par
|
||||
\begin{centering}
|
||||
\medskip
|
||||
{
|
||||
\textbf{#1} \\
|
||||
}
|
||||
{
|
||||
\Large
|
||||
\textbf{\textsc{#2}} \\
|
||||
}
|
||||
\bigskip
|
||||
\end{centering}
|
||||
\@afterheading
|
||||
}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand{\References}{
|
||||
\medskip
|
||||
\section*{Литература}
|
||||
\nopagebreak
|
||||
\medskip
|
||||
\@afterheading
|
||||
}
|
||||
\makeatother
|
||||
|
||||
\makeatletter
|
||||
\newcommand{\ReferencesEng}{
|
||||
\medskip
|
||||
\section*{References}
|
||||
\nopagebreak
|
||||
\medskip
|
||||
\@afterheading
|
||||
}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand{\unchapter}[1]{%
|
||||
\begingroup
|
||||
\let\@makechapterhead\@gobble % make \@makechapterhead do nothing
|
||||
\chapter{#1}
|
||||
\endgroup
|
||||
}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\makeatletter
|
||||
\newcommand{\startgroup}[1]{\addtocontents{toc}{\contentsline{chapter}{#1}{}}}
|
||||
\makeatother
|
||||
%% ---------------------------------------------------------------------------
|
||||
\renewcommand*\thesection{\arabic{section}}
|
||||
\newcommand{\AddRefPrefix}[1]{
|
||||
\renewcommand{\Pageref}[1]{\pageref{#1##1}}
|
||||
\renewcommand{\Ref}[1]{\ref{#1##1}}
|
||||
\renewcommand{\Label}[1]{\label{#1##1}}
|
||||
}
|
||||
%%\newcommand{\Ref}[1]{\ref{#1}}
|
||||
\newcommand{\Pageref}[1]{\pageref{#1}}
|
||||
\newcommand{\Label}[1]{\label{#1}}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\renewcommand{\thetable}{\arabic{table}}
|
||||
\renewcommand{\thefigure}{\arabic{figure}}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\usepackage{pdfpages}
|
||||
%% ---------------------------------------------------------------------------
|
||||
\begin{document}
|
||||
%% \setcounter{page}{3}
|
||||
\setcounter{tocdepth}{0}
|
||||
%% \tableofcontents
|
||||
%% ---------------------------------------------------------------------------
|
||||
|
||||
% \startgroup{Секция 1. Разные вещи}
|
||||
% \unchapter{\textnormal{\textit{Иванов И.И.}} Первая статья в сборнике}
|
||||
% \AddRefPrefix{th1}
|
||||
\input{article_hbd}
|
||||
|
||||
%% ---------------------------------------------------------------------------
|
||||
\end{document}
|
||||
%% ---------------------------------------------------------------------------
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
@@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate unique words similar to a given word by applying random mutations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_ALPHABET = string.ascii_letters + string.digits + "_"
|
||||
|
||||
OPERATION_ALIASES = {
|
||||
"1": "replace",
|
||||
"replace": "replace",
|
||||
"2": "delete",
|
||||
"delete": "delete",
|
||||
"3": "add",
|
||||
"add": "add",
|
||||
"4": "swap",
|
||||
"swap": "swap",
|
||||
"5": "case",
|
||||
"case": "case",
|
||||
"6": "first",
|
||||
"first": "first",
|
||||
"7": "last",
|
||||
"last": "last",
|
||||
}
|
||||
|
||||
OPERATION_HELP = """операция изменения:
|
||||
1, replace заменить случайный символ
|
||||
2, delete удалить случайный символ
|
||||
3, add добавить символ в случайную позицию
|
||||
4, swap переставить два соседних символа
|
||||
5, case сменить регистр случайного символа
|
||||
6, first изменить первый символ
|
||||
7, last изменить последний символ"""
|
||||
|
||||
|
||||
class MutationError(ValueError):
|
||||
"""Raised when the selected mutation cannot be applied."""
|
||||
|
||||
|
||||
def different_character(current: str, alphabet: str, rng: random.Random) -> str:
|
||||
choices = [character for character in alphabet if character != current]
|
||||
if not choices:
|
||||
raise MutationError("алфавит не содержит символа, отличного от заменяемого")
|
||||
return rng.choice(choices)
|
||||
|
||||
|
||||
def replace_character(word: str, alphabet: str, rng: random.Random) -> str:
|
||||
if not word:
|
||||
raise MutationError("нельзя заменить символ в пустом слове")
|
||||
index = rng.randrange(len(word))
|
||||
replacement = different_character(word[index], alphabet, rng)
|
||||
return word[:index] + replacement + word[index + 1 :]
|
||||
|
||||
|
||||
def delete_character(word: str, _alphabet: str, rng: random.Random) -> str:
|
||||
if not word:
|
||||
raise MutationError("нельзя удалить символ из пустого слова")
|
||||
index = rng.randrange(len(word))
|
||||
return word[:index] + word[index + 1 :]
|
||||
|
||||
|
||||
def add_character(word: str, alphabet: str, rng: random.Random) -> str:
|
||||
index = rng.randrange(len(word) + 1)
|
||||
return word[:index] + rng.choice(alphabet) + word[index:]
|
||||
|
||||
|
||||
def swap_adjacent(word: str, _alphabet: str, rng: random.Random) -> str:
|
||||
indexes = [
|
||||
index for index in range(len(word) - 1) if word[index] != word[index + 1]
|
||||
]
|
||||
if not indexes:
|
||||
raise MutationError(
|
||||
"для перестановки нужны хотя бы два соседних различных символа"
|
||||
)
|
||||
index = rng.choice(indexes)
|
||||
return word[:index] + word[index + 1] + word[index] + word[index + 2 :]
|
||||
|
||||
|
||||
def change_case(word: str, _alphabet: str, rng: random.Random) -> str:
|
||||
indexes = []
|
||||
replacements: dict[int, str] = {}
|
||||
for index, character in enumerate(word):
|
||||
swapped = character.swapcase()
|
||||
if swapped != character and len(swapped) == 1:
|
||||
indexes.append(index)
|
||||
replacements[index] = swapped
|
||||
if not indexes:
|
||||
raise MutationError("в слове нет символов, у которых можно сменить регистр")
|
||||
index = rng.choice(indexes)
|
||||
return word[:index] + replacements[index] + word[index + 1 :]
|
||||
|
||||
|
||||
def change_first(word: str, alphabet: str, rng: random.Random) -> str:
|
||||
if not word:
|
||||
raise MutationError("нельзя изменить первый символ пустого слова")
|
||||
replacement = different_character(word[0], alphabet, rng)
|
||||
return replacement + word[1:]
|
||||
|
||||
|
||||
def change_last(word: str, alphabet: str, rng: random.Random) -> str:
|
||||
if not word:
|
||||
raise MutationError("нельзя изменить последний символ пустого слова")
|
||||
replacement = different_character(word[-1], alphabet, rng)
|
||||
return word[:-1] + replacement
|
||||
|
||||
|
||||
Mutation = Callable[[str, str, random.Random], str]
|
||||
MUTATIONS: dict[str, Mutation] = {
|
||||
"replace": replace_character,
|
||||
"delete": delete_character,
|
||||
"add": add_character,
|
||||
"swap": swap_adjacent,
|
||||
"case": change_case,
|
||||
"first": change_first,
|
||||
"last": change_last,
|
||||
}
|
||||
|
||||
|
||||
def parse_operation(value: str) -> str:
|
||||
try:
|
||||
return OPERATION_ALIASES[value.lower()]
|
||||
except KeyError as error:
|
||||
valid = ", ".join(OPERATION_ALIASES)
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"неизвестная операция {value!r}; допустимы: {valid}"
|
||||
) from error
|
||||
|
||||
|
||||
def generate_words(
|
||||
source: str,
|
||||
count: int,
|
||||
operation: str,
|
||||
operation_count: int,
|
||||
alphabet: str = DEFAULT_ALPHABET,
|
||||
seed: int | None = None,
|
||||
max_attempts: int | None = None,
|
||||
) -> list[str]:
|
||||
"""Generate up to ``count`` unique mutations made by exactly N operations."""
|
||||
if count < 1:
|
||||
raise ValueError("количество выходных слов должно быть положительным")
|
||||
if operation_count < 1:
|
||||
raise ValueError("количество операций должно быть положительным")
|
||||
if not alphabet:
|
||||
raise ValueError("алфавит не должен быть пустым")
|
||||
|
||||
mutation = MUTATIONS[operation]
|
||||
rng = random.Random(seed)
|
||||
attempt_limit = max_attempts or max(10_000, count * 1_000)
|
||||
words: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for _attempt in range(attempt_limit):
|
||||
candidate = source
|
||||
try:
|
||||
for _ in range(operation_count):
|
||||
candidate = mutation(candidate, alphabet, rng)
|
||||
except MutationError:
|
||||
continue
|
||||
|
||||
if candidate != source and candidate not in seen:
|
||||
seen.add(candidate)
|
||||
words.append(candidate)
|
||||
if len(words) == count:
|
||||
return words
|
||||
|
||||
return words
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Создаёт список уникальных слов, похожих на исходное. "
|
||||
"Каждое слово получается независимо от исходного ровно заданным "
|
||||
"числом случайных операций."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=(
|
||||
f"{OPERATION_HELP}\n\n"
|
||||
"Пример:\n"
|
||||
" python3 nexus.py example -o swap -n 20 -k 2 --seed 42"
|
||||
),
|
||||
)
|
||||
parser.add_argument("word", help="исходное слово")
|
||||
parser.add_argument(
|
||||
"-o", "--operation", required=True, type=parse_operation, help=OPERATION_HELP
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
default=10,
|
||||
help="верхняя граница числа уникальных выходных слов (по умолчанию: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--operations",
|
||||
type=int,
|
||||
default=1,
|
||||
help="число операций над каждым словом (по умолчанию: 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alphabet",
|
||||
default=DEFAULT_ALPHABET,
|
||||
help=("символы для добавления и замены " f"(по умолчанию: {DEFAULT_ALPHABET})"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed", type=int, help="seed генератора для воспроизводимого результата"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-attempts",
|
||||
type=int,
|
||||
help="предельное число попыток собрать уникальные слова",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
help="записать слова в файл вместо стандартного вывода",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
if args.max_attempts is not None and args.max_attempts < 1:
|
||||
parser.error("--max-attempts должен быть положительным")
|
||||
|
||||
try:
|
||||
words = generate_words(
|
||||
source=args.word,
|
||||
count=args.count,
|
||||
operation=args.operation,
|
||||
operation_count=args.operations,
|
||||
alphabet=args.alphabet,
|
||||
seed=args.seed,
|
||||
max_attempts=args.max_attempts,
|
||||
)
|
||||
except (MutationError, ValueError) as error:
|
||||
parser.error(str(error))
|
||||
|
||||
if len(words) < args.count:
|
||||
print(
|
||||
f"warning: operation={args.operation}: generated {len(words)} "
|
||||
f"of at most {args.count} unique words",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
output = "".join(f"{word}\n" for word in words)
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(output, encoding="utf-8")
|
||||
else:
|
||||
sys.stdout.write(output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,109 +0,0 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define JOAAT_SEED UINT32_C(0x9e3779b9)
|
||||
|
||||
static uint32_t jenkins_oaat(const unsigned char *data, size_t length)
|
||||
{
|
||||
uint32_t hash = JOAAT_SEED;
|
||||
|
||||
for (size_t index = 0; index < length; ++index) {
|
||||
hash += data[index];
|
||||
hash += hash << 10;
|
||||
hash ^= hash >> 6;
|
||||
}
|
||||
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 11;
|
||||
hash += hash << 15;
|
||||
return hash;
|
||||
}
|
||||
|
||||
static int is_ascii_trailing_space(unsigned char character)
|
||||
{
|
||||
return character == ' ' || character == '\t' || character == '\n' ||
|
||||
character == '\r' || character == '\v' || character == '\f';
|
||||
}
|
||||
|
||||
static int read_stdin(unsigned char **data, size_t *length)
|
||||
{
|
||||
size_t capacity = 256;
|
||||
unsigned char *buffer = malloc(capacity);
|
||||
|
||||
if (buffer == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*length = 0;
|
||||
for (;;) {
|
||||
size_t available = capacity - *length;
|
||||
size_t bytes_read = fread(buffer + *length, 1, available, stdin);
|
||||
*length += bytes_read;
|
||||
|
||||
if (bytes_read < available) {
|
||||
if (ferror(stdin)) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (capacity > SIZE_MAX / 2) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
capacity *= 2;
|
||||
|
||||
unsigned char *larger_buffer = realloc(buffer, capacity);
|
||||
if (larger_buffer == NULL) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
buffer = larger_buffer;
|
||||
}
|
||||
|
||||
*data = buffer;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const unsigned char *word;
|
||||
unsigned char *stdin_buffer = NULL;
|
||||
size_t length;
|
||||
|
||||
if (argc > 2) {
|
||||
fprintf(stderr, "usage: %s [ASCII_WORD]\n", argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (argc == 2) {
|
||||
word = (const unsigned char *)argv[1];
|
||||
length = strlen(argv[1]);
|
||||
} else {
|
||||
if (read_stdin(&stdin_buffer, &length) != 0) {
|
||||
fprintf(stderr, "failed to read input\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
word = stdin_buffer;
|
||||
}
|
||||
|
||||
while (length > 0 && is_ascii_trailing_space(word[length - 1])) {
|
||||
--length;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < length; ++index) {
|
||||
if (word[index] > 0x7f) {
|
||||
fprintf(stderr, "input must contain ASCII characters only\n");
|
||||
free(stdin_buffer);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
printf("%08" PRIx32 "\n", jenkins_oaat(word, length));
|
||||
free(stdin_buffer);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
unsigned int hash(const char* str) {
|
||||
unsigned int hash = 0x9e3779b9;
|
||||
const unsigned char* p = (const unsigned char*)str;
|
||||
while (*p) {
|
||||
hash += *p++;
|
||||
hash += (hash << 10);
|
||||
hash ^= (hash >> 6);
|
||||
}
|
||||
hash += (hash << 3);
|
||||
hash ^= (hash >> 11);
|
||||
hash += (hash << 15);
|
||||
return hash;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Standalone t1ha2_atonce command-line wrapper for avalanche testing.
|
||||
*
|
||||
* Upstream: https://gitflic.ru/project/erthink/t1ha
|
||||
* Commit: 00eb779b6c042ccd831ec2f1ae757409c73f39f6
|
||||
* Algorithm: t1ha2_atonce(data, length, seed=0), stable portable 64-bit mode.
|
||||
*
|
||||
* The vendored upstream implementation is licensed under the zlib License;
|
||||
* see upstream/LICENSE. This wrapper is an altered integration file and is not
|
||||
* represented as an original upstream source file.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define T1HA0_DISABLED
|
||||
#define T1HA1_DISABLED
|
||||
#define T1HA_SYS_UNALIGNED_ACCESS 0
|
||||
#define T1HA_USE_FAST_ONESHOT_READ 0
|
||||
#include "upstream/src/t1ha2.c"
|
||||
|
||||
#define T1HA2_SEED UINT64_C(0)
|
||||
|
||||
static int is_ascii_trailing_space(unsigned char character)
|
||||
{
|
||||
return character == ' ' || character == '\t' || character == '\n' ||
|
||||
character == '\r' || character == '\v' || character == '\f';
|
||||
}
|
||||
|
||||
static int read_stdin(unsigned char **data, size_t *length)
|
||||
{
|
||||
size_t capacity = 256;
|
||||
unsigned char *buffer = malloc(capacity);
|
||||
|
||||
if (buffer == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*length = 0;
|
||||
for (;;) {
|
||||
size_t available = capacity - *length;
|
||||
size_t bytes_read = fread(buffer + *length, 1, available, stdin);
|
||||
*length += bytes_read;
|
||||
|
||||
if (bytes_read < available) {
|
||||
if (ferror(stdin)) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (capacity > SIZE_MAX / 2U) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
capacity *= 2U;
|
||||
|
||||
{
|
||||
unsigned char *larger_buffer = realloc(buffer, capacity);
|
||||
if (larger_buffer == NULL) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
buffer = larger_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
*data = buffer;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const unsigned char *word;
|
||||
unsigned char *stdin_buffer = NULL;
|
||||
size_t length;
|
||||
|
||||
if (argc > 2) {
|
||||
fprintf(stderr, "usage: %s [ASCII_WORD]\n", argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (argc == 2) {
|
||||
word = (const unsigned char *)argv[1];
|
||||
length = strlen(argv[1]);
|
||||
} else {
|
||||
if (read_stdin(&stdin_buffer, &length) != 0) {
|
||||
fprintf(stderr, "failed to read input\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
word = stdin_buffer;
|
||||
}
|
||||
|
||||
while (length > 0U && is_ascii_trailing_space(word[length - 1U])) {
|
||||
--length;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < length; ++index) {
|
||||
if (word[index] > 0x7fU) {
|
||||
fprintf(stderr, "input must contain ASCII characters only\n");
|
||||
free(stdin_buffer);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
printf("%016" PRIx64 "\n", t1ha2_atonce(word, length, T1HA2_SEED));
|
||||
free(stdin_buffer);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
zlib License, see https://en.wikipedia.org/wiki/Zlib_License
|
||||
|
||||
Copyright (c) 2016-2020 Positive Technologies, https://www.ptsecurity.com,
|
||||
Fast Positive Hash.
|
||||
|
||||
Portions Copyright (c) 2010-2013 Leonid Yuriev <leo@yuriev.ru>,
|
||||
The 1Hippeus project (t1h).
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
@@ -1,383 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2020 Positive Technologies, https://www.ptsecurity.com,
|
||||
* Fast Positive Hash.
|
||||
*
|
||||
* Portions Copyright (c) 2010-2020 Leonid Yuriev <leo@yuriev.ru>,
|
||||
* The 1Hippeus project (t1h).
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will the authors be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must not
|
||||
* claim that you wrote the original software. If you use this software
|
||||
* in a product, an acknowledgement in the product documentation would be
|
||||
* appreciated but is not required.
|
||||
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software.
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
/*
|
||||
* t1ha = { Fast Positive Hash, aka "Позитивный Хэш" }
|
||||
* by [Positive Technologies](https://www.ptsecurity.ru)
|
||||
*
|
||||
* Briefly, it is a 64-bit Hash Function:
|
||||
* 1. Created for 64-bit little-endian platforms, in predominantly for x86_64,
|
||||
* but portable and without penalties it can run on any 64-bit CPU.
|
||||
* 2. In most cases up to 15% faster than City64, xxHash, mum-hash, metro-hash
|
||||
* and all others portable hash-functions (which do not use specific
|
||||
* hardware tricks).
|
||||
* 3. Not suitable for cryptography.
|
||||
*
|
||||
* The Future will (be) Positive. Всё будет хорошо.
|
||||
*
|
||||
* ACKNOWLEDGEMENT:
|
||||
* The t1ha was originally developed by Leonid Yuriev (Леонид Юрьев)
|
||||
* for The 1Hippeus project - zerocopy messaging in the spirit of Sparta!
|
||||
*/
|
||||
|
||||
#ifndef T1HA2_DISABLED
|
||||
#include "t1ha_bits.h"
|
||||
#include "t1ha_selfcheck.h"
|
||||
|
||||
static __always_inline void init_ab(t1ha_state256_t *s, uint64_t x,
|
||||
uint64_t y) {
|
||||
s->n.a = x;
|
||||
s->n.b = y;
|
||||
}
|
||||
|
||||
static __always_inline void init_cd(t1ha_state256_t *s, uint64_t x,
|
||||
uint64_t y) {
|
||||
s->n.c = rot64(y, 23) + ~x;
|
||||
s->n.d = ~y + rot64(x, 19);
|
||||
}
|
||||
|
||||
/* TODO: C++ template in the next version */
|
||||
#define T1HA2_UPDATE(ENDIANNES, ALIGNESS, state, v) \
|
||||
do { \
|
||||
t1ha_state256_t *const s = state; \
|
||||
const uint64_t w0 = fetch64_##ENDIANNES##_##ALIGNESS(v + 0); \
|
||||
const uint64_t w1 = fetch64_##ENDIANNES##_##ALIGNESS(v + 1); \
|
||||
const uint64_t w2 = fetch64_##ENDIANNES##_##ALIGNESS(v + 2); \
|
||||
const uint64_t w3 = fetch64_##ENDIANNES##_##ALIGNESS(v + 3); \
|
||||
\
|
||||
const uint64_t d02 = w0 + rot64(w2 + s->n.d, 56); \
|
||||
const uint64_t c13 = w1 + rot64(w3 + s->n.c, 19); \
|
||||
s->n.d ^= s->n.b + rot64(w1, 38); \
|
||||
s->n.c ^= s->n.a + rot64(w0, 57); \
|
||||
s->n.b ^= prime_6 * (c13 + w2); \
|
||||
s->n.a ^= prime_5 * (d02 + w3); \
|
||||
} while (0)
|
||||
|
||||
static __always_inline void squash(t1ha_state256_t *s) {
|
||||
s->n.a ^= prime_6 * (s->n.c + rot64(s->n.d, 23));
|
||||
s->n.b ^= prime_5 * (rot64(s->n.c, 19) + s->n.d);
|
||||
}
|
||||
|
||||
/* TODO: C++ template in the next version */
|
||||
#define T1HA2_LOOP(ENDIANNES, ALIGNESS, state, data, len) \
|
||||
do { \
|
||||
const void *detent = (const uint8_t *)data + len - 31; \
|
||||
do { \
|
||||
const uint64_t *v = (const uint64_t *)data; \
|
||||
data = (const uint64_t *)data + 4; \
|
||||
prefetch(data); \
|
||||
T1HA2_UPDATE(le, ALIGNESS, state, v); \
|
||||
} while (likely(data < detent)); \
|
||||
} while (0)
|
||||
|
||||
/* TODO: C++ template in the next version */
|
||||
#define T1HA2_TAIL_AB(ENDIANNES, ALIGNESS, state, data, len) \
|
||||
do { \
|
||||
t1ha_state256_t *const s = state; \
|
||||
const uint64_t *v = (const uint64_t *)data; \
|
||||
switch (len) { \
|
||||
default: \
|
||||
mixup64(&s->n.a, &s->n.b, fetch64_##ENDIANNES##_##ALIGNESS(v++), \
|
||||
prime_4); \
|
||||
/* fall through */ \
|
||||
case 24: \
|
||||
case 23: \
|
||||
case 22: \
|
||||
case 21: \
|
||||
case 20: \
|
||||
case 19: \
|
||||
case 18: \
|
||||
case 17: \
|
||||
mixup64(&s->n.b, &s->n.a, fetch64_##ENDIANNES##_##ALIGNESS(v++), \
|
||||
prime_3); \
|
||||
/* fall through */ \
|
||||
case 16: \
|
||||
case 15: \
|
||||
case 14: \
|
||||
case 13: \
|
||||
case 12: \
|
||||
case 11: \
|
||||
case 10: \
|
||||
case 9: \
|
||||
mixup64(&s->n.a, &s->n.b, fetch64_##ENDIANNES##_##ALIGNESS(v++), \
|
||||
prime_2); \
|
||||
/* fall through */ \
|
||||
case 8: \
|
||||
case 7: \
|
||||
case 6: \
|
||||
case 5: \
|
||||
case 4: \
|
||||
case 3: \
|
||||
case 2: \
|
||||
case 1: \
|
||||
mixup64(&s->n.b, &s->n.a, tail64_##ENDIANNES##_##ALIGNESS(v, len), \
|
||||
prime_1); \
|
||||
/* fall through */ \
|
||||
case 0: \
|
||||
return final64(s->n.a, s->n.b); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* TODO: C++ template in the next version */
|
||||
#define T1HA2_TAIL_ABCD(ENDIANNES, ALIGNESS, state, data, len) \
|
||||
do { \
|
||||
t1ha_state256_t *const s = state; \
|
||||
const uint64_t *v = (const uint64_t *)data; \
|
||||
switch (len) { \
|
||||
default: \
|
||||
mixup64(&s->n.a, &s->n.d, fetch64_##ENDIANNES##_##ALIGNESS(v++), \
|
||||
prime_4); \
|
||||
/* fall through */ \
|
||||
case 24: \
|
||||
case 23: \
|
||||
case 22: \
|
||||
case 21: \
|
||||
case 20: \
|
||||
case 19: \
|
||||
case 18: \
|
||||
case 17: \
|
||||
mixup64(&s->n.b, &s->n.a, fetch64_##ENDIANNES##_##ALIGNESS(v++), \
|
||||
prime_3); \
|
||||
/* fall through */ \
|
||||
case 16: \
|
||||
case 15: \
|
||||
case 14: \
|
||||
case 13: \
|
||||
case 12: \
|
||||
case 11: \
|
||||
case 10: \
|
||||
case 9: \
|
||||
mixup64(&s->n.c, &s->n.b, fetch64_##ENDIANNES##_##ALIGNESS(v++), \
|
||||
prime_2); \
|
||||
/* fall through */ \
|
||||
case 8: \
|
||||
case 7: \
|
||||
case 6: \
|
||||
case 5: \
|
||||
case 4: \
|
||||
case 3: \
|
||||
case 2: \
|
||||
case 1: \
|
||||
mixup64(&s->n.d, &s->n.c, tail64_##ENDIANNES##_##ALIGNESS(v, len), \
|
||||
prime_1); \
|
||||
/* fall through */ \
|
||||
case 0: \
|
||||
return final128(s->n.a, s->n.b, s->n.c, s->n.d, extra_result); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static __always_inline uint64_t final128(uint64_t a, uint64_t b, uint64_t c,
|
||||
uint64_t d, uint64_t *h) {
|
||||
mixup64(&a, &b, rot64(c, 41) ^ d, prime_0);
|
||||
mixup64(&b, &c, rot64(d, 23) ^ a, prime_6);
|
||||
mixup64(&c, &d, rot64(a, 19) ^ b, prime_5);
|
||||
mixup64(&d, &a, rot64(b, 31) ^ c, prime_4);
|
||||
*h = c + d;
|
||||
return a ^ b;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
uint64_t t1ha2_atonce(const void *data, size_t length, uint64_t seed) {
|
||||
t1ha_state256_t state;
|
||||
init_ab(&state, seed, length);
|
||||
|
||||
#if T1HA_SYS_UNALIGNED_ACCESS == T1HA_UNALIGNED_ACCESS__EFFICIENT
|
||||
if (unlikely(length > 32)) {
|
||||
init_cd(&state, seed, length);
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, unaligned, &state, data, length);
|
||||
squash(&state);
|
||||
length &= 31;
|
||||
}
|
||||
T1HA2_TAIL_AB(le, unaligned, &state, data, length);
|
||||
#else
|
||||
const bool misaligned = (((uintptr_t)data) & (ALIGNMENT_64 - 1)) != 0;
|
||||
if (misaligned) {
|
||||
if (unlikely(length > 32)) {
|
||||
init_cd(&state, seed, length);
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, unaligned, &state, data, length);
|
||||
squash(&state);
|
||||
length &= 31;
|
||||
}
|
||||
T1HA2_TAIL_AB(le, unaligned, &state, data, length);
|
||||
} else {
|
||||
if (unlikely(length > 32)) {
|
||||
init_cd(&state, seed, length);
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, aligned, &state, data, length);
|
||||
squash(&state);
|
||||
length &= 31;
|
||||
}
|
||||
T1HA2_TAIL_AB(le, aligned, &state, data, length);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
uint64_t t1ha2_atonce128(uint64_t *__restrict extra_result,
|
||||
const void *__restrict data, size_t length,
|
||||
uint64_t seed) {
|
||||
t1ha_state256_t state;
|
||||
init_ab(&state, seed, length);
|
||||
init_cd(&state, seed, length);
|
||||
|
||||
#if T1HA_SYS_UNALIGNED_ACCESS == T1HA_UNALIGNED_ACCESS__EFFICIENT
|
||||
if (unlikely(length > 32)) {
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, unaligned, &state, data, length);
|
||||
length &= 31;
|
||||
}
|
||||
T1HA2_TAIL_ABCD(le, unaligned, &state, data, length);
|
||||
#else
|
||||
const bool misaligned = (((uintptr_t)data) & (ALIGNMENT_64 - 1)) != 0;
|
||||
if (misaligned) {
|
||||
if (unlikely(length > 32)) {
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, unaligned, &state, data, length);
|
||||
length &= 31;
|
||||
}
|
||||
T1HA2_TAIL_ABCD(le, unaligned, &state, data, length);
|
||||
} else {
|
||||
if (unlikely(length > 32)) {
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, aligned, &state, data, length);
|
||||
length &= 31;
|
||||
}
|
||||
T1HA2_TAIL_ABCD(le, aligned, &state, data, length);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void t1ha2_init(t1ha_context_t *ctx, uint64_t seed_x, uint64_t seed_y) {
|
||||
init_ab(&ctx->state, seed_x, seed_y);
|
||||
init_cd(&ctx->state, seed_x, seed_y);
|
||||
ctx->partial = 0;
|
||||
ctx->total = 0;
|
||||
}
|
||||
|
||||
void t1ha2_update(t1ha_context_t *__restrict ctx, const void *__restrict data,
|
||||
size_t length) {
|
||||
ctx->total += length;
|
||||
|
||||
if (ctx->partial) {
|
||||
const size_t left = 32 - ctx->partial;
|
||||
const size_t chunk = (length >= left) ? left : length;
|
||||
memcpy(ctx->buffer.bytes + ctx->partial, data, chunk);
|
||||
ctx->partial += chunk;
|
||||
if (ctx->partial < 32) {
|
||||
assert(left >= length);
|
||||
return;
|
||||
}
|
||||
ctx->partial = 0;
|
||||
data = (const uint8_t *)data + chunk;
|
||||
length -= chunk;
|
||||
T1HA2_UPDATE(le, aligned, &ctx->state, ctx->buffer.u64);
|
||||
}
|
||||
|
||||
if (length >= 32) {
|
||||
#if T1HA_SYS_UNALIGNED_ACCESS == T1HA_UNALIGNED_ACCESS__EFFICIENT
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, unaligned, &ctx->state, data, length);
|
||||
#else
|
||||
const bool misaligned = (((uintptr_t)data) & (ALIGNMENT_64 - 1)) != 0;
|
||||
if (misaligned) {
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, unaligned, &ctx->state, data, length);
|
||||
} else {
|
||||
#if defined(__LCC__) && __LCC__ > 123
|
||||
/* Форсирует комбинирование пар арифметических операций в двухэтажные операции
|
||||
* в ближайшем после объявления директивы цикле, даже если эвристики оптимизации
|
||||
* говорят, что это нецелесообразно */
|
||||
#pragma comb_oper
|
||||
#endif /* E2K LCC > 1.23 */
|
||||
T1HA2_LOOP(le, aligned, &ctx->state, data, length);
|
||||
}
|
||||
#endif
|
||||
length &= 31;
|
||||
}
|
||||
|
||||
if (length)
|
||||
memcpy(ctx->buffer.bytes, data, ctx->partial = length);
|
||||
}
|
||||
|
||||
uint64_t t1ha2_final(t1ha_context_t *__restrict ctx,
|
||||
uint64_t *__restrict extra_result) {
|
||||
uint64_t bits = (ctx->total << 3) ^ (UINT64_C(1) << 63);
|
||||
#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
|
||||
bits = bswap64(bits);
|
||||
#endif
|
||||
t1ha2_update(ctx, &bits, 8);
|
||||
|
||||
if (likely(!extra_result)) {
|
||||
squash(&ctx->state);
|
||||
T1HA2_TAIL_AB(le, aligned, &ctx->state, ctx->buffer.u64, ctx->partial);
|
||||
}
|
||||
|
||||
T1HA2_TAIL_ABCD(le, aligned, &ctx->state, ctx->buffer.u64, ctx->partial);
|
||||
}
|
||||
|
||||
#endif /* T1HA2_DISABLED */
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2020 Positive Technologies, https://www.ptsecurity.com,
|
||||
* Fast Positive Hash.
|
||||
*
|
||||
* Portions Copyright (c) 2010-2020 Leonid Yuriev <leo@yuriev.ru>,
|
||||
* The 1Hippeus project (t1h).
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will the authors be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must not
|
||||
* claim that you wrote the original software. If you use this software
|
||||
* in a product, an acknowledgement in the product documentation would be
|
||||
* appreciated but is not required.
|
||||
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software.
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
/*
|
||||
* t1ha = { Fast Positive Hash, aka "Позитивный Хэш" }
|
||||
* by [Positive Technologies](https://www.ptsecurity.ru)
|
||||
*
|
||||
* Briefly, it is a 64-bit Hash Function:
|
||||
* 1. Created for 64-bit little-endian platforms, in predominantly for x86_64,
|
||||
* but portable and without penalties it can run on any 64-bit CPU.
|
||||
* 2. In most cases up to 15% faster than City64, xxHash, mum-hash, metro-hash
|
||||
* and all others portable hash-functions (which do not use specific
|
||||
* hardware tricks).
|
||||
* 3. Not suitable for cryptography.
|
||||
*
|
||||
* The Future will (be) Positive. Всё будет хорошо.
|
||||
*
|
||||
* ACKNOWLEDGEMENT:
|
||||
* The t1ha was originally developed by Leonid Yuriev (Леонид Юрьев)
|
||||
* for The 1Hippeus project - zerocopy messaging in the spirit of Sparta!
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#if defined(_MSC_VER) && _MSC_VER > 1800
|
||||
#pragma warning(disable : 4464) /* relative include path contains '..' */
|
||||
#endif /* MSVC */
|
||||
#include "../t1ha.h"
|
||||
|
||||
/***************************************************************************/
|
||||
/* Self-checking */
|
||||
|
||||
extern const uint8_t t1ha_test_pattern[64];
|
||||
int t1ha_selfcheck(uint64_t (*hash)(const void *, size_t, uint64_t),
|
||||
const uint64_t *reference_values);
|
||||
|
||||
#ifndef T1HA2_DISABLED
|
||||
extern const uint64_t t1ha_refval_2atonce[81];
|
||||
extern const uint64_t t1ha_refval_2atonce128[81];
|
||||
extern const uint64_t t1ha_refval_2stream[81];
|
||||
extern const uint64_t t1ha_refval_2stream128[81];
|
||||
#endif /* T1HA2_DISABLED */
|
||||
|
||||
#ifndef T1HA1_DISABLED
|
||||
extern const uint64_t t1ha_refval_64le[81];
|
||||
extern const uint64_t t1ha_refval_64be[81];
|
||||
#endif /* T1HA1_DISABLED */
|
||||
|
||||
#ifndef T1HA0_DISABLED
|
||||
extern const uint64_t t1ha_refval_32le[81];
|
||||
extern const uint64_t t1ha_refval_32be[81];
|
||||
#if T1HA0_AESNI_AVAILABLE
|
||||
extern const uint64_t t1ha_refval_ia32aes_a[81];
|
||||
extern const uint64_t t1ha_refval_ia32aes_b[81];
|
||||
#endif /* T1HA0_AESNI_AVAILABLE */
|
||||
#endif /* T1HA0_DISABLED */
|
||||
@@ -1,719 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2020 Positive Technologies, https://www.ptsecurity.com,
|
||||
* Fast Positive Hash.
|
||||
*
|
||||
* Portions Copyright (c) 2010-2020 Leonid Yuriev <leo@yuriev.ru>,
|
||||
* The 1Hippeus project (t1h).
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will the authors be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must not
|
||||
* claim that you wrote the original software. If you use this software
|
||||
* in a product, an acknowledgement in the product documentation would be
|
||||
* appreciated but is not required.
|
||||
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software.
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
/*
|
||||
* t1ha = { Fast Positive Hash, aka "Позитивный Хэш" }
|
||||
* by [Positive Technologies](https://www.ptsecurity.ru)
|
||||
*
|
||||
* Briefly, it is a 64-bit Hash Function:
|
||||
* 1. Created for 64-bit little-endian platforms, in predominantly for x86_64,
|
||||
* but portable and without penalties it can run on any 64-bit CPU.
|
||||
* 2. In most cases up to 15% faster than City64, xxHash, mum-hash, metro-hash
|
||||
* and all others portable hash-functions (which do not use specific
|
||||
* hardware tricks).
|
||||
* 3. Not suitable for cryptography.
|
||||
*
|
||||
* The Future will (be) Positive. Всё будет хорошо.
|
||||
*
|
||||
* ACKNOWLEDGEMENT:
|
||||
* The t1ha was originally developed by Leonid Yuriev (Леонид Юрьев)
|
||||
* for The 1Hippeus project - zerocopy messaging in the spirit of Sparta!
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/*****************************************************************************
|
||||
*
|
||||
* PLEASE PAY ATTENTION TO THE FOLLOWING NOTES
|
||||
* about macros definitions which controls t1ha behaviour and/or performance.
|
||||
*
|
||||
*
|
||||
* 1) T1HA_SYS_UNALIGNED_ACCESS = Defines the system/platform/CPU/architecture
|
||||
* abilities for unaligned data access.
|
||||
*
|
||||
* By default, when the T1HA_SYS_UNALIGNED_ACCESS not defined,
|
||||
* it will defined on the basis hardcoded knowledge about of capabilities
|
||||
* of most common CPU architectures. But you could override this
|
||||
* default behavior when build t1ha library itself:
|
||||
*
|
||||
* // To disable unaligned access at all.
|
||||
* #define T1HA_SYS_UNALIGNED_ACCESS 0
|
||||
*
|
||||
* // To enable unaligned access, but indicate that it significantly slow.
|
||||
* #define T1HA_SYS_UNALIGNED_ACCESS 1
|
||||
*
|
||||
* // To enable unaligned access, and indicate that it effecient.
|
||||
* #define T1HA_SYS_UNALIGNED_ACCESS 2
|
||||
*
|
||||
*
|
||||
* 2) T1HA_USE_FAST_ONESHOT_READ = Controls the data reads at the end of buffer.
|
||||
*
|
||||
* When defined to non-zero, t1ha will use 'one shot' method for reading
|
||||
* up to 8 bytes at the end of data. In this case just the one 64-bit read
|
||||
* will be performed even when the available less than 8 bytes.
|
||||
*
|
||||
* This is little bit faster that switching by length of data tail.
|
||||
* Unfortunately this will triggering a false-positive alarms from Valgrind,
|
||||
* AddressSanitizer and other similar tool.
|
||||
*
|
||||
* By default, t1ha defines it to 1, but you could override this
|
||||
* default behavior when build t1ha library itself:
|
||||
*
|
||||
* // For little bit faster and small code.
|
||||
* #define T1HA_USE_FAST_ONESHOT_READ 1
|
||||
*
|
||||
* // For calmness if doubt.
|
||||
* #define T1HA_USE_FAST_ONESHOT_READ 0
|
||||
*
|
||||
*
|
||||
* 3) T1HA0_RUNTIME_SELECT = Controls choice fastest function in runtime.
|
||||
*
|
||||
* t1ha library offers the t1ha0() function as the fastest for current CPU.
|
||||
* But actual CPU's features/capabilities and may be significantly different,
|
||||
* especially on x86 platform. Therefore, internally, t1ha0() may require
|
||||
* dynamic dispatching for choice best implementation.
|
||||
*
|
||||
* By default, t1ha enables such runtime choice and (may be) corresponding
|
||||
* indirect calls if it reasonable, but you could override this default
|
||||
* behavior when build t1ha library itself:
|
||||
*
|
||||
* // To enable runtime choice of fastest implementation.
|
||||
* #define T1HA0_RUNTIME_SELECT 1
|
||||
*
|
||||
* // To disable runtime choice of fastest implementation.
|
||||
* #define T1HA0_RUNTIME_SELECT 0
|
||||
*
|
||||
* When T1HA0_RUNTIME_SELECT is nonzero the t1ha0_resolve() function could
|
||||
* be used to get actual t1ha0() implementation address at runtime. This is
|
||||
* useful for two cases:
|
||||
* - calling by local pointer-to-function usually is little
|
||||
* bit faster (less overhead) than via a PLT thru the DSO boundary.
|
||||
* - GNU Indirect functions (see below) don't supported by environment
|
||||
* and calling by t1ha0_funcptr is not available and/or expensive.
|
||||
*
|
||||
* 4) T1HA_USE_INDIRECT_FUNCTIONS = Controls usage of GNU Indirect functions.
|
||||
*
|
||||
* In continue of T1HA0_RUNTIME_SELECT the T1HA_USE_INDIRECT_FUNCTIONS
|
||||
* controls usage of ELF indirect functions feature. In general, when
|
||||
* available, this reduces overhead of indirect function's calls though
|
||||
* a DSO-bundary (https://sourceware.org/glibc/wiki/GNU_IFUNC).
|
||||
*
|
||||
* By default, t1ha engage GNU Indirect functions when it available
|
||||
* and useful, but you could override this default behavior when build
|
||||
* t1ha library itself:
|
||||
*
|
||||
* // To enable use of GNU ELF Indirect functions.
|
||||
* #define T1HA_USE_INDIRECT_FUNCTIONS 1
|
||||
*
|
||||
* // To disable use of GNU ELF Indirect functions. This may be useful
|
||||
* // if the actual toolchain or the system's loader don't support ones.
|
||||
* #define T1HA_USE_INDIRECT_FUNCTIONS 0
|
||||
*
|
||||
* 5) T1HA0_AESNI_AVAILABLE = Controls AES-NI detection and dispatching on x86.
|
||||
*
|
||||
* In continue of T1HA0_RUNTIME_SELECT the T1HA0_AESNI_AVAILABLE controls
|
||||
* detection and usage of AES-NI CPU's feature. On the other hand, this
|
||||
* requires compiling parts of t1ha library with certain properly options,
|
||||
* and could be difficult or inconvenient in some cases.
|
||||
*
|
||||
* By default, t1ha engade AES-NI for t1ha0() on the x86 platform, but
|
||||
* you could override this default behavior when build t1ha library itself:
|
||||
*
|
||||
* // To disable detection and usage of AES-NI instructions for t1ha0().
|
||||
* // This may be useful when you unable to build t1ha library properly
|
||||
* // or known that AES-NI will be unavailable at the deploy.
|
||||
* #define T1HA0_AESNI_AVAILABLE 0
|
||||
*
|
||||
* // To force detection and usage of AES-NI instructions for t1ha0(),
|
||||
* // but I don't known reasons to anybody would need this.
|
||||
* #define T1HA0_AESNI_AVAILABLE 1
|
||||
*
|
||||
* 6) T1HA0_DISABLED, T1HA1_DISABLED, T1HA2_DISABLED = Controls availability of
|
||||
* t1ha functions.
|
||||
*
|
||||
* In some cases could be useful to import/use only few of t1ha functions
|
||||
* or just the one. So, this definitions allows disable corresponding parts
|
||||
* of t1ha library.
|
||||
*
|
||||
* // To disable t1ha0(), t1ha0_32le(), t1ha0_32be() and all AES-NI.
|
||||
* #define T1HA0_DISABLED
|
||||
*
|
||||
* // To disable t1ha1_le() and t1ha1_be().
|
||||
* #define T1HA1_DISABLED
|
||||
*
|
||||
* // To disable t1ha2_atonce(), t1ha2_atonce128() and so on.
|
||||
* #define T1HA2_DISABLED
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
#define T1HA_VERSION_MAJOR 2
|
||||
#define T1HA_VERSION_MINOR 1
|
||||
#define T1HA_VERSION_RELEASE 1
|
||||
|
||||
#ifndef __has_attribute
|
||||
#define __has_attribute(x) (0)
|
||||
#endif
|
||||
|
||||
#ifndef __has_include
|
||||
#define __has_include(x) (0)
|
||||
#endif
|
||||
|
||||
#ifndef __GNUC_PREREQ
|
||||
#if defined(__GNUC__) && defined(__GNUC_MINOR__)
|
||||
#define __GNUC_PREREQ(maj, min) \
|
||||
((__GNUC__ << 16) + __GNUC_MINOR__ >= ((maj) << 16) + (min))
|
||||
#else
|
||||
#define __GNUC_PREREQ(maj, min) 0
|
||||
#endif
|
||||
#endif /* __GNUC_PREREQ */
|
||||
|
||||
#ifndef __CLANG_PREREQ
|
||||
#ifdef __clang__
|
||||
#define __CLANG_PREREQ(maj, min) \
|
||||
((__clang_major__ << 16) + __clang_minor__ >= ((maj) << 16) + (min))
|
||||
#else
|
||||
#define __CLANG_PREREQ(maj, min) (0)
|
||||
#endif
|
||||
#endif /* __CLANG_PREREQ */
|
||||
|
||||
#ifndef __LCC_PREREQ
|
||||
#ifdef __LCC__
|
||||
#define __LCC_PREREQ(maj, min) \
|
||||
((__LCC__ << 16) + __LCC_MINOR__ >= ((maj) << 16) + (min))
|
||||
#else
|
||||
#define __LCC_PREREQ(maj, min) (0)
|
||||
#endif
|
||||
#endif /* __LCC_PREREQ */
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
#ifdef _MSC_VER
|
||||
/* Avoid '16' bytes padding added after data member 't1ha_context::total'
|
||||
* and other warnings from std-headers if warning-level > 3. */
|
||||
#pragma warning(push, 3)
|
||||
#endif
|
||||
|
||||
#if defined(__cplusplus) && __cplusplus >= 201103L
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#else
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
#if defined(i386) || defined(__386) || defined(__i386) || defined(__i386__) || \
|
||||
defined(i486) || defined(__i486) || defined(__i486__) || \
|
||||
defined(i586) | defined(__i586) || defined(__i586__) || defined(i686) || \
|
||||
defined(__i686) || defined(__i686__) || defined(_M_IX86) || \
|
||||
defined(_X86_) || defined(__THW_INTEL__) || defined(__I86__) || \
|
||||
defined(__INTEL__) || defined(__x86_64) || defined(__x86_64__) || \
|
||||
defined(__amd64__) || defined(__amd64) || defined(_M_X64) || \
|
||||
defined(_M_AMD64) || defined(__IA32__) || defined(__INTEL__)
|
||||
#ifndef __ia32__
|
||||
/* LY: define neutral __ia32__ for x86 and x86-64 archs */
|
||||
#define __ia32__ 1
|
||||
#endif /* __ia32__ */
|
||||
#if !defined(__amd64__) && (defined(__x86_64) || defined(__x86_64__) || \
|
||||
defined(__amd64) || defined(_M_X64))
|
||||
/* LY: define trusty __amd64__ for all AMD64/x86-64 arch */
|
||||
#define __amd64__ 1
|
||||
#endif /* __amd64__ */
|
||||
#endif /* all x86 */
|
||||
|
||||
#if !defined(__BYTE_ORDER__) || !defined(__ORDER_LITTLE_ENDIAN__) || \
|
||||
!defined(__ORDER_BIG_ENDIAN__)
|
||||
|
||||
/* *INDENT-OFF* */
|
||||
/* clang-format off */
|
||||
|
||||
#if defined(__GLIBC__) || defined(__GNU_LIBRARY__) || defined(__ANDROID__) || \
|
||||
defined(HAVE_ENDIAN_H) || __has_include(<endian.h>)
|
||||
#include <endian.h>
|
||||
#elif defined(__APPLE__) || defined(__MACH__) || defined(__OpenBSD__) || \
|
||||
defined(HAVE_MACHINE_ENDIAN_H) || __has_include(<machine/endian.h>)
|
||||
#include <machine/endian.h>
|
||||
#elif defined(HAVE_SYS_ISA_DEFS_H) || __has_include(<sys/isa_defs.h>)
|
||||
#include <sys/isa_defs.h>
|
||||
#elif (defined(HAVE_SYS_TYPES_H) && defined(HAVE_SYS_ENDIAN_H)) || \
|
||||
(__has_include(<sys/types.h>) && __has_include(<sys/endian.h>))
|
||||
#include <sys/endian.h>
|
||||
#include <sys/types.h>
|
||||
#elif defined(__bsdi__) || defined(__DragonFly__) || defined(__FreeBSD__) || \
|
||||
defined(__NETBSD__) || defined(__NetBSD__) || \
|
||||
defined(HAVE_SYS_PARAM_H) || __has_include(<sys/param.h>)
|
||||
#include <sys/param.h>
|
||||
#endif /* OS */
|
||||
|
||||
/* *INDENT-ON* */
|
||||
/* clang-format on */
|
||||
|
||||
#if defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && defined(__BIG_ENDIAN)
|
||||
#define __ORDER_LITTLE_ENDIAN__ __LITTLE_ENDIAN
|
||||
#define __ORDER_BIG_ENDIAN__ __BIG_ENDIAN
|
||||
#define __BYTE_ORDER__ __BYTE_ORDER
|
||||
#elif defined(_BYTE_ORDER) && defined(_LITTLE_ENDIAN) && defined(_BIG_ENDIAN)
|
||||
#define __ORDER_LITTLE_ENDIAN__ _LITTLE_ENDIAN
|
||||
#define __ORDER_BIG_ENDIAN__ _BIG_ENDIAN
|
||||
#define __BYTE_ORDER__ _BYTE_ORDER
|
||||
#else
|
||||
#define __ORDER_LITTLE_ENDIAN__ 1234
|
||||
#define __ORDER_BIG_ENDIAN__ 4321
|
||||
|
||||
#if defined(__LITTLE_ENDIAN__) || \
|
||||
(defined(_LITTLE_ENDIAN) && !defined(_BIG_ENDIAN)) || \
|
||||
defined(__ARMEL__) || defined(__THUMBEL__) || defined(__AARCH64EL__) || \
|
||||
defined(__MIPSEL__) || defined(_MIPSEL) || defined(__MIPSEL) || \
|
||||
defined(_M_ARM) || defined(_M_ARM64) || defined(__e2k__) || \
|
||||
defined(__elbrus_4c__) || defined(__elbrus_8c__) || defined(__bfin__) || \
|
||||
defined(__BFIN__) || defined(__ia64__) || defined(_IA64) || \
|
||||
defined(__IA64__) || defined(__ia64) || defined(_M_IA64) || \
|
||||
defined(__itanium__) || defined(__ia32__) || defined(__CYGWIN__) || \
|
||||
defined(_WIN64) || defined(_WIN32) || defined(__TOS_WIN__) || \
|
||||
defined(__WINDOWS__)
|
||||
#define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__
|
||||
|
||||
#elif defined(__BIG_ENDIAN__) || \
|
||||
(defined(_BIG_ENDIAN) && !defined(_LITTLE_ENDIAN)) || \
|
||||
defined(__ARMEB__) || defined(__THUMBEB__) || defined(__AARCH64EB__) || \
|
||||
defined(__MIPSEB__) || defined(_MIPSEB) || defined(__MIPSEB) || \
|
||||
defined(__m68k__) || defined(M68000) || defined(__hppa__) || \
|
||||
defined(__hppa) || defined(__HPPA__) || defined(__sparc__) || \
|
||||
defined(__sparc) || defined(__370__) || defined(__THW_370__) || \
|
||||
defined(__s390__) || defined(__s390x__) || defined(__SYSC_ZARCH__)
|
||||
#define __BYTE_ORDER__ __ORDER_BIG_ENDIAN__
|
||||
|
||||
#else
|
||||
#error __BYTE_ORDER__ should be defined.
|
||||
#endif /* Arch */
|
||||
|
||||
#endif
|
||||
#endif /* __BYTE_ORDER__ || __ORDER_LITTLE_ENDIAN__ || __ORDER_BIG_ENDIAN__ */
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
#ifndef __dll_export
|
||||
#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__)
|
||||
#if defined(__GNUC__) || __has_attribute(dllexport)
|
||||
#define __dll_export __attribute__((dllexport))
|
||||
#else
|
||||
#define __dll_export __declspec(dllexport)
|
||||
#endif
|
||||
#elif defined(__GNUC__) || __has_attribute(__visibility__)
|
||||
#define __dll_export __attribute__((__visibility__("default")))
|
||||
#else
|
||||
#define __dll_export
|
||||
#endif
|
||||
#endif /* __dll_export */
|
||||
|
||||
#ifndef __dll_import
|
||||
#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__)
|
||||
#if defined(__GNUC__) || __has_attribute(dllimport)
|
||||
#define __dll_import __attribute__((dllimport))
|
||||
#else
|
||||
#define __dll_import __declspec(dllimport)
|
||||
#endif
|
||||
#elif defined(__GNUC__) || __has_attribute(__visibility__)
|
||||
#define __dll_import __attribute__((__visibility__("default")))
|
||||
#else
|
||||
#define __dll_import
|
||||
#endif
|
||||
#endif /* __dll_import */
|
||||
|
||||
#ifndef __force_inline
|
||||
#ifdef _MSC_VER
|
||||
#define __force_inline __forceinline
|
||||
#elif __GNUC_PREREQ(3, 2) || __has_attribute(__always_inline__)
|
||||
#define __force_inline __inline __attribute__((__always_inline__))
|
||||
#else
|
||||
#define __force_inline __inline
|
||||
#endif
|
||||
#endif /* __force_inline */
|
||||
|
||||
#ifndef T1HA_API
|
||||
#if defined(t1ha_EXPORTS)
|
||||
#define T1HA_API __dll_export
|
||||
#elif defined(t1ha_IMPORTS)
|
||||
#define T1HA_API __dll_import
|
||||
#else
|
||||
#define T1HA_API
|
||||
#endif
|
||||
#endif /* T1HA_API */
|
||||
|
||||
#if defined(_MSC_VER) && defined(__ia32__)
|
||||
#define T1HA_ALIGN_PREFIX __declspec(align(32)) /* required only for SIMD */
|
||||
#else
|
||||
#define T1HA_ALIGN_PREFIX
|
||||
#endif /* _MSC_VER */
|
||||
|
||||
#if defined(__GNUC__) && defined(__ia32__)
|
||||
#define T1HA_ALIGN_SUFFIX \
|
||||
__attribute__((__aligned__(32))) /* required only for SIMD */
|
||||
#else
|
||||
#define T1HA_ALIGN_SUFFIX
|
||||
#endif /* GCC x86 */
|
||||
|
||||
#ifndef T1HA_USE_INDIRECT_FUNCTIONS
|
||||
/* GNU ELF indirect functions usage control. For more info please see
|
||||
* https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
* and https://sourceware.org/glibc/wiki/GNU_IFUNC */
|
||||
#if defined(__ELF__) && defined(__amd64__) && \
|
||||
(__has_attribute(__ifunc__) || \
|
||||
(!defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 4 && \
|
||||
!defined(__SANITIZE_ADDRESS__) && !defined(__SSP_ALL__)))
|
||||
/* Enable gnu_indirect_function by default if :
|
||||
* - ELF AND x86_64
|
||||
* - attribute(__ifunc__) is available OR
|
||||
* GCC >= 4 WITHOUT -fsanitize=address NOR -fstack-protector-all */
|
||||
#define T1HA_USE_INDIRECT_FUNCTIONS 1
|
||||
#else
|
||||
#define T1HA_USE_INDIRECT_FUNCTIONS 0
|
||||
#endif
|
||||
#endif /* T1HA_USE_INDIRECT_FUNCTIONS */
|
||||
|
||||
#if __GNUC_PREREQ(4, 0)
|
||||
#pragma GCC visibility push(hidden)
|
||||
#endif /* __GNUC_PREREQ(4,0) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef union T1HA_ALIGN_PREFIX t1ha_state256 {
|
||||
uint8_t bytes[32];
|
||||
uint32_t u32[8];
|
||||
uint64_t u64[4];
|
||||
struct {
|
||||
uint64_t a, b, c, d;
|
||||
} n;
|
||||
} t1ha_state256_t T1HA_ALIGN_SUFFIX;
|
||||
|
||||
typedef struct t1ha_context {
|
||||
t1ha_state256_t state;
|
||||
t1ha_state256_t buffer;
|
||||
size_t partial;
|
||||
uint64_t total;
|
||||
} t1ha_context_t;
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Self-testing API.
|
||||
*
|
||||
* Unfortunately, some compilers (exactly only Microsoft Visual C/C++) has
|
||||
* a bugs which leads t1ha-functions to produce wrong results. This API allows
|
||||
* check the correctness of the actual code in runtime.
|
||||
*
|
||||
* All check-functions returns 0 on success, or -1 in case the corresponding
|
||||
* hash-function failed verification. PLEASE, always perform such checking at
|
||||
* initialization of your code, if you using MSVC or other troubleful compilers.
|
||||
*/
|
||||
|
||||
T1HA_API int t1ha_selfcheck__all_enabled(void);
|
||||
|
||||
#ifndef T1HA2_DISABLED
|
||||
T1HA_API int t1ha_selfcheck__t1ha2_atonce(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha2_atonce128(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha2_stream(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha2(void);
|
||||
#endif /* T1HA2_DISABLED */
|
||||
|
||||
#ifndef T1HA1_DISABLED
|
||||
T1HA_API int t1ha_selfcheck__t1ha1_le(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha1_be(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha1(void);
|
||||
#endif /* T1HA1_DISABLED */
|
||||
|
||||
#ifndef T1HA0_DISABLED
|
||||
T1HA_API int t1ha_selfcheck__t1ha0_32le(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha0_32be(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha0(void);
|
||||
|
||||
/* Define T1HA0_AESNI_AVAILABLE to 0 for disable AES-NI support. */
|
||||
#ifndef T1HA0_AESNI_AVAILABLE
|
||||
#if defined(__e2k__) || \
|
||||
(defined(__ia32__) && (!defined(_M_IX86) || _MSC_VER > 1800))
|
||||
#define T1HA0_AESNI_AVAILABLE 1
|
||||
#else
|
||||
#define T1HA0_AESNI_AVAILABLE 0
|
||||
#endif
|
||||
#endif /* ifndef T1HA0_AESNI_AVAILABLE */
|
||||
|
||||
#if T1HA0_AESNI_AVAILABLE
|
||||
T1HA_API int t1ha_selfcheck__t1ha0_ia32aes_noavx(void);
|
||||
T1HA_API int t1ha_selfcheck__t1ha0_ia32aes_avx(void);
|
||||
#ifndef __e2k__
|
||||
T1HA_API int t1ha_selfcheck__t1ha0_ia32aes_avx2(void);
|
||||
#endif
|
||||
#endif /* if T1HA0_AESNI_AVAILABLE */
|
||||
#endif /* T1HA0_DISABLED */
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* t1ha2 = 64 and 128-bit, SLIGHTLY MORE ATTENTION FOR QUALITY AND STRENGTH.
|
||||
*
|
||||
* - The recommended version of "Fast Positive Hash" with good quality
|
||||
* for checksum, hash tables and fingerprinting.
|
||||
* - Portable and extremely efficiency on modern 64-bit CPUs.
|
||||
* Designed for 64-bit little-endian platforms,
|
||||
* in other cases will runs slowly.
|
||||
* - Great quality of hashing and still faster than other non-t1ha hashes.
|
||||
* Provides streaming mode and 128-bit result.
|
||||
*
|
||||
* Note: Due performance reason 64- and 128-bit results are completely
|
||||
* different each other, i.e. 64-bit result is NOT any part of 128-bit.
|
||||
*/
|
||||
#ifndef T1HA2_DISABLED
|
||||
|
||||
/* The at-once variant with 64-bit result */
|
||||
T1HA_API uint64_t t1ha2_atonce(const void *data, size_t length, uint64_t seed);
|
||||
|
||||
/* The at-once variant with 128-bit result.
|
||||
* Argument `extra_result` is NOT optional and MUST be valid.
|
||||
* The high 64-bit part of 128-bit hash will be always unconditionally
|
||||
* stored to the address given by `extra_result` argument. */
|
||||
T1HA_API uint64_t t1ha2_atonce128(uint64_t *__restrict extra_result,
|
||||
const void *__restrict data, size_t length,
|
||||
uint64_t seed);
|
||||
|
||||
/* The init/update/final trinity for streaming.
|
||||
* Return 64 or 128-bit result depentently from `extra_result` argument. */
|
||||
T1HA_API void t1ha2_init(t1ha_context_t *ctx, uint64_t seed_x, uint64_t seed_y);
|
||||
T1HA_API void t1ha2_update(t1ha_context_t *__restrict ctx,
|
||||
const void *__restrict data, size_t length);
|
||||
|
||||
/* Argument `extra_result` is optional and MAY be NULL.
|
||||
* - If `extra_result` is NOT NULL then the 128-bit hash will be calculated,
|
||||
* and high 64-bit part of it will be stored to the address given
|
||||
* by `extra_result` argument.
|
||||
* - Otherwise the 64-bit hash will be calculated
|
||||
* and returned from function directly.
|
||||
*
|
||||
* Note: Due performance reason 64- and 128-bit results are completely
|
||||
* different each other, i.e. 64-bit result is NOT any part of 128-bit. */
|
||||
T1HA_API uint64_t t1ha2_final(t1ha_context_t *__restrict ctx,
|
||||
uint64_t *__restrict extra_result /* optional */);
|
||||
|
||||
#endif /* T1HA2_DISABLED */
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* t1ha1 = 64-bit, BASELINE FAST PORTABLE HASH:
|
||||
*
|
||||
* - Runs faster on 64-bit platforms in other cases may runs slowly.
|
||||
* - Portable and stable, returns same 64-bit result
|
||||
* on all architectures and CPUs.
|
||||
* - Unfortunately it fails the "strict avalanche criteria",
|
||||
* see test results at https://github.com/demerphq/smhasher.
|
||||
*
|
||||
* This flaw is insignificant for the t1ha1() purposes and imperceptible
|
||||
* from a practical point of view.
|
||||
* However, nowadays this issue has resolved in the next t1ha2(),
|
||||
* that was initially planned to providing a bit more quality.
|
||||
*/
|
||||
#ifndef T1HA1_DISABLED
|
||||
|
||||
/* The little-endian variant. */
|
||||
T1HA_API uint64_t t1ha1_le(const void *data, size_t length, uint64_t seed);
|
||||
|
||||
/* The big-endian variant. */
|
||||
T1HA_API uint64_t t1ha1_be(const void *data, size_t length, uint64_t seed);
|
||||
|
||||
#endif /* T1HA1_DISABLED */
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* t1ha0 = 64-bit, JUST ONLY FASTER:
|
||||
*
|
||||
* - Provides fast-as-possible hashing for current CPU, including
|
||||
* 32-bit systems and engaging the available hardware acceleration.
|
||||
* - It is a facade that selects most quick-and-dirty hash
|
||||
* for the current processor. For instance, on IA32 (x86) actual function
|
||||
* will be selected in runtime, depending on current CPU capabilities
|
||||
*
|
||||
* BE CAREFUL!!! THIS IS MEANS:
|
||||
*
|
||||
* 1. The quality of hash is a subject for tradeoffs with performance.
|
||||
* So, the quality and strength of t1ha0() may be lower than t1ha1(),
|
||||
* especially on 32-bit targets, but then much faster.
|
||||
* However, guaranteed that it passes all SMHasher tests.
|
||||
*
|
||||
* 2. No warranty that the hash result will be same for particular
|
||||
* key on another machine or another version of libt1ha.
|
||||
*
|
||||
* Briefly, such hash-results and their derivatives, should be
|
||||
* used only in runtime, but should not be persist or transferred
|
||||
* over a network.
|
||||
*
|
||||
*
|
||||
* When T1HA0_RUNTIME_SELECT is nonzero the t1ha0_resolve() function could
|
||||
* be used to get actual t1ha0() implementation address at runtime. This is
|
||||
* useful for two cases:
|
||||
* - calling by local pointer-to-function usually is little
|
||||
* bit faster (less overhead) than via a PLT thru the DSO boundary.
|
||||
* - GNU Indirect functions (see below) don't supported by environment
|
||||
* and calling by t1ha0_funcptr is not available and/or expensive.
|
||||
*/
|
||||
|
||||
#ifndef T1HA0_DISABLED
|
||||
|
||||
/* The little-endian variant for 32-bit CPU. */
|
||||
uint64_t t1ha0_32le(const void *data, size_t length, uint64_t seed);
|
||||
/* The big-endian variant for 32-bit CPU. */
|
||||
uint64_t t1ha0_32be(const void *data, size_t length, uint64_t seed);
|
||||
|
||||
/* Define T1HA0_AESNI_AVAILABLE to 0 for disable AES-NI support. */
|
||||
#ifndef T1HA0_AESNI_AVAILABLE
|
||||
#if defined(__e2k__) || \
|
||||
(defined(__ia32__) && (!defined(_M_IX86) || _MSC_VER > 1800))
|
||||
#define T1HA0_AESNI_AVAILABLE 1
|
||||
#else
|
||||
#define T1HA0_AESNI_AVAILABLE 0
|
||||
#endif
|
||||
#endif /* T1HA0_AESNI_AVAILABLE */
|
||||
|
||||
/* Define T1HA0_RUNTIME_SELECT to 0 for disable dispatching t1ha0 at runtime. */
|
||||
#ifndef T1HA0_RUNTIME_SELECT
|
||||
#if T1HA0_AESNI_AVAILABLE && !defined(__e2k__)
|
||||
#define T1HA0_RUNTIME_SELECT 1
|
||||
#else
|
||||
#define T1HA0_RUNTIME_SELECT 0
|
||||
#endif
|
||||
#endif /* T1HA0_RUNTIME_SELECT */
|
||||
|
||||
#if !T1HA0_RUNTIME_SELECT && !defined(T1HA0_USE_DEFINE)
|
||||
#if defined(__LCC__)
|
||||
#define T1HA0_USE_DEFINE 1
|
||||
#else
|
||||
#define T1HA0_USE_DEFINE 0
|
||||
#endif
|
||||
#endif /* T1HA0_USE_DEFINE */
|
||||
|
||||
#if T1HA0_AESNI_AVAILABLE
|
||||
uint64_t t1ha0_ia32aes_noavx(const void *data, size_t length, uint64_t seed);
|
||||
uint64_t t1ha0_ia32aes_avx(const void *data, size_t length, uint64_t seed);
|
||||
#ifndef __e2k__
|
||||
uint64_t t1ha0_ia32aes_avx2(const void *data, size_t length, uint64_t seed);
|
||||
#endif
|
||||
#endif /* T1HA0_AESNI_AVAILABLE */
|
||||
|
||||
#if T1HA0_RUNTIME_SELECT
|
||||
typedef uint64_t (*t1ha0_function_t)(const void *, size_t, uint64_t);
|
||||
T1HA_API t1ha0_function_t t1ha0_resolve(void);
|
||||
#if T1HA_USE_INDIRECT_FUNCTIONS
|
||||
T1HA_API uint64_t t1ha0(const void *data, size_t length, uint64_t seed);
|
||||
#else
|
||||
/* Otherwise function pointer will be used.
|
||||
* Unfortunately this may cause some overhead calling. */
|
||||
T1HA_API extern uint64_t (*t1ha0_funcptr)(const void *data, size_t length,
|
||||
uint64_t seed);
|
||||
static __force_inline uint64_t t1ha0(const void *data, size_t length,
|
||||
uint64_t seed) {
|
||||
return t1ha0_funcptr(data, length, seed);
|
||||
}
|
||||
#endif /* T1HA_USE_INDIRECT_FUNCTIONS */
|
||||
|
||||
#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
|
||||
#if T1HA0_USE_DEFINE
|
||||
|
||||
#if (UINTPTR_MAX > 0xffffFFFFul || ULONG_MAX > 0xffffFFFFul) && \
|
||||
(!defined(T1HA1_DISABLED) || !defined(T1HA2_DISABLED))
|
||||
#if defined(T1HA1_DISABLED)
|
||||
#define t1ha0 t1ha2_atonce
|
||||
#else
|
||||
#define t1ha0 t1ha1_be
|
||||
#endif /* T1HA1_DISABLED */
|
||||
#else /* 32/64 */
|
||||
#define t1ha0 t1ha0_32be
|
||||
#endif /* 32/64 */
|
||||
|
||||
#else /* T1HA0_USE_DEFINE */
|
||||
|
||||
static __force_inline uint64_t t1ha0(const void *data, size_t length,
|
||||
uint64_t seed) {
|
||||
#if (UINTPTR_MAX > 0xffffFFFFul || ULONG_MAX > 0xffffFFFFul) && \
|
||||
(!defined(T1HA1_DISABLED) || !defined(T1HA2_DISABLED))
|
||||
#if defined(T1HA1_DISABLED)
|
||||
return t1ha2_atonce(data, length, seed);
|
||||
#else
|
||||
return t1ha1_be(data, length, seed);
|
||||
#endif /* T1HA1_DISABLED */
|
||||
#else /* 32/64 */
|
||||
return t1ha0_32be(data, length, seed);
|
||||
#endif /* 32/64 */
|
||||
}
|
||||
|
||||
#endif /* !T1HA0_USE_DEFINE */
|
||||
|
||||
#else /* !T1HA0_RUNTIME_SELECT && __BYTE_ORDER__ != __ORDER_BIG_ENDIAN__ */
|
||||
|
||||
#if T1HA0_USE_DEFINE
|
||||
|
||||
#if (UINTPTR_MAX > 0xffffFFFFul || ULONG_MAX > 0xffffFFFFul) && \
|
||||
(!defined(T1HA1_DISABLED) || !defined(T1HA2_DISABLED))
|
||||
#if defined(T1HA1_DISABLED)
|
||||
#define t1ha0 t1ha2_atonce
|
||||
#else
|
||||
#define t1ha0 t1ha1_le
|
||||
#endif /* T1HA1_DISABLED */
|
||||
#else /* 32/64 */
|
||||
#define t1ha0 t1ha0_32le
|
||||
#endif /* 32/64 */
|
||||
|
||||
#else
|
||||
|
||||
static __force_inline uint64_t t1ha0(const void *data, size_t length,
|
||||
uint64_t seed) {
|
||||
#if (UINTPTR_MAX > 0xffffFFFFul || ULONG_MAX > 0xffffFFFFul) && \
|
||||
(!defined(T1HA1_DISABLED) || !defined(T1HA2_DISABLED))
|
||||
#if defined(T1HA1_DISABLED)
|
||||
return t1ha2_atonce(data, length, seed);
|
||||
#else
|
||||
return t1ha1_le(data, length, seed);
|
||||
#endif /* T1HA1_DISABLED */
|
||||
#else /* 32/64 */
|
||||
return t1ha0_32le(data, length, seed);
|
||||
#endif /* 32/64 */
|
||||
}
|
||||
|
||||
#endif /* !T1HA0_USE_DEFINE */
|
||||
|
||||
#endif /* !T1HA0_RUNTIME_SELECT */
|
||||
|
||||
#endif /* T1HA0_DISABLED */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#if __GNUC_PREREQ(4, 0)
|
||||
#pragma GCC visibility pop
|
||||
#endif /* __GNUC_PREREQ(4,0) */
|
||||
@@ -1,224 +0,0 @@
|
||||
/*
|
||||
* Standalone XXH64 command-line wrapper for avalanche testing.
|
||||
*
|
||||
* XXH64 algorithm derived from xxHash by Yann Collet:
|
||||
* https://github.com/Cyan4973/xxHash
|
||||
*
|
||||
* Copyright (C) 2012-2023 Yann Collet
|
||||
*
|
||||
* BSD 2-Clause License
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DAMAGES ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
* DAMAGE.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define XXH64_SEED UINT64_C(0)
|
||||
#define XXH_PRIME64_1 UINT64_C(11400714785074694791)
|
||||
#define XXH_PRIME64_2 UINT64_C(14029467366897019727)
|
||||
#define XXH_PRIME64_3 UINT64_C(1609587929392839161)
|
||||
#define XXH_PRIME64_4 UINT64_C(9650029242287828579)
|
||||
#define XXH_PRIME64_5 UINT64_C(2870177450012600261)
|
||||
|
||||
static uint64_t rotate_left64(uint64_t value, unsigned int count)
|
||||
{
|
||||
return (value << count) | (value >> (64U - count));
|
||||
}
|
||||
|
||||
static uint32_t read_little_endian32(const unsigned char *data)
|
||||
{
|
||||
return (uint32_t)data[0] | ((uint32_t)data[1] << 8U) |
|
||||
((uint32_t)data[2] << 16U) | ((uint32_t)data[3] << 24U);
|
||||
}
|
||||
|
||||
static uint64_t read_little_endian64(const unsigned char *data)
|
||||
{
|
||||
return (uint64_t)read_little_endian32(data) |
|
||||
((uint64_t)read_little_endian32(data + 4) << 32U);
|
||||
}
|
||||
|
||||
static uint64_t xxh64_round(uint64_t accumulator, uint64_t input)
|
||||
{
|
||||
accumulator += input * XXH_PRIME64_2;
|
||||
accumulator = rotate_left64(accumulator, 31U);
|
||||
accumulator *= XXH_PRIME64_1;
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
static uint64_t xxh64_merge_round(uint64_t accumulator, uint64_t value)
|
||||
{
|
||||
value = xxh64_round(UINT64_C(0), value);
|
||||
accumulator ^= value;
|
||||
accumulator = accumulator * XXH_PRIME64_1 + XXH_PRIME64_4;
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
static uint64_t xxh64(const unsigned char *data, size_t length, uint64_t seed)
|
||||
{
|
||||
const unsigned char *position = data;
|
||||
const unsigned char *const end = data + length;
|
||||
uint64_t hash;
|
||||
|
||||
if (length >= 32U) {
|
||||
const unsigned char *const block_end = end - 32U;
|
||||
uint64_t accumulator1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2;
|
||||
uint64_t accumulator2 = seed + XXH_PRIME64_2;
|
||||
uint64_t accumulator3 = seed;
|
||||
uint64_t accumulator4 = seed - XXH_PRIME64_1;
|
||||
|
||||
do {
|
||||
accumulator1 = xxh64_round(accumulator1, read_little_endian64(position));
|
||||
position += 8;
|
||||
accumulator2 = xxh64_round(accumulator2, read_little_endian64(position));
|
||||
position += 8;
|
||||
accumulator3 = xxh64_round(accumulator3, read_little_endian64(position));
|
||||
position += 8;
|
||||
accumulator4 = xxh64_round(accumulator4, read_little_endian64(position));
|
||||
position += 8;
|
||||
} while (position <= block_end);
|
||||
|
||||
hash = rotate_left64(accumulator1, 1U) +
|
||||
rotate_left64(accumulator2, 7U) +
|
||||
rotate_left64(accumulator3, 12U) +
|
||||
rotate_left64(accumulator4, 18U);
|
||||
hash = xxh64_merge_round(hash, accumulator1);
|
||||
hash = xxh64_merge_round(hash, accumulator2);
|
||||
hash = xxh64_merge_round(hash, accumulator3);
|
||||
hash = xxh64_merge_round(hash, accumulator4);
|
||||
} else {
|
||||
hash = seed + XXH_PRIME64_5;
|
||||
}
|
||||
|
||||
hash += (uint64_t)length;
|
||||
|
||||
while ((size_t)(end - position) >= 8U) {
|
||||
uint64_t value = xxh64_round(UINT64_C(0), read_little_endian64(position));
|
||||
hash ^= value;
|
||||
hash = rotate_left64(hash, 27U) * XXH_PRIME64_1 + XXH_PRIME64_4;
|
||||
position += 8;
|
||||
}
|
||||
|
||||
if ((size_t)(end - position) >= 4U) {
|
||||
hash ^= (uint64_t)read_little_endian32(position) * XXH_PRIME64_1;
|
||||
hash = rotate_left64(hash, 23U) * XXH_PRIME64_2 + XXH_PRIME64_3;
|
||||
position += 4;
|
||||
}
|
||||
|
||||
while (position < end) {
|
||||
hash ^= (uint64_t)(*position) * XXH_PRIME64_5;
|
||||
hash = rotate_left64(hash, 11U) * XXH_PRIME64_1;
|
||||
++position;
|
||||
}
|
||||
|
||||
hash ^= hash >> 33U;
|
||||
hash *= XXH_PRIME64_2;
|
||||
hash ^= hash >> 29U;
|
||||
hash *= XXH_PRIME64_3;
|
||||
hash ^= hash >> 32U;
|
||||
return hash;
|
||||
}
|
||||
|
||||
static int is_ascii_trailing_space(unsigned char character)
|
||||
{
|
||||
return character == ' ' || character == '\t' || character == '\n' ||
|
||||
character == '\r' || character == '\v' || character == '\f';
|
||||
}
|
||||
|
||||
static int read_stdin(unsigned char **data, size_t *length)
|
||||
{
|
||||
size_t capacity = 256;
|
||||
unsigned char *buffer = malloc(capacity);
|
||||
|
||||
if (buffer == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*length = 0;
|
||||
for (;;) {
|
||||
size_t available = capacity - *length;
|
||||
size_t bytes_read = fread(buffer + *length, 1, available, stdin);
|
||||
*length += bytes_read;
|
||||
|
||||
if (bytes_read < available) {
|
||||
if (ferror(stdin)) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (capacity > SIZE_MAX / 2U) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
capacity *= 2U;
|
||||
|
||||
{
|
||||
unsigned char *larger_buffer = realloc(buffer, capacity);
|
||||
if (larger_buffer == NULL) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
buffer = larger_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
*data = buffer;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const unsigned char *word;
|
||||
unsigned char *stdin_buffer = NULL;
|
||||
size_t length;
|
||||
|
||||
if (argc > 2) {
|
||||
fprintf(stderr, "usage: %s [ASCII_WORD]\n", argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (argc == 2) {
|
||||
word = (const unsigned char *)argv[1];
|
||||
length = strlen(argv[1]);
|
||||
} else {
|
||||
if (read_stdin(&stdin_buffer, &length) != 0) {
|
||||
fprintf(stderr, "failed to read input\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
word = stdin_buffer;
|
||||
}
|
||||
|
||||
while (length > 0U && is_ascii_trailing_space(word[length - 1U])) {
|
||||
--length;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < length; ++index) {
|
||||
if (word[index] > 0x7fU) {
|
||||
fprintf(stderr, "input must contain ASCII characters only\n");
|
||||
free(stdin_buffer);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
printf("%016" PRIx64 "\n", xxh64(word, length, XXH64_SEED));
|
||||
free(stdin_buffer);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render avalanche bit-probability CSV files as two PNG bar charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from statistics import fmean
|
||||
from typing import Sequence
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
BACKGROUND = "#f7f8fa"
|
||||
PANEL = "#ffffff"
|
||||
GRID = "#d9dee7"
|
||||
TEXT = "#172033"
|
||||
MUTED = "#637083"
|
||||
REFERENCE = "#d24b4b"
|
||||
COLORS = (
|
||||
"#377eb8",
|
||||
"#4daf4a",
|
||||
"#984ea3",
|
||||
"#ff7f00",
|
||||
"#e41a1c",
|
||||
"#00a6a6",
|
||||
"#a65628",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProbabilityRow:
|
||||
operation: str
|
||||
pairs: int
|
||||
probabilities: list[float]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChartScale:
|
||||
minimum: float
|
||||
maximum: float
|
||||
ticks: tuple[float, ...]
|
||||
|
||||
|
||||
def nice_step(value: float) -> float:
|
||||
exponent = math.floor(math.log10(value))
|
||||
fraction = value / 10**exponent
|
||||
nice_fraction = min((1.0, 2.0, 2.5, 5.0, 10.0), key=lambda item: abs(item - fraction))
|
||||
return nice_fraction * 10**exponent
|
||||
|
||||
|
||||
def make_scale(
|
||||
values: Sequence[float],
|
||||
*,
|
||||
hard_limits: tuple[float, float],
|
||||
reference: float | None = None,
|
||||
) -> ChartScale:
|
||||
"""Build a padded shared scale constrained to hard limits."""
|
||||
hard_minimum, hard_maximum = hard_limits
|
||||
finite_values = [value for value in values if math.isfinite(value)]
|
||||
if reference is not None:
|
||||
finite_values.append(reference)
|
||||
if not finite_values:
|
||||
finite_values = [hard_minimum, hard_maximum]
|
||||
|
||||
minimum = min(finite_values)
|
||||
maximum = max(finite_values)
|
||||
if minimum == maximum:
|
||||
expansion = (hard_maximum - hard_minimum) * 0.1
|
||||
minimum -= expansion / 2
|
||||
maximum += expansion / 2
|
||||
|
||||
span = maximum - minimum
|
||||
padded_minimum = max(hard_minimum, minimum - span * 0.1)
|
||||
padded_maximum = min(hard_maximum, maximum + span * 0.1)
|
||||
step = nice_step(max((padded_maximum - padded_minimum) / 5, 1e-12))
|
||||
scaled_minimum = max(hard_minimum, math.floor(padded_minimum / step) * step)
|
||||
scaled_maximum = min(hard_maximum, math.ceil(padded_maximum / step) * step)
|
||||
if scaled_minimum == scaled_maximum:
|
||||
scaled_minimum, scaled_maximum = hard_minimum, hard_maximum
|
||||
|
||||
tick_count = round((scaled_maximum - scaled_minimum) / step)
|
||||
ticks = [scaled_minimum + index * step for index in range(tick_count + 1)]
|
||||
if reference is not None and scaled_minimum <= reference <= scaled_maximum:
|
||||
ticks.append(reference)
|
||||
normalized_ticks = tuple(
|
||||
sorted({round(value, 12) for value in ticks if scaled_minimum <= value <= scaled_maximum})
|
||||
)
|
||||
return ChartScale(scaled_minimum, scaled_maximum, normalized_ticks)
|
||||
|
||||
|
||||
def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
names = (
|
||||
"DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
|
||||
if bold
|
||||
else "/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
if bold
|
||||
else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
)
|
||||
for name in names:
|
||||
try:
|
||||
return ImageFont.truetype(name, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def read_probability_map(path: Path) -> list[ProbabilityRow]:
|
||||
"""Read operation rows and numerically ordered bit columns from a CSV file."""
|
||||
try:
|
||||
stream = path.open(encoding="utf-8", newline="")
|
||||
except OSError as error:
|
||||
raise ValueError(f"не удалось открыть {path}: {error}") from error
|
||||
|
||||
with stream:
|
||||
reader = csv.DictReader(stream)
|
||||
fields = reader.fieldnames
|
||||
if not fields or "operation" not in fields or "pairs" not in fields:
|
||||
raise ValueError("CSV должен содержать колонки operation и pairs")
|
||||
|
||||
bit_fields: list[tuple[int, str]] = []
|
||||
for field in fields:
|
||||
if not field.startswith("bit_"):
|
||||
continue
|
||||
try:
|
||||
bit_fields.append((int(field.removeprefix("bit_")), field))
|
||||
except ValueError as error:
|
||||
raise ValueError(f"некорректная битовая колонка: {field}") from error
|
||||
bit_fields.sort()
|
||||
if not bit_fields:
|
||||
raise ValueError("CSV не содержит колонок bit_N")
|
||||
expected_bits = list(range(len(bit_fields)))
|
||||
actual_bits = [bit for bit, _field in bit_fields]
|
||||
if actual_bits != expected_bits:
|
||||
raise ValueError("битовые колонки должны непрерывно идти от bit_0")
|
||||
|
||||
rows: list[ProbabilityRow] = []
|
||||
for line_number, row in enumerate(reader, start=2):
|
||||
operation = (row.get("operation") or "").strip()
|
||||
if not operation:
|
||||
raise ValueError(f"строка {line_number}: пустая операция")
|
||||
try:
|
||||
pairs = int(row["pairs"] or "")
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError(
|
||||
f"строка {line_number}: некорректное число пар"
|
||||
) from error
|
||||
if pairs < 0:
|
||||
raise ValueError(f"строка {line_number}: число пар меньше нуля")
|
||||
|
||||
probabilities: list[float] = []
|
||||
for _bit, field in bit_fields:
|
||||
try:
|
||||
value = float(row[field] or "")
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError(
|
||||
f"строка {line_number}: некорректное значение {field}"
|
||||
) from error
|
||||
if not math.isnan(value) and not 0.0 <= value <= 1.0:
|
||||
raise ValueError(
|
||||
f"строка {line_number}: {field} должен быть от 0 до 1"
|
||||
)
|
||||
probabilities.append(value)
|
||||
rows.append(ProbabilityRow(operation, pairs, probabilities))
|
||||
|
||||
if not rows:
|
||||
raise ValueError("CSV не содержит строк с операциями")
|
||||
return rows
|
||||
|
||||
|
||||
def mean_absolute_deviation(probabilities: Sequence[float]) -> float:
|
||||
"""Return the mean |p - 0.5| over finite bit probabilities."""
|
||||
deviations = [
|
||||
abs(probability - 0.5)
|
||||
for probability in probabilities
|
||||
if math.isfinite(probability)
|
||||
]
|
||||
return fmean(deviations) if deviations else float("nan")
|
||||
|
||||
|
||||
def text_width(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
text: str,
|
||||
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
|
||||
) -> int:
|
||||
box = draw.textbbox((0, 0), text, font=font)
|
||||
return round(box[2] - box[0])
|
||||
|
||||
|
||||
def draw_centered_text(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
center_x: float,
|
||||
y: float,
|
||||
text: str,
|
||||
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
|
||||
fill: str = TEXT,
|
||||
) -> None:
|
||||
draw.text(
|
||||
(center_x - text_width(draw, text, font) / 2, y),
|
||||
text,
|
||||
font=font,
|
||||
fill=fill,
|
||||
)
|
||||
|
||||
|
||||
def draw_bit_panel(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
bounds: tuple[int, int, int, int],
|
||||
row: ProbabilityRow,
|
||||
color: str,
|
||||
scale: ChartScale,
|
||||
) -> None:
|
||||
left, top, right, bottom = bounds
|
||||
title_font = load_font(22, bold=True)
|
||||
label_font = load_font(14)
|
||||
tick_font = load_font(12)
|
||||
|
||||
draw.rounded_rectangle(bounds, radius=12, fill=PANEL, outline=GRID, width=1)
|
||||
draw.text((left + 18, top + 13), row.operation, font=title_font, fill=TEXT)
|
||||
pairs_text = f"pairs: {row.pairs}"
|
||||
draw.text(
|
||||
(right - 18 - text_width(draw, pairs_text, label_font), top + 17),
|
||||
pairs_text,
|
||||
font=label_font,
|
||||
fill=MUTED,
|
||||
)
|
||||
|
||||
plot_left = left + 54
|
||||
plot_right = right - 18
|
||||
plot_top = top + 55
|
||||
plot_bottom = bottom - 42
|
||||
plot_height = plot_bottom - plot_top
|
||||
|
||||
scale_span = scale.maximum - scale.minimum
|
||||
for probability in scale.ticks:
|
||||
y = round(plot_bottom - (probability - scale.minimum) / scale_span * plot_height)
|
||||
line_color = REFERENCE if probability == 0.5 else GRID
|
||||
line_width = 2 if probability == 0.5 else 1
|
||||
draw.line((plot_left, y, plot_right, y), fill=line_color, width=line_width)
|
||||
label = f"{probability:.3g}"
|
||||
draw.text(
|
||||
(plot_left - 8 - text_width(draw, label, tick_font), y - 7),
|
||||
label,
|
||||
font=tick_font,
|
||||
fill=MUTED,
|
||||
)
|
||||
|
||||
bit_count = len(row.probabilities)
|
||||
slot_width = (plot_right - plot_left) / bit_count
|
||||
bar_width = max(1, int(slot_width * 0.72))
|
||||
for bit, probability in enumerate(row.probabilities):
|
||||
if not math.isfinite(probability):
|
||||
continue
|
||||
center = plot_left + (bit + 0.5) * slot_width
|
||||
x0 = round(center - bar_width / 2)
|
||||
x1 = round(center + bar_width / 2)
|
||||
y = round(
|
||||
plot_bottom
|
||||
- (probability - scale.minimum) / scale_span * plot_height
|
||||
)
|
||||
draw.rectangle((x0, y, x1, plot_bottom), fill=color)
|
||||
|
||||
tick_step = max(1, math.ceil(bit_count / 16))
|
||||
for bit in range(0, bit_count, tick_step):
|
||||
center = plot_left + (bit + 0.5) * slot_width
|
||||
label = str(bit)
|
||||
draw.text(
|
||||
(center - text_width(draw, label, tick_font) / 2, plot_bottom + 7),
|
||||
label,
|
||||
font=tick_font,
|
||||
fill=MUTED,
|
||||
)
|
||||
draw_centered_text(
|
||||
draw,
|
||||
(plot_left + plot_right) / 2,
|
||||
bottom - 21,
|
||||
"output bit (0 = LSB)",
|
||||
tick_font,
|
||||
MUTED,
|
||||
)
|
||||
|
||||
|
||||
def render_bit_probabilities(
|
||||
rows: Sequence[ProbabilityRow], output: Path, title: str
|
||||
) -> None:
|
||||
columns = 2 if len(rows) > 1 else 1
|
||||
panel_width = 760
|
||||
panel_height = 330
|
||||
gap = 18
|
||||
margin = 24
|
||||
title_height = 70
|
||||
row_count = math.ceil(len(rows) / columns)
|
||||
width = margin * 2 + columns * panel_width + (columns - 1) * gap
|
||||
height = title_height + margin + row_count * panel_height + (row_count - 1) * gap
|
||||
|
||||
image = Image.new("RGB", (width, height), BACKGROUND)
|
||||
draw = ImageDraw.Draw(image)
|
||||
scale = make_scale(
|
||||
[probability for row in rows for probability in row.probabilities],
|
||||
reference=0.5,
|
||||
hard_limits=(0.0, 1.0),
|
||||
)
|
||||
draw_centered_text(draw, width / 2, 18, title, load_font(30, bold=True))
|
||||
draw_centered_text(
|
||||
draw,
|
||||
width / 2,
|
||||
52,
|
||||
f"Shared scale {scale.minimum:.3g}–{scale.maximum:.3g}; red line = ideal p=0.5",
|
||||
load_font(14),
|
||||
MUTED,
|
||||
)
|
||||
|
||||
for index, row in enumerate(rows):
|
||||
column = index % columns
|
||||
grid_row = index // columns
|
||||
left = margin + column * (panel_width + gap)
|
||||
top = title_height + grid_row * (panel_height + gap)
|
||||
draw_bit_panel(
|
||||
draw,
|
||||
(left, top, left + panel_width, top + panel_height),
|
||||
row,
|
||||
COLORS[index % len(COLORS)],
|
||||
scale,
|
||||
)
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(output, "PNG", optimize=True)
|
||||
|
||||
|
||||
def render_mean_deviations(
|
||||
rows: Sequence[ProbabilityRow], output: Path, title: str
|
||||
) -> None:
|
||||
width, height = 1200, 720
|
||||
image = Image.new("RGB", (width, height), BACKGROUND)
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw_centered_text(draw, width / 2, 22, title, load_font(30, bold=True))
|
||||
draw_centered_text(
|
||||
draw,
|
||||
width / 2,
|
||||
58,
|
||||
"Mean absolute deviation from ideal avalanche probability: mean(|p - 0.5|)",
|
||||
load_font(15),
|
||||
MUTED,
|
||||
)
|
||||
|
||||
plot_left, plot_right = 90, width - 40
|
||||
plot_top, plot_bottom = 110, height - 120
|
||||
plot_height = plot_bottom - plot_top
|
||||
deviations = [mean_absolute_deviation(row.probabilities) for row in rows]
|
||||
scale = make_scale(deviations, hard_limits=(0.0, 0.5))
|
||||
scale_span = scale.maximum - scale.minimum
|
||||
tick_font = load_font(13)
|
||||
label_font = load_font(15)
|
||||
value_font = load_font(14, bold=True)
|
||||
|
||||
for value in scale.ticks:
|
||||
y = round(plot_bottom - (value - scale.minimum) / scale_span * plot_height)
|
||||
draw.line((plot_left, y, plot_right, y), fill=GRID, width=1)
|
||||
label = f"{value:.3g}"
|
||||
draw.text(
|
||||
(plot_left - 10 - text_width(draw, label, tick_font), y - 7),
|
||||
label,
|
||||
font=tick_font,
|
||||
fill=MUTED,
|
||||
)
|
||||
|
||||
slot_width = (plot_right - plot_left) / len(rows)
|
||||
bar_width = min(105, max(20, int(slot_width * 0.62)))
|
||||
for index, (row, deviation) in enumerate(zip(rows, deviations, strict=True)):
|
||||
center = plot_left + (index + 0.5) * slot_width
|
||||
x0 = round(center - bar_width / 2)
|
||||
x1 = round(center + bar_width / 2)
|
||||
if math.isfinite(deviation):
|
||||
y = round(
|
||||
plot_bottom
|
||||
- (deviation - scale.minimum) / scale_span * plot_height
|
||||
)
|
||||
draw.rectangle((x0, y, x1, plot_bottom), fill=COLORS[index % len(COLORS)])
|
||||
value_label = f"{deviation:.4f}"
|
||||
else:
|
||||
y = plot_bottom
|
||||
value_label = "n/a"
|
||||
draw_centered_text(draw, center, max(plot_top, y - 22), value_label, value_font)
|
||||
draw_centered_text(draw, center, plot_bottom + 12, row.operation, label_font)
|
||||
draw_centered_text(
|
||||
draw, center, plot_bottom + 36, f"pairs: {row.pairs}", tick_font, MUTED
|
||||
)
|
||||
|
||||
draw.line((plot_left, plot_top, plot_left, plot_bottom), fill=TEXT, width=2)
|
||||
draw.line((plot_left, plot_bottom, plot_right, plot_bottom), fill=TEXT, width=2)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(output, "PNG", optimize=True)
|
||||
|
||||
|
||||
def generate_plots(source: Path, output_directory: Path | None = None) -> tuple[Path, Path]:
|
||||
rows = read_probability_map(source)
|
||||
destination = output_directory or source.parent
|
||||
bit_output = destination / f"{source.stem}_bits.png"
|
||||
deviation_output = destination / f"{source.stem}_deviation.png"
|
||||
display_name = source.stem
|
||||
|
||||
render_bit_probabilities(rows, bit_output, f"{display_name}: per-bit avalanche map")
|
||||
render_mean_deviations(
|
||||
rows,
|
||||
deviation_output,
|
||||
f"{display_name}: deviation from p=0.5",
|
||||
)
|
||||
return bit_output, deviation_output
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Создаёт две PNG-гистограммы из CSV вероятностной карты: "
|
||||
"вероятности по битам для каждой операции и среднее |p-0.5|."
|
||||
)
|
||||
)
|
||||
parser.add_argument("csv_file", type=Path, help="CSV из probability_map.py")
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
help="каталог PNG (по умолчанию каталог исходного CSV)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
outputs = generate_plots(args.csv_file, args.output_dir)
|
||||
except ValueError as error:
|
||||
raise SystemExit(f"error: {error}") from error
|
||||
|
||||
for output in outputs:
|
||||
print(f"wrote {output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,354 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build bit-probability maps for hash functions and word mutation types.
|
||||
|
||||
For every hash listed in HASHES, the script compares each source word hash with
|
||||
hashes of generated similar words. A table cell contains the probability that
|
||||
the corresponding output bit changed (XOR with its source word hash).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
from generate_input import (
|
||||
DEFAULT_ALPHABET,
|
||||
MUTATIONS,
|
||||
MutationError,
|
||||
generate_words,
|
||||
parse_operation,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
HASH_FUNCS_DIR = ROOT / "hash_funcs"
|
||||
DEFAULT_OUTPUT_DIR = ROOT / "probability_maps"
|
||||
|
||||
# Add directory names from hash_funcs here to include more implementations.
|
||||
HASHES = [
|
||||
"jenkinsOAAT",
|
||||
"xxh64",
|
||||
"t1ha2",
|
||||
]
|
||||
|
||||
HEX_HASH = re.compile(r"(?:0[xX])?([0-9a-fA-F]+)")
|
||||
ProbabilityRow = tuple[int, list[float]]
|
||||
|
||||
|
||||
class HashToolError(RuntimeError):
|
||||
"""Raised when a hash executable cannot be prepared or invoked."""
|
||||
|
||||
|
||||
def prepare_hash(hash_name: str, hash_funcs_dir: Path = HASH_FUNCS_DIR) -> Path:
|
||||
"""Return an executable hash tool, building or preparing it if necessary."""
|
||||
if not hash_name or Path(hash_name).name != hash_name:
|
||||
raise HashToolError(f"некорректное имя хэша: {hash_name!r}")
|
||||
|
||||
hash_directory = hash_funcs_dir / hash_name
|
||||
if not hash_directory.is_dir():
|
||||
raise HashToolError(f"не найдена папка хэша: {hash_directory}")
|
||||
|
||||
binary = hash_directory / "bin_hash"
|
||||
if binary.is_file():
|
||||
binary.chmod(binary.stat().st_mode | stat.S_IXUSR)
|
||||
return binary
|
||||
|
||||
sources = (
|
||||
(hash_directory / "bin_hash.c", "cc"),
|
||||
(hash_directory / "bin_hash.cpp", "c++"),
|
||||
(hash_directory / "bin_hash.cc", "c++"),
|
||||
(hash_directory / "bin_hash.cxx", "c++"),
|
||||
)
|
||||
for source, compiler in sources:
|
||||
if not source.is_file():
|
||||
continue
|
||||
command = [
|
||||
compiler,
|
||||
"-O2",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-Werror",
|
||||
str(source),
|
||||
"-o",
|
||||
str(binary),
|
||||
]
|
||||
if compiler == "cc":
|
||||
command[1:1] = ["-std=c11"]
|
||||
else:
|
||||
command[1:1] = ["-std=c++17"]
|
||||
result = subprocess.run(command, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
details = result.stderr.strip() or result.stdout.strip()
|
||||
raise HashToolError(f"не удалось скомпилировать {source}: {details}")
|
||||
return binary
|
||||
|
||||
python_source = hash_directory / "bin_hash.py"
|
||||
if python_source.is_file():
|
||||
content = python_source.read_text(encoding="utf-8")
|
||||
if not content.startswith("#!"):
|
||||
python_source.write_text(
|
||||
"#!/usr/bin/env python3\n" + content,
|
||||
encoding="utf-8",
|
||||
)
|
||||
python_source.chmod(
|
||||
python_source.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
)
|
||||
return python_source
|
||||
|
||||
raise HashToolError(
|
||||
f"для {hash_name} не найден bin_hash, bin_hash.c/cpp/cc/cxx или bin_hash.py"
|
||||
)
|
||||
|
||||
|
||||
def run_hash(executable: Path, word: str) -> tuple[int, int]:
|
||||
"""Run a hash tool and return its integer value and explicit output width."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(executable), word],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError as error:
|
||||
raise HashToolError(f"не удалось запустить {executable}: {error}") from error
|
||||
|
||||
if result.returncode != 0:
|
||||
details = result.stderr.strip() or result.stdout.strip()
|
||||
raise HashToolError(
|
||||
f"{executable} завершился с кодом {result.returncode}: {details}"
|
||||
)
|
||||
|
||||
output = result.stdout.strip()
|
||||
match = HEX_HASH.fullmatch(output)
|
||||
if match is None:
|
||||
raise HashToolError(f"{executable} вернул не шестнадцатеричный хэш: {output!r}")
|
||||
|
||||
digits = match.group(1)
|
||||
return int(digits, 16), len(digits) * 4
|
||||
|
||||
|
||||
def bit_probabilities(
|
||||
source_hash: int, changed_hashes: Sequence[int], bits: int
|
||||
) -> list[float]:
|
||||
"""Calculate per-bit change probabilities, ordered from LSB to MSB."""
|
||||
if bits < 1:
|
||||
raise ValueError("число бит должно быть положительным")
|
||||
if not changed_hashes:
|
||||
raise ValueError("список изменённых хэшей не должен быть пустым")
|
||||
|
||||
changed_counts = [0] * bits
|
||||
for changed_hash in changed_hashes:
|
||||
difference = source_hash ^ changed_hash
|
||||
for bit in range(bits):
|
||||
changed_counts[bit] += (difference >> bit) & 1
|
||||
|
||||
sample_count = len(changed_hashes)
|
||||
return [count / sample_count for count in changed_counts]
|
||||
|
||||
|
||||
def write_csv_table(stream: TextIO, rows: Mapping[str, ProbabilityRow]) -> None:
|
||||
"""Write one operation-by-bit probability table as CSV."""
|
||||
if not rows:
|
||||
raise ValueError("таблица вероятностей не должна быть пустой")
|
||||
|
||||
widths = {len(probabilities) for _pair_count, probabilities in rows.values()}
|
||||
if len(widths) != 1:
|
||||
raise ValueError("все строки таблицы должны иметь одинаковое число бит")
|
||||
bits = widths.pop()
|
||||
|
||||
writer = csv.writer(stream, lineterminator="\n")
|
||||
writer.writerow(["operation", "pairs", *(f"bit_{bit}" for bit in range(bits))])
|
||||
for operation, (pair_count, probabilities) in rows.items():
|
||||
writer.writerow(
|
||||
[
|
||||
operation,
|
||||
pair_count,
|
||||
*(f"{probability:.6f}" for probability in probabilities),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def build_probability_table(
|
||||
executable: Path,
|
||||
sources: Sequence[str],
|
||||
operations: Sequence[str],
|
||||
count: int,
|
||||
operation_count: int,
|
||||
alphabet: str,
|
||||
seed: int | None,
|
||||
max_attempts: int | None,
|
||||
) -> dict[str, ProbabilityRow]:
|
||||
"""Aggregate avalanche probabilities across all source words."""
|
||||
if not sources:
|
||||
raise ValueError("нужно указать хотя бы одно исходное слово")
|
||||
|
||||
source_hashes: list[int] = []
|
||||
bits: int | None = None
|
||||
for source in sources:
|
||||
source_hash, source_bits = run_hash(executable, source)
|
||||
if bits is None:
|
||||
bits = source_bits
|
||||
elif source_bits != bits:
|
||||
raise HashToolError(
|
||||
f"{executable} вернул хэши разной ширины: "
|
||||
f"{bits} и {source_bits} бит"
|
||||
)
|
||||
source_hashes.append(source_hash)
|
||||
|
||||
assert bits is not None
|
||||
table: dict[str, ProbabilityRow] = {}
|
||||
|
||||
for operation in operations:
|
||||
differences: list[int] = []
|
||||
for source, source_hash in zip(sources, source_hashes, strict=True):
|
||||
words = generate_words(
|
||||
source=source,
|
||||
count=count,
|
||||
operation=operation,
|
||||
operation_count=operation_count,
|
||||
alphabet=alphabet,
|
||||
seed=seed,
|
||||
max_attempts=max_attempts,
|
||||
)
|
||||
if len(words) < count:
|
||||
print(
|
||||
f"warning: operation={operation} word={source!r}: "
|
||||
f"generated {len(words)} of at most {count} unique words",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for word in words:
|
||||
changed_hash, changed_bits = run_hash(executable, word)
|
||||
if changed_bits != bits:
|
||||
raise HashToolError(
|
||||
f"{executable} вернул хэши разной ширины: "
|
||||
f"{bits} и {changed_bits} бит"
|
||||
)
|
||||
differences.append(source_hash ^ changed_hash)
|
||||
|
||||
if differences:
|
||||
table[operation] = (
|
||||
len(differences),
|
||||
bit_probabilities(0, differences, bits),
|
||||
)
|
||||
else:
|
||||
table[operation] = (0, [float("nan")] * bits)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Строит для каждого хэша CSV-таблицу вероятностей изменения "
|
||||
"выходных битов. bit_0 — младший бит."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"words",
|
||||
nargs="+",
|
||||
help="одно или несколько исходных ASCII-слов",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--operation",
|
||||
action="append",
|
||||
type=parse_operation,
|
||||
help=(
|
||||
"тип изменения (номер или имя как в generate_input.py); "
|
||||
"можно повторять, по умолчанию используются все типы"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
default=10,
|
||||
help=(
|
||||
"верхняя граница числа уникальных изменённых слов для каждого "
|
||||
"исходного слова и типа (по умолчанию: 10)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--operations",
|
||||
type=int,
|
||||
default=1,
|
||||
help="число операций над каждым словом (по умолчанию: 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alphabet",
|
||||
default=DEFAULT_ALPHABET,
|
||||
help="алфавит для добавления и замены",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed", type=int, help="seed генератора для воспроизводимого результата"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-attempts",
|
||||
type=int,
|
||||
help="предельное число попыток собрать уникальные слова",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=DEFAULT_OUTPUT_DIR,
|
||||
help=("каталог для CSV-таблиц " f"(по умолчанию: {DEFAULT_OUTPUT_DIR})"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hash",
|
||||
dest="hashes",
|
||||
action="append",
|
||||
help="проверить только указанный хэш; можно повторять (по умолчанию HASHES)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.count < 1:
|
||||
parser.error("--count должен быть положительным")
|
||||
if args.operations < 1:
|
||||
parser.error("--operations должен быть положительным")
|
||||
if args.max_attempts is not None and args.max_attempts < 1:
|
||||
parser.error("--max-attempts должен быть положительным")
|
||||
|
||||
operations = list(dict.fromkeys(args.operation or MUTATIONS.keys()))
|
||||
hashes = list(dict.fromkeys(args.hashes or HASHES))
|
||||
if not hashes:
|
||||
parser.error("массив HASHES не должен быть пустым")
|
||||
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
for hash_name in hashes:
|
||||
executable = prepare_hash(hash_name)
|
||||
table = build_probability_table(
|
||||
executable=executable,
|
||||
sources=args.words,
|
||||
operations=operations,
|
||||
count=args.count,
|
||||
operation_count=args.operations,
|
||||
alphabet=args.alphabet,
|
||||
seed=args.seed,
|
||||
max_attempts=args.max_attempts,
|
||||
)
|
||||
output_path = args.output / f"{hash_name}.csv"
|
||||
with output_path.open("w", encoding="utf-8", newline="") as stream:
|
||||
write_csv_table(stream, table)
|
||||
print(f"wrote {output_path}")
|
||||
except (HashToolError, MutationError, ValueError) as error:
|
||||
parser.error(str(error))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,8 +0,0 @@
|
||||
operation,pairs,bit_0,bit_1,bit_2,bit_3,bit_4,bit_5,bit_6,bit_7,bit_8,bit_9,bit_10,bit_11,bit_12,bit_13,bit_14,bit_15,bit_16,bit_17,bit_18,bit_19,bit_20,bit_21,bit_22,bit_23,bit_24,bit_25,bit_26,bit_27,bit_28,bit_29,bit_30,bit_31
|
||||
replace,1984,0.484375,0.499496,0.494960,0.497984,0.506048,0.490423,0.501512,0.511593,0.496472,0.499496,0.515121,0.504536,0.485383,0.500504,0.496976,0.477319,0.510081,0.490423,0.492440,0.501512,0.502016,0.484879,0.490927,0.490927,0.511593,0.494960,0.512097,0.523185,0.498992,0.505544,0.496472,0.500504
|
||||
delete,31,0.612903,0.451613,0.612903,0.516129,0.419355,0.548387,0.451613,0.612903,0.419355,0.548387,0.677419,0.419355,0.580645,0.516129,0.451613,0.516129,0.516129,0.645161,0.548387,0.516129,0.548387,0.419355,0.516129,0.419355,0.645161,0.612903,0.516129,0.580645,0.516129,0.419355,0.580645,0.354839
|
||||
add,2173,0.501611,0.492867,0.500690,0.505292,0.515877,0.505292,0.505292,0.505752,0.497929,0.502991,0.485044,0.503451,0.496088,0.524160,0.517257,0.507593,0.489646,0.476760,0.496549,0.521859,0.514956,0.514036,0.513116,0.508053,0.492867,0.503451,0.518638,0.506673,0.495628,0.506213,0.518178,0.489185
|
||||
swap,28,0.607143,0.500000,0.428571,0.535714,0.392857,0.535714,0.500000,0.357143,0.571429,0.428571,0.678571,0.750000,0.428571,0.464286,0.392857,0.464286,0.500000,0.428571,0.428571,0.428571,0.464286,0.428571,0.500000,0.678571,0.571429,0.464286,0.607143,0.571429,0.571429,0.535714,0.428571,0.607143
|
||||
case,29,0.344828,0.551724,0.517241,0.448276,0.448276,0.310345,0.482759,0.344828,0.379310,0.413793,0.413793,0.448276,0.620690,0.620690,0.448276,0.586207,0.517241,0.551724,0.482759,0.448276,0.413793,0.517241,0.275862,0.655172,0.551724,0.586207,0.551724,0.482759,0.448276,0.551724,0.517241,0.655172
|
||||
first,186,0.500000,0.500000,0.494624,0.467742,0.521505,0.451613,0.489247,0.478495,0.521505,0.543011,0.516129,0.510753,0.478495,0.510753,0.510753,0.510753,0.456989,0.473118,0.456989,0.543011,0.489247,0.478495,0.456989,0.467742,0.500000,0.483871,0.483871,0.489247,0.559140,0.559140,0.462366,0.483871
|
||||
last,186,0.510753,0.516129,0.510753,0.467742,0.510753,0.505376,0.473118,0.516129,0.500000,0.516129,0.516129,0.521505,0.483871,0.526882,0.510753,0.451613,0.548387,0.489247,0.505376,0.430108,0.526882,0.456989,0.478495,0.505376,0.516129,0.537634,0.526882,0.478495,0.467742,0.494624,0.446237,0.548387
|
||||
|
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,8 +0,0 @@
|
||||
operation,pairs,bit_0,bit_1,bit_2,bit_3,bit_4,bit_5,bit_6,bit_7,bit_8,bit_9,bit_10,bit_11,bit_12,bit_13,bit_14,bit_15,bit_16,bit_17,bit_18,bit_19,bit_20,bit_21,bit_22,bit_23,bit_24,bit_25,bit_26,bit_27,bit_28,bit_29,bit_30,bit_31,bit_32,bit_33,bit_34,bit_35,bit_36,bit_37,bit_38,bit_39,bit_40,bit_41,bit_42,bit_43,bit_44,bit_45,bit_46,bit_47,bit_48,bit_49,bit_50,bit_51,bit_52,bit_53,bit_54,bit_55,bit_56,bit_57,bit_58,bit_59,bit_60,bit_61,bit_62,bit_63
|
||||
replace,1984,0.511593,0.515121,0.510081,0.525706,0.499496,0.504032,0.495464,0.494960,0.522681,0.521169,0.507056,0.484879,0.502016,0.482359,0.513609,0.513105,0.500504,0.503024,0.496472,0.509073,0.491431,0.488911,0.495464,0.491431,0.494960,0.491431,0.496976,0.496976,0.502016,0.511593,0.502016,0.496976,0.504536,0.499496,0.494456,0.496976,0.496976,0.490927,0.523690,0.489919,0.495968,0.478327,0.492944,0.529234,0.495968,0.496472,0.500000,0.493448,0.519657,0.510585,0.493448,0.496976,0.495464,0.501512,0.512097,0.500504,0.494960,0.494456,0.512097,0.494456,0.494960,0.495464,0.494456,0.515121
|
||||
delete,31,0.741935,0.387097,0.548387,0.354839,0.483871,0.451613,0.483871,0.612903,0.451613,0.645161,0.516129,0.419355,0.419355,0.483871,0.516129,0.483871,0.548387,0.741935,0.354839,0.483871,0.645161,0.516129,0.516129,0.548387,0.516129,0.354839,0.387097,0.387097,0.354839,0.548387,0.419355,0.516129,0.548387,0.516129,0.322581,0.419355,0.387097,0.483871,0.483871,0.419355,0.451613,0.612903,0.451613,0.419355,0.516129,0.580645,0.548387,0.483871,0.387097,0.612903,0.419355,0.516129,0.483871,0.548387,0.516129,0.516129,0.677419,0.451613,0.483871,0.451613,0.516129,0.612903,0.516129,0.483871
|
||||
add,2173,0.496088,0.508514,0.489646,0.488725,0.500230,0.514496,0.491486,0.479521,0.499770,0.484123,0.488725,0.495168,0.499310,0.484584,0.498850,0.530143,0.488265,0.484123,0.519098,0.494248,0.497469,0.505752,0.501611,0.520018,0.497929,0.512195,0.498389,0.474919,0.498389,0.504372,0.491026,0.514496,0.496549,0.479521,0.492867,0.499310,0.499770,0.498389,0.480902,0.493787,0.492407,0.505292,0.492867,0.499310,0.494708,0.511735,0.524620,0.506213,0.518638,0.518178,0.492867,0.498850,0.498389,0.502531,0.509434,0.498850,0.492867,0.499310,0.501611,0.506213,0.482743,0.503451,0.511275,0.493787
|
||||
swap,28,0.571429,0.571429,0.607143,0.535714,0.428571,0.392857,0.428571,0.535714,0.571429,0.535714,0.571429,0.464286,0.285714,0.714286,0.607143,0.500000,0.500000,0.357143,0.392857,0.571429,0.678571,0.464286,0.428571,0.392857,0.357143,0.392857,0.607143,0.571429,0.392857,0.642857,0.464286,0.678571,0.750000,0.428571,0.607143,0.428571,0.535714,0.500000,0.571429,0.642857,0.428571,0.357143,0.535714,0.571429,0.535714,0.571429,0.535714,0.464286,0.607143,0.642857,0.642857,0.678571,0.285714,0.500000,0.642857,0.571429,0.500000,0.357143,0.464286,0.428571,0.571429,0.571429,0.678571,0.535714
|
||||
case,29,0.413793,0.413793,0.620690,0.586207,0.379310,0.586207,0.482759,0.448276,0.551724,0.448276,0.655172,0.586207,0.655172,0.586207,0.379310,0.551724,0.517241,0.310345,0.517241,0.517241,0.310345,0.620690,0.482759,0.655172,0.551724,0.379310,0.413793,0.482759,0.517241,0.517241,0.482759,0.448276,0.586207,0.448276,0.620690,0.448276,0.448276,0.448276,0.551724,0.586207,0.551724,0.413793,0.413793,0.586207,0.517241,0.482759,0.551724,0.482759,0.586207,0.586207,0.379310,0.482759,0.586207,0.379310,0.482759,0.344828,0.620690,0.482759,0.448276,0.551724,0.482759,0.551724,0.344828,0.482759
|
||||
first,186,0.500000,0.500000,0.526882,0.526882,0.489247,0.483871,0.456989,0.532258,0.510753,0.483871,0.537634,0.456989,0.500000,0.424731,0.500000,0.435484,0.532258,0.456989,0.478495,0.532258,0.430108,0.489247,0.521505,0.478495,0.478495,0.532258,0.478495,0.532258,0.500000,0.473118,0.473118,0.537634,0.575269,0.526882,0.505376,0.526882,0.424731,0.462366,0.548387,0.478495,0.462366,0.532258,0.500000,0.473118,0.473118,0.559140,0.510753,0.526882,0.564516,0.521505,0.532258,0.521505,0.564516,0.537634,0.467742,0.473118,0.500000,0.424731,0.424731,0.462366,0.494624,0.489247,0.537634,0.559140
|
||||
last,186,0.510753,0.543011,0.580645,0.516129,0.516129,0.564516,0.500000,0.478495,0.489247,0.494624,0.494624,0.478495,0.521505,0.462366,0.526882,0.521505,0.564516,0.451613,0.489247,0.543011,0.537634,0.467742,0.467742,0.526882,0.500000,0.526882,0.478495,0.532258,0.462366,0.473118,0.569892,0.505376,0.516129,0.494624,0.456989,0.478495,0.537634,0.473118,0.516129,0.494624,0.440860,0.505376,0.537634,0.548387,0.408602,0.569892,0.537634,0.559140,0.494624,0.559140,0.526882,0.559140,0.489247,0.569892,0.537634,0.543011,0.543011,0.494624,0.537634,0.505376,0.473118,0.494624,0.532258,0.483871
|
||||
|
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,8 +0,0 @@
|
||||
operation,pairs,bit_0,bit_1,bit_2,bit_3,bit_4,bit_5,bit_6,bit_7,bit_8,bit_9,bit_10,bit_11,bit_12,bit_13,bit_14,bit_15,bit_16,bit_17,bit_18,bit_19,bit_20,bit_21,bit_22,bit_23,bit_24,bit_25,bit_26,bit_27,bit_28,bit_29,bit_30,bit_31,bit_32,bit_33,bit_34,bit_35,bit_36,bit_37,bit_38,bit_39,bit_40,bit_41,bit_42,bit_43,bit_44,bit_45,bit_46,bit_47,bit_48,bit_49,bit_50,bit_51,bit_52,bit_53,bit_54,bit_55,bit_56,bit_57,bit_58,bit_59,bit_60,bit_61,bit_62,bit_63
|
||||
replace,1984,0.491431,0.497984,0.532258,0.504536,0.494456,0.519657,0.516129,0.493448,0.501008,0.519657,0.490423,0.495968,0.516129,0.498488,0.491935,0.509073,0.520665,0.487903,0.497984,0.487903,0.502016,0.504536,0.502520,0.510081,0.512097,0.492440,0.505544,0.492944,0.491431,0.500000,0.509577,0.511593,0.491431,0.485383,0.481351,0.493448,0.498992,0.498992,0.499496,0.504032,0.495968,0.483367,0.495464,0.498488,0.512097,0.489415,0.502520,0.496472,0.501008,0.495464,0.503528,0.507056,0.510081,0.504032,0.487399,0.523690,0.511089,0.472278,0.496976,0.501008,0.503528,0.504032,0.494456,0.491935
|
||||
delete,31,0.548387,0.516129,0.451613,0.580645,0.548387,0.483871,0.548387,0.387097,0.451613,0.451613,0.451613,0.387097,0.677419,0.419355,0.354839,0.612903,0.387097,0.419355,0.548387,0.645161,0.451613,0.548387,0.451613,0.322581,0.516129,0.483871,0.516129,0.387097,0.483871,0.483871,0.483871,0.451613,0.483871,0.516129,0.645161,0.483871,0.580645,0.612903,0.677419,0.516129,0.516129,0.516129,0.516129,0.516129,0.677419,0.419355,0.516129,0.612903,0.612903,0.451613,0.483871,0.419355,0.516129,0.419355,0.580645,0.483871,0.645161,0.419355,0.419355,0.677419,0.451613,0.483871,0.419355,0.516129
|
||||
add,2173,0.506213,0.502531,0.484584,0.506213,0.505292,0.469397,0.497929,0.499310,0.487345,0.516797,0.502071,0.504832,0.515416,0.498389,0.486884,0.492867,0.505752,0.484123,0.513116,0.503451,0.508974,0.494248,0.501150,0.509894,0.485964,0.482743,0.490106,0.501150,0.487345,0.491486,0.515416,0.502071,0.497009,0.480902,0.499310,0.510815,0.509894,0.499310,0.511735,0.510354,0.498389,0.485504,0.497009,0.511275,0.504832,0.490566,0.495628,0.495628,0.498850,0.497009,0.485504,0.498850,0.506673,0.488265,0.492407,0.509434,0.495168,0.489185,0.500230,0.489646,0.498850,0.493327,0.493327,0.504832
|
||||
swap,28,0.535714,0.607143,0.428571,0.321429,0.535714,0.464286,0.500000,0.357143,0.464286,0.500000,0.464286,0.607143,0.535714,0.357143,0.607143,0.321429,0.428571,0.571429,0.464286,0.357143,0.357143,0.464286,0.464286,0.607143,0.500000,0.642857,0.642857,0.535714,0.428571,0.464286,0.535714,0.535714,0.285714,0.607143,0.500000,0.571429,0.428571,0.642857,0.500000,0.607143,0.571429,0.392857,0.500000,0.535714,0.535714,0.392857,0.571429,0.285714,0.642857,0.500000,0.535714,0.321429,0.428571,0.428571,0.464286,0.678571,0.642857,0.571429,0.607143,0.535714,0.535714,0.571429,0.285714,0.357143
|
||||
case,29,0.448276,0.551724,0.586207,0.620690,0.482759,0.655172,0.655172,0.586207,0.517241,0.379310,0.551724,0.448276,0.482759,0.482759,0.586207,0.448276,0.517241,0.448276,0.448276,0.517241,0.551724,0.620690,0.655172,0.655172,0.551724,0.586207,0.655172,0.586207,0.482759,0.379310,0.586207,0.517241,0.517241,0.551724,0.517241,0.655172,0.413793,0.551724,0.551724,0.448276,0.517241,0.379310,0.517241,0.517241,0.586207,0.310345,0.620690,0.379310,0.413793,0.448276,0.586207,0.689655,0.482759,0.310345,0.551724,0.413793,0.448276,0.517241,0.586207,0.551724,0.517241,0.448276,0.448276,0.448276
|
||||
first,186,0.521505,0.456989,0.543011,0.516129,0.505376,0.494624,0.569892,0.516129,0.564516,0.500000,0.500000,0.500000,0.516129,0.446237,0.580645,0.532258,0.521505,0.521505,0.473118,0.462366,0.462366,0.473118,0.526882,0.500000,0.500000,0.516129,0.532258,0.532258,0.489247,0.559140,0.462366,0.564516,0.521505,0.467742,0.456989,0.516129,0.537634,0.483871,0.548387,0.500000,0.532258,0.516129,0.478495,0.478495,0.462366,0.483871,0.494624,0.526882,0.537634,0.478495,0.446237,0.473118,0.478495,0.564516,0.435484,0.467742,0.526882,0.494624,0.494624,0.494624,0.543011,0.478495,0.430108,0.494624
|
||||
last,186,0.526882,0.446237,0.526882,0.521505,0.494624,0.521505,0.521505,0.483871,0.516129,0.500000,0.478495,0.500000,0.564516,0.500000,0.381720,0.500000,0.537634,0.473118,0.532258,0.543011,0.505376,0.510753,0.483871,0.456989,0.564516,0.489247,0.596774,0.526882,0.510753,0.462366,0.494624,0.494624,0.408602,0.548387,0.505376,0.543011,0.494624,0.467742,0.462366,0.440860,0.548387,0.505376,0.516129,0.510753,0.543011,0.462366,0.478495,0.505376,0.478495,0.532258,0.510753,0.586022,0.526882,0.478495,0.467742,0.516129,0.543011,0.478495,0.510753,0.478495,0.494624,0.505376,0.548387,0.478495
|
||||
|
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Behavior tests for plot_probability_map.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import plot_probability_map
|
||||
|
||||
|
||||
class ProbabilityMapPlotTests(unittest.TestCase):
|
||||
def test_reads_bit_columns_and_ignores_operation_and_pairs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
path = Path(temporary_directory) / "hash.csv"
|
||||
path.write_text(
|
||||
"operation,pairs,bit_0,bit_1\n"
|
||||
"replace,4,0.25,0.75\n"
|
||||
"delete,2,0.5,nan\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
rows = plot_probability_map.read_probability_map(path)
|
||||
|
||||
self.assertEqual(rows[0].operation, "replace")
|
||||
self.assertEqual(rows[0].pairs, 4)
|
||||
self.assertEqual(rows[0].probabilities, [0.25, 0.75])
|
||||
self.assertEqual(rows[1].operation, "delete")
|
||||
self.assertEqual(rows[1].pairs, 2)
|
||||
self.assertEqual(len(rows[1].probabilities), 2)
|
||||
|
||||
def test_mean_absolute_deviation_from_half_ignores_nan(self) -> None:
|
||||
result = plot_probability_map.mean_absolute_deviation(
|
||||
[0.25, 0.5, 0.75, float("nan")]
|
||||
)
|
||||
|
||||
self.assertEqual(result, 1 / 6)
|
||||
|
||||
def test_shared_scale_zooms_to_all_values_and_reference(self) -> None:
|
||||
scale = plot_probability_map.make_scale(
|
||||
[0.45, 0.48, 0.52, 0.55],
|
||||
reference=0.5,
|
||||
hard_limits=(0.0, 1.0),
|
||||
)
|
||||
|
||||
self.assertGreater(scale.minimum, 0.0)
|
||||
self.assertLess(scale.maximum, 1.0)
|
||||
self.assertLessEqual(scale.minimum, 0.45)
|
||||
self.assertGreaterEqual(scale.maximum, 0.55)
|
||||
self.assertIn(0.5, scale.ticks)
|
||||
|
||||
def test_deviation_scale_uses_data_range_instead_of_fixed_half(self) -> None:
|
||||
scale = plot_probability_map.make_scale(
|
||||
[0.05, 0.08], hard_limits=(0.0, 0.5)
|
||||
)
|
||||
|
||||
self.assertGreater(scale.minimum, 0.0)
|
||||
self.assertLess(scale.maximum, 0.5)
|
||||
self.assertLessEqual(scale.minimum, 0.05)
|
||||
self.assertGreaterEqual(scale.maximum, 0.08)
|
||||
|
||||
def test_generates_two_nonempty_png_files(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
source = root / "sample.csv"
|
||||
source.write_text(
|
||||
"operation,pairs,bit_0,bit_1,bit_2,bit_3\n"
|
||||
"replace,4,0.25,0.50,0.75,1.0\n"
|
||||
"delete,2,0.10,0.20,0.30,0.40\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
outputs = plot_probability_map.generate_plots(source, root / "plots")
|
||||
|
||||
self.assertEqual(len(outputs), 2)
|
||||
for output in outputs:
|
||||
self.assertTrue(output.is_file())
|
||||
with Image.open(output) as image:
|
||||
self.assertEqual(image.format, "PNG")
|
||||
self.assertGreater(image.width, 300)
|
||||
self.assertGreater(image.height, 200)
|
||||
colors = image.convert("RGB").getcolors(maxcolors=1_000_000)
|
||||
self.assertIsNotNone(colors)
|
||||
self.assertGreater(len(colors or []), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Behavior tests for probability_map.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stderr
|
||||
from pathlib import Path
|
||||
|
||||
import generate_input
|
||||
import probability_map
|
||||
|
||||
|
||||
class PrepareHashTests(unittest.TestCase):
|
||||
def test_compiles_c_source_when_binary_is_missing(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
hash_directory = root / "constant"
|
||||
hash_directory.mkdir()
|
||||
(hash_directory / "bin_hash.c").write_text(
|
||||
"#include <stdio.h>\n"
|
||||
"int main(void) { puts(\"00000001\"); return 0; }\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
executable = probability_map.prepare_hash("constant", root)
|
||||
|
||||
self.assertEqual(executable, hash_directory / "bin_hash")
|
||||
self.assertEqual(
|
||||
subprocess.check_output([executable, "word"], text=True).strip(),
|
||||
"00000001",
|
||||
)
|
||||
|
||||
def test_adds_python_shebang_and_execute_permission(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
hash_directory = root / "python_hash"
|
||||
hash_directory.mkdir()
|
||||
source = hash_directory / "bin_hash.py"
|
||||
source.write_text("print('00000002')\n", encoding="utf-8")
|
||||
|
||||
executable = probability_map.prepare_hash("python_hash", root)
|
||||
|
||||
self.assertEqual(executable, source)
|
||||
self.assertTrue(os.access(source, os.X_OK))
|
||||
self.assertTrue(
|
||||
source.read_text(encoding="utf-8").startswith(
|
||||
"#!/usr/bin/env python3\n"
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
subprocess.check_output([executable, "word"], text=True).strip(),
|
||||
"00000002",
|
||||
)
|
||||
|
||||
|
||||
class ProbabilityTests(unittest.TestCase):
|
||||
def test_counts_changed_hash_bits_relative_to_source(self) -> None:
|
||||
probabilities = probability_map.bit_probabilities(
|
||||
source_hash=0b0000,
|
||||
changed_hashes=[0b0001, 0b0011, 0b0010, 0b0000],
|
||||
bits=4,
|
||||
)
|
||||
|
||||
self.assertEqual(probabilities, [0.5, 0.5, 0.0, 0.0])
|
||||
|
||||
def test_csv_table_has_operation_rows_and_bit_columns(self) -> None:
|
||||
stream = io.StringIO()
|
||||
|
||||
probability_map.write_csv_table(
|
||||
stream,
|
||||
{
|
||||
"replace": (4, [0.25, 0.75]),
|
||||
"delete": (2, [0.5, 0.0]),
|
||||
},
|
||||
)
|
||||
|
||||
rows = list(csv.reader(io.StringIO(stream.getvalue())))
|
||||
self.assertEqual(rows[0], ["operation", "pairs", "bit_0", "bit_1"])
|
||||
self.assertEqual(rows[1], ["replace", "4", "0.250000", "0.750000"])
|
||||
self.assertEqual(rows[2], ["delete", "2", "0.500000", "0.000000"])
|
||||
|
||||
def test_parser_accepts_multiple_source_words(self) -> None:
|
||||
arguments = probability_map.build_parser().parse_args(["first", "second"])
|
||||
|
||||
self.assertEqual(arguments.words, ["first", "second"])
|
||||
|
||||
def test_aggregates_samples_from_multiple_words_and_warns_on_shortfall(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
executable = Path(temporary_directory) / "hash.py"
|
||||
executable.write_text(
|
||||
"#!/usr/bin/env python3\n"
|
||||
"import sys\n"
|
||||
"print(f'{sum(sys.argv[1].encode()):08x}')\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
executable.chmod(0o755)
|
||||
warnings = io.StringIO()
|
||||
|
||||
with redirect_stderr(warnings):
|
||||
table = probability_map.build_probability_table(
|
||||
executable=executable,
|
||||
sources=["A", "B"],
|
||||
operations=["delete"],
|
||||
count=10,
|
||||
operation_count=1,
|
||||
alphabet=generate_input.DEFAULT_ALPHABET,
|
||||
seed=1,
|
||||
max_attempts=None,
|
||||
)
|
||||
|
||||
pair_count, probabilities = table["delete"]
|
||||
self.assertEqual(pair_count, 2)
|
||||
self.assertEqual(
|
||||
probabilities[:8],
|
||||
[0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
)
|
||||
self.assertEqual(warnings.getvalue().count("delete"), 2)
|
||||
|
||||
|
||||
class GenerateWordsTests(unittest.TestCase):
|
||||
def test_count_is_an_upper_bound_when_unique_results_are_exhausted(self) -> None:
|
||||
words = generate_input.generate_words(
|
||||
source="abc",
|
||||
count=100,
|
||||
operation="delete",
|
||||
operation_count=1,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
self.assertEqual(set(words), {"ab", "ac", "bc"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Behavior tests for the standalone t1ha2_atonce hash CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "hash_funcs" / "t1ha2" / "bin_hash.c"
|
||||
|
||||
|
||||
class T1ha2BinHashTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.temporary_directory = tempfile.TemporaryDirectory()
|
||||
cls.binary = Path(cls.temporary_directory.name) / "bin_hash"
|
||||
subprocess.run(
|
||||
[
|
||||
"cc",
|
||||
"-std=c11",
|
||||
"-O2",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-Werror",
|
||||
str(SOURCE),
|
||||
"-o",
|
||||
str(cls.binary),
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
def run_hash(
|
||||
self, word: str | None = None, stdin: bytes | None = None
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
command = [str(self.binary)]
|
||||
if word is not None:
|
||||
command.append(word)
|
||||
return subprocess.run(command, input=stdin, capture_output=True, check=False)
|
||||
|
||||
def test_matches_upstream_t1ha2_atonce_seed_zero_vectors(self) -> None:
|
||||
vectors = {
|
||||
"": b"0000000000000000\n",
|
||||
"hello": b"2a5f2abd74df73b4\n",
|
||||
"HashWord": b"3885d16135ce64f0\n",
|
||||
"abc": b"16bae0f716c45f2e\n",
|
||||
"12345678901234567890123456789012": b"75ed8a8aa66a4602\n",
|
||||
}
|
||||
|
||||
for word, expected in vectors.items():
|
||||
with self.subTest(word=word):
|
||||
result = self.run_hash(word)
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertEqual(result.stdout, expected)
|
||||
self.assertRegex(result.stdout.decode(), r"^[0-9a-f]{16}\n$")
|
||||
|
||||
def test_argv_and_stdin_are_equivalent_and_strip_trailing_ascii_space(self) -> None:
|
||||
argv = self.run_hash("hello")
|
||||
stdin = self.run_hash(stdin=b"hello \t\r\n")
|
||||
|
||||
self.assertEqual(stdin.returncode, 0)
|
||||
self.assertEqual(stdin.stdout, argv.stdout)
|
||||
|
||||
def test_handles_long_ascii_input(self) -> None:
|
||||
payload = b"a" * 100_000
|
||||
result = self.run_hash(stdin=payload)
|
||||
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertEqual(result.stdout, b"bdf3f8539f0504ea\n")
|
||||
|
||||
def test_rejects_non_ascii_input(self) -> None:
|
||||
result = self.run_hash(stdin="ёж".encode())
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(b"ASCII", result.stderr)
|
||||
self.assertEqual(result.stdout, b"")
|
||||
|
||||
def test_unaligned_argv_is_clean_under_undefined_behavior_sanitizer(self) -> None:
|
||||
sanitized = Path(self.temporary_directory.name) / "bin_hash_ubsan"
|
||||
subprocess.run(
|
||||
[
|
||||
"cc",
|
||||
"-std=c11",
|
||||
"-O1",
|
||||
"-g",
|
||||
"-fsanitize=undefined",
|
||||
"-fno-sanitize-recover=undefined",
|
||||
str(SOURCE),
|
||||
"-o",
|
||||
str(sanitized),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
for word in ("hello", "12345678", "a" * 33):
|
||||
with self.subTest(word=word):
|
||||
result = subprocess.run(
|
||||
[str(sanitized), word], capture_output=True, check=False
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr.decode())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Behavior tests for the standalone XXH64 hash CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "hash_funcs" / "xxh64" / "bin_hash.c"
|
||||
|
||||
|
||||
class Xxh64BinHashTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.temporary_directory = tempfile.TemporaryDirectory()
|
||||
cls.binary = Path(cls.temporary_directory.name) / "bin_hash"
|
||||
subprocess.run(
|
||||
[
|
||||
"cc",
|
||||
"-std=c11",
|
||||
"-O2",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-Werror",
|
||||
str(SOURCE),
|
||||
"-o",
|
||||
str(cls.binary),
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
def run_hash(self, word: str | None = None, stdin: bytes | None = None) -> subprocess.CompletedProcess[bytes]:
|
||||
command = [str(self.binary)]
|
||||
if word is not None:
|
||||
command.append(word)
|
||||
return subprocess.run(command, input=stdin, capture_output=True, check=False)
|
||||
|
||||
def test_matches_official_xxh64_seed_zero_vectors(self) -> None:
|
||||
vectors = {
|
||||
"": b"ef46db3751d8e999\n",
|
||||
"hello": b"26c7827d889f6da3\n",
|
||||
"HashWord": b"3e26fc2935163fbe\n",
|
||||
}
|
||||
|
||||
for word, expected in vectors.items():
|
||||
with self.subTest(word=word):
|
||||
result = self.run_hash(word)
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertEqual(result.stdout, expected)
|
||||
self.assertRegex(result.stdout.decode(), r"^[0-9a-f]{16}\n$")
|
||||
|
||||
def test_argv_and_stdin_are_equivalent_and_strip_trailing_ascii_space(self) -> None:
|
||||
argv = self.run_hash("hello")
|
||||
stdin = self.run_hash(stdin=b"hello \t\r\n")
|
||||
|
||||
self.assertEqual(stdin.returncode, 0)
|
||||
self.assertEqual(stdin.stdout, argv.stdout)
|
||||
|
||||
def test_handles_long_ascii_input(self) -> None:
|
||||
payload = b"a" * 100_000
|
||||
result = self.run_hash(stdin=payload)
|
||||
reference = subprocess.run(
|
||||
["xxhsum", "-H64"], input=payload, capture_output=True, check=True
|
||||
).stdout.split()[0]
|
||||
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertEqual(result.stdout.strip(), reference)
|
||||
|
||||
def test_rejects_non_ascii_input(self) -> None:
|
||||
result = self.run_hash(stdin="ёж".encode())
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(b"ASCII", result.stderr)
|
||||
self.assertEqual(result.stdout, b"")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,296 +0,0 @@
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef ARSV_WITH_RPM
|
||||
#include <rpm/header.h>
|
||||
#include <rpm/rpmio.h>
|
||||
#include <rpm/rpmlib.h>
|
||||
#include <rpm/rpmtag.h>
|
||||
#include <rpm/rpmtd.h>
|
||||
#endif
|
||||
|
||||
int arsv_set9_decode(const char* source, unsigned** hashes, size_t* count, unsigned* bpp);
|
||||
|
||||
#define D1_PREFIX "set:D1"
|
||||
#define D1_HEADER_LEN 8
|
||||
|
||||
static const char base64_alphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static size_t base64_encoded_size(size_t byte_count) {
|
||||
if (byte_count > SIZE_MAX - 2) return SIZE_MAX;
|
||||
size_t groups = (byte_count + 2) / 3;
|
||||
if (groups > SIZE_MAX / 4) return SIZE_MAX;
|
||||
size_t size = groups * 4;
|
||||
size_t remainder = byte_count % 3;
|
||||
if (remainder) size -= 3 - remainder;
|
||||
return size;
|
||||
}
|
||||
|
||||
static void base64_encode(const unsigned char* input, size_t input_len, char* output) {
|
||||
while (input_len >= 3) {
|
||||
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8) | input[2];
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = base64_alphabet[(value >> 6) & 0x3f];
|
||||
output[3] = base64_alphabet[value & 0x3f];
|
||||
input += 3;
|
||||
input_len -= 3;
|
||||
output += 4;
|
||||
}
|
||||
if (input_len == 1) {
|
||||
uint32_t value = (uint32_t)input[0] << 16;
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
} else if (input_len == 2) {
|
||||
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8);
|
||||
output[0] = base64_alphabet[(value >> 18) & 0x3f];
|
||||
output[1] = base64_alphabet[(value >> 12) & 0x3f];
|
||||
output[2] = base64_alphabet[(value >> 6) & 0x3f];
|
||||
}
|
||||
}
|
||||
|
||||
static int encode_d1(const unsigned* hashes, size_t count, unsigned bpp, char** result) {
|
||||
if (!hashes || !count || !result || bpp < 10 || bpp > 32) return -EINVAL;
|
||||
if (count > (SIZE_MAX - 7) / bpp) return -EOVERFLOW;
|
||||
|
||||
size_t bit_count = count * bpp;
|
||||
size_t byte_count = (bit_count + 7) / 8;
|
||||
unsigned char* bytes = calloc(byte_count, 1);
|
||||
if (!bytes) return -ENOMEM;
|
||||
|
||||
unsigned char* output = bytes;
|
||||
uint64_t bits = 0;
|
||||
unsigned filled = 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
bits |= (uint64_t)hashes[i] << filled;
|
||||
filled += bpp;
|
||||
while (filled >= 8) {
|
||||
*output++ = (unsigned char)bits;
|
||||
bits >>= 8;
|
||||
filled -= 8;
|
||||
}
|
||||
}
|
||||
if (filled) *output++ = (unsigned char)bits;
|
||||
if ((size_t)(output - bytes) != byte_count) {
|
||||
free(bytes);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
size_t payload_len = base64_encoded_size(byte_count);
|
||||
if (payload_len == SIZE_MAX || payload_len > SIZE_MAX - D1_HEADER_LEN - 1) {
|
||||
free(bytes);
|
||||
return -EOVERFLOW;
|
||||
}
|
||||
char* encoded = malloc(D1_HEADER_LEN + payload_len + 1);
|
||||
if (!encoded) {
|
||||
free(bytes);
|
||||
return -ENOMEM;
|
||||
}
|
||||
memcpy(encoded, D1_PREFIX, sizeof(D1_PREFIX) - 1);
|
||||
encoded[6] = (char)('0' + bpp / 10);
|
||||
encoded[7] = (char)('0' + bpp % 10);
|
||||
base64_encode(bytes, byte_count, encoded + D1_HEADER_LEN);
|
||||
encoded[D1_HEADER_LEN + payload_len] = '\0';
|
||||
free(bytes);
|
||||
*result = encoded;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int convert_set(const char* source, char** result, size_t* hash_count, unsigned* bpp) {
|
||||
if (!source || strncmp(source, "set:", 4) != 0 || strncmp(source, D1_PREFIX, 6) == 0)
|
||||
return -EINVAL;
|
||||
|
||||
unsigned* hashes = NULL;
|
||||
size_t count = 0;
|
||||
unsigned precision = 0;
|
||||
int rc = arsv_set9_decode(source, &hashes, &count, &precision);
|
||||
if (rc == 0) rc = encode_d1(hashes, count, precision, result);
|
||||
free(hashes);
|
||||
if (rc < 0) return rc;
|
||||
if (hash_count) *hash_count = count;
|
||||
if (bpp) *bpp = precision;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef ARSV_WITH_RPM
|
||||
struct statistics {
|
||||
unsigned long headers;
|
||||
unsigned long set_occurrences;
|
||||
unsigned long set_bytes_old;
|
||||
unsigned long set_bytes_new;
|
||||
unsigned long hashes;
|
||||
};
|
||||
|
||||
static int rewrite_version_tag(Header header, rpmTagVal tag, const char* package,
|
||||
struct statistics* stats) {
|
||||
struct rpmtd_s values;
|
||||
memset(&values, 0, sizeof(values));
|
||||
if (headerGet(header, tag, &values, HEADERGET_MINMEM) != 1) return 0;
|
||||
if (rpmtdType(&values) != RPM_STRING_ARRAY_TYPE) {
|
||||
rpmtdFreeData(&values);
|
||||
fprintf(stderr, "%s: tag %d is not a string array\n", package, (int)tag);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
rpm_count_t count = rpmtdCount(&values);
|
||||
const char** rewritten = calloc((size_t)count, sizeof(*rewritten));
|
||||
if (!rewritten) {
|
||||
rpmtdFreeData(&values);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
int changed = 0;
|
||||
rpmtdInit(&values);
|
||||
for (rpm_count_t i = 0; i < count; ++i) {
|
||||
const char* value = rpmtdNextString(&values);
|
||||
if (!value) {
|
||||
rc = -EINVAL;
|
||||
break;
|
||||
}
|
||||
if (strncmp(value, "set:", 4) == 0) {
|
||||
char* converted = NULL;
|
||||
size_t hashes = 0;
|
||||
unsigned bpp = 0;
|
||||
rc = convert_set(value, &converted, &hashes, &bpp);
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "%s: cannot convert tag %d index %u (rc=%d)\n", package,
|
||||
(int)tag, (unsigned)i, rc);
|
||||
break;
|
||||
}
|
||||
rewritten[i] = converted;
|
||||
changed = 1;
|
||||
++stats->set_occurrences;
|
||||
stats->set_bytes_old += strlen(value);
|
||||
stats->set_bytes_new += strlen(converted);
|
||||
stats->hashes += hashes;
|
||||
} else {
|
||||
rewritten[i] = strdup(value);
|
||||
if (!rewritten[i]) {
|
||||
rc = -ENOMEM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rc == 0 && changed) {
|
||||
headerDel(header, tag);
|
||||
if (!headerPutStringArray(header, tag, rewritten, count)) rc = -EIO;
|
||||
}
|
||||
for (rpm_count_t i = 0; i < count; ++i) free((void*)rewritten[i]);
|
||||
free(rewritten);
|
||||
rpmtdFreeData(&values);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int rewrite_pkglist(const char* input_path, const char* output_path, unsigned long limit) {
|
||||
FD_t input = Fopen(input_path, "r.ufdio");
|
||||
if (!input || Ferror(input)) {
|
||||
fprintf(stderr, "%s: %s\n", input_path, input ? Fstrerror(input) : "cannot open");
|
||||
return 1;
|
||||
}
|
||||
FD_t output = Fopen(output_path, "w.ufdio");
|
||||
if (!output || Ferror(output)) {
|
||||
fprintf(stderr, "%s: %s\n", output_path, output ? Fstrerror(output) : "cannot open");
|
||||
Fclose(input);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct statistics stats = {0};
|
||||
Header header;
|
||||
int failed = 0;
|
||||
const rpmTagVal tags[] = {
|
||||
RPMTAG_REQUIREVERSION,
|
||||
RPMTAG_PROVIDEVERSION,
|
||||
RPMTAG_CONFLICTVERSION,
|
||||
RPMTAG_OBSOLETEVERSION,
|
||||
RPMTAG_RECOMMENDVERSION,
|
||||
RPMTAG_SUGGESTVERSION,
|
||||
RPMTAG_SUPPLEMENTVERSION,
|
||||
RPMTAG_ENHANCEVERSION,
|
||||
};
|
||||
while ((!limit || stats.headers < limit) &&
|
||||
(header = headerRead(input, HEADER_MAGIC_YES)) != NULL) {
|
||||
const char* package = headerGetString(header, RPMTAG_NAME);
|
||||
if (!package) package = "<unknown>";
|
||||
for (size_t i = 0; i < sizeof(tags) / sizeof(tags[0]); ++i) {
|
||||
int rc = rewrite_version_tag(header, tags[i], package, &stats);
|
||||
if (rc < 0) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!failed && headerWrite(output, header, HEADER_MAGIC_YES) != 0) {
|
||||
fprintf(stderr, "%s: failed to write header for %s\n", output_path, package);
|
||||
failed = 1;
|
||||
}
|
||||
headerFree(header);
|
||||
if (failed) break;
|
||||
++stats.headers;
|
||||
}
|
||||
if (!failed && Ferror(input)) {
|
||||
fprintf(stderr, "%s: read error: %s\n", input_path, Fstrerror(input));
|
||||
failed = 1;
|
||||
}
|
||||
if (Fclose(output) != 0) failed = 1;
|
||||
Fclose(input);
|
||||
|
||||
if (failed) {
|
||||
remove(output_path);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"headers=%lu set_occurrences=%lu hashes=%lu old_set_bytes=%lu "
|
||||
"new_set_bytes=%lu\n",
|
||||
stats.headers, stats.set_occurrences, stats.hashes, stats.set_bytes_old,
|
||||
stats.set_bytes_new);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void usage(const char* program) {
|
||||
fprintf(stderr, "usage: %s --convert-set set:VALUE\n", program);
|
||||
#ifdef ARSV_WITH_RPM
|
||||
fprintf(stderr, " %s --rewrite INPUT OUTPUT [--max-headers N]\n", program);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc == 3 && strcmp(argv[1], "--convert-set") == 0) {
|
||||
char* converted = NULL;
|
||||
int rc = convert_set(argv[2], &converted, NULL, NULL);
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "cannot convert set value (rc=%d)\n", rc);
|
||||
return 1;
|
||||
}
|
||||
puts(converted);
|
||||
free(converted);
|
||||
return 0;
|
||||
}
|
||||
#ifdef ARSV_WITH_RPM
|
||||
if ((argc == 4 || argc == 6) && strcmp(argv[1], "--rewrite") == 0) {
|
||||
unsigned long limit = 0;
|
||||
if (argc == 6) {
|
||||
if (strcmp(argv[4], "--max-headers") != 0) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
char* end = NULL;
|
||||
errno = 0;
|
||||
limit = strtoul(argv[5], &end, 10);
|
||||
if (errno || !end || *end || !limit) {
|
||||
fprintf(stderr, "invalid --max-headers value: %s\n", argv[5]);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
return rewrite_pkglist(argv[2], argv[3], limit);
|
||||
}
|
||||
#endif
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create D1 copies of the real Sisyphus x86_64 and noarch pkglist files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[2]
|
||||
ARCHITECTURES = ("x86_64", "noarch")
|
||||
SUMMARY_RE = re.compile(
|
||||
r"headers=(?P<headers>\d+) set_occurrences=(?P<set_occurrences>\d+) "
|
||||
r"hashes=(?P<hashes>\d+) old_set_bytes=(?P<old_set_bytes>\d+) "
|
||||
r"new_set_bytes=(?P<new_set_bytes>\d+)"
|
||||
)
|
||||
VERSION_FORMAT = (
|
||||
"[%{REQUIREVERSION}\\n]"
|
||||
"[%{PROVIDEVERSION}\\n]"
|
||||
"[%{CONFLICTVERSION}\\n]"
|
||||
"[%{OBSOLETEVERSION}\\n]"
|
||||
"[%{RECOMMENDVERSION}\\n]"
|
||||
"[%{SUGGESTVERSION}\\n]"
|
||||
"[%{SUPPLEMENTVERSION}\\n]"
|
||||
"[%{ENHANCEVERSION}\\n]"
|
||||
)
|
||||
|
||||
|
||||
def classify_pkglist(path: Path) -> str | None:
|
||||
name = path.name
|
||||
if not name.endswith("_base_pkglist.classic"):
|
||||
return None
|
||||
if "_Sisyphus_x86%5f64_" in name or "_Sisyphus_x86_64_" in name:
|
||||
return "x86_64"
|
||||
if "_Sisyphus_noarch_" in name:
|
||||
return "noarch"
|
||||
return None
|
||||
|
||||
|
||||
def validate_output(path: Path) -> Path:
|
||||
resolved = path.expanduser().resolve()
|
||||
home = Path.home().resolve()
|
||||
source = ROOT.resolve()
|
||||
if resolved == home or resolved == source or source in resolved.parents:
|
||||
raise ValueError(f"refusing protected output path: {resolved}")
|
||||
if resolved == Path("/"):
|
||||
raise ValueError("refusing filesystem root as output")
|
||||
return resolved
|
||||
|
||||
|
||||
def run(
|
||||
command: list[str],
|
||||
*,
|
||||
check: bool = True,
|
||||
cwd: Path | None = None,
|
||||
stdout: int | None = None,
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
stdout=stdout if stdout is not None else subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
stderr = result.stderr.decode(errors="replace")
|
||||
raise RuntimeError(f"command failed ({result.returncode}): {' '.join(command)}\n{stderr}")
|
||||
return result
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def compile_converter(
|
||||
destination: Path,
|
||||
rpm_include_dir: Path | None = None,
|
||||
rpm_libraries: tuple[Path, ...] = (),
|
||||
) -> None:
|
||||
include = destination.parent / "compat-include"
|
||||
include.mkdir()
|
||||
for name in ("rpmlib.h", "system.h", "set.h"):
|
||||
(include / name).touch()
|
||||
command = [
|
||||
"cc",
|
||||
"-O2",
|
||||
"-std=gnu11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-D_GNU_SOURCE",
|
||||
"-DARSV_SET9_EXPORT",
|
||||
"-DARSV_WITH_RPM",
|
||||
"-I",
|
||||
str(include),
|
||||
]
|
||||
if rpm_include_dir is not None:
|
||||
command.extend(["-I", str(rpm_include_dir)])
|
||||
command.extend(
|
||||
[
|
||||
"-include",
|
||||
str(ROOT / "scripts/rpmsetcmp/newset_compat.h"),
|
||||
str(ROOT / "reimplement/set9.c"),
|
||||
str(HERE / "rewrite_sisyphus_pkglist.c"),
|
||||
]
|
||||
)
|
||||
if rpm_libraries:
|
||||
command.extend(str(path) for path in rpm_libraries)
|
||||
else:
|
||||
command.extend(["-lrpm", "-lrpmio"])
|
||||
command.extend(["-o", str(destination)])
|
||||
run(command)
|
||||
|
||||
|
||||
def find_pkglists(lists_dir: Path) -> dict[str, Path]:
|
||||
found: dict[str, Path] = {}
|
||||
for path in lists_dir.glob("*_base_pkglist.classic"):
|
||||
architecture = classify_pkglist(path)
|
||||
if architecture is None:
|
||||
continue
|
||||
if architecture in found:
|
||||
raise RuntimeError(f"multiple Sisyphus {architecture} pkglist files")
|
||||
found[architecture] = path
|
||||
missing = set(ARCHITECTURES) - set(found)
|
||||
if missing:
|
||||
raise RuntimeError(f"missing Sisyphus pkglist for: {', '.join(sorted(missing))}")
|
||||
return found
|
||||
|
||||
|
||||
def query_header_count(path: Path) -> int:
|
||||
process = subprocess.Popen(
|
||||
["pkglist-query", "%{NAME}\\n", str(path)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
count = sum(1 for _ in process.stdout)
|
||||
stderr = process.stderr.read() if process.stderr else b""
|
||||
status = process.wait()
|
||||
if status != 0:
|
||||
raise RuntimeError(f"pkglist-query failed for {path}: {stderr.decode(errors='replace')}")
|
||||
return count
|
||||
|
||||
|
||||
def query_set_counts(path: Path) -> dict[str, int]:
|
||||
process = subprocess.Popen(
|
||||
["pkglist-query", VERSION_FORMAT, str(path)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
old = direct = 0
|
||||
for raw_line in process.stdout:
|
||||
value = raw_line.strip()
|
||||
if value.startswith(b"set:D1"):
|
||||
direct += 1
|
||||
elif value.startswith(b"set:"):
|
||||
old += 1
|
||||
stderr = process.stderr.read() if process.stderr else b""
|
||||
status = process.wait()
|
||||
if status != 0:
|
||||
raise RuntimeError(f"pkglist-query failed for {path}: {stderr.decode(errors='replace')}")
|
||||
return {"set9": old, "d1": direct}
|
||||
|
||||
|
||||
def conversion_summary(stderr: bytes) -> dict[str, int]:
|
||||
text = stderr.decode(errors="replace")
|
||||
match = SUMMARY_RE.search(text)
|
||||
if not match:
|
||||
raise RuntimeError(f"converter did not report summary:\n{text}")
|
||||
return {name: int(value) for name, value in match.groupdict().items()}
|
||||
|
||||
|
||||
def convert_one(converter: Path, source: Path, destination: Path) -> dict[str, object]:
|
||||
result = run([str(converter), "--rewrite", str(source), str(destination)])
|
||||
summary = conversion_summary(result.stderr)
|
||||
source_headers = query_header_count(source)
|
||||
output_headers = query_header_count(destination)
|
||||
source_sets = query_set_counts(source)
|
||||
output_sets = query_set_counts(destination)
|
||||
if source_headers != output_headers or output_headers != summary["headers"]:
|
||||
raise RuntimeError(
|
||||
f"header count mismatch: source={source_headers} output={output_headers} "
|
||||
f"converter={summary['headers']}"
|
||||
)
|
||||
if source_sets["d1"] != 0:
|
||||
raise RuntimeError(f"source already contains {source_sets['d1']} D1 values")
|
||||
if output_sets["set9"] != 0:
|
||||
raise RuntimeError(f"output still contains {output_sets['set9']} set9 values")
|
||||
if source_sets["set9"] != output_sets["d1"] or output_sets["d1"] != summary["set_occurrences"]:
|
||||
raise RuntimeError(
|
||||
"set occurrence mismatch: "
|
||||
f"source={source_sets['set9']} output={output_sets['d1']} "
|
||||
f"converter={summary['set_occurrences']}"
|
||||
)
|
||||
if summary["set_occurrences"] == 0:
|
||||
raise RuntimeError("source pkglist contains no set versions")
|
||||
return {
|
||||
**summary,
|
||||
"source": {
|
||||
"path": source.name,
|
||||
"size": source.stat().st_size,
|
||||
"sha256": sha256_file(source),
|
||||
},
|
||||
"output": {
|
||||
"path": destination.name,
|
||||
"size": destination.stat().st_size,
|
||||
"sha256": sha256_file(destination),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def convert_local(
|
||||
output: Path,
|
||||
lists_dir: Path,
|
||||
rpm_include_dir: Path | None = None,
|
||||
rpm_libraries: tuple[Path, ...] = (),
|
||||
) -> None:
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
if any(output.iterdir()):
|
||||
raise RuntimeError(f"output directory is not empty: {output}")
|
||||
|
||||
lists_dir = lists_dir.expanduser().resolve()
|
||||
if not lists_dir.is_dir():
|
||||
raise RuntimeError(f"APT lists directory does not exist: {lists_dir}")
|
||||
pkglists = find_pkglists(lists_dir)
|
||||
staging = Path(tempfile.mkdtemp(prefix=".d1-staging-", dir=output))
|
||||
try:
|
||||
converter = staging / "rewrite-sisyphus-pkglist"
|
||||
compile_converter(converter, rpm_include_dir, rpm_libraries)
|
||||
architectures: dict[str, object] = {}
|
||||
for architecture in ARCHITECTURES:
|
||||
destination = staging / f"Sisyphus.{architecture}.pkglist.classic"
|
||||
architectures[architecture] = convert_one(
|
||||
converter, pkglists[architecture], destination
|
||||
)
|
||||
converter.unlink()
|
||||
shutil.rmtree(staging / "compat-include")
|
||||
manifest = {
|
||||
"format": 1,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"input": {"lists_dir": str(lists_dir)},
|
||||
"rpm": run(["rpm", "--version"]).stdout.decode().strip(),
|
||||
"apt": run(["rpmquery", "--qf", "%{VERSION}-%{RELEASE}", "apt"])
|
||||
.stdout.decode()
|
||||
.strip(),
|
||||
"converter_source": {
|
||||
"set9_sha256": sha256_file(ROOT / "reimplement/set9.c"),
|
||||
"rewrite_sha256": sha256_file(HERE / "rewrite_sisyphus_pkglist.c"),
|
||||
},
|
||||
"architectures": architectures,
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n"
|
||||
)
|
||||
final = output / "d1-pkglists"
|
||||
staging.replace(final)
|
||||
print(final)
|
||||
for architecture in ARCHITECTURES:
|
||||
data = architectures[architecture]
|
||||
assert isinstance(data, dict)
|
||||
print(
|
||||
f"{architecture}: headers={data['headers']} "
|
||||
f"set_occurrences={data['set_occurrences']} "
|
||||
f"output={final / f'Sisyphus.{architecture}.pkglist.classic'}"
|
||||
)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create D1 copies of local Sisyphus x86_64/noarch pkglist files"
|
||||
)
|
||||
parser.add_argument("output", type=Path, help="new or empty output directory")
|
||||
parser.add_argument(
|
||||
"--lists-dir",
|
||||
type=Path,
|
||||
default=Path("/var/lib/apt/lists"),
|
||||
help="APT lists snapshot to convert (default: /var/lib/apt/lists)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rpm-include-dir",
|
||||
type=Path,
|
||||
help="directory containing rpm/header.h (for an unpacked librpm-devel)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rpm-library",
|
||||
type=Path,
|
||||
action="append",
|
||||
default=[],
|
||||
help="versioned RPM library to link; repeat for librpm and librpmio",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
output = validate_output(args.output)
|
||||
rpm_include_dir = (
|
||||
args.rpm_include_dir.expanduser().resolve()
|
||||
if args.rpm_include_dir is not None
|
||||
else None
|
||||
)
|
||||
rpm_libraries = tuple(path.expanduser().resolve() for path in args.rpm_library)
|
||||
if rpm_include_dir is not None and not (rpm_include_dir / "rpm/header.h").is_file():
|
||||
raise ValueError(f"rpm/header.h not found below: {rpm_include_dir}")
|
||||
if rpm_libraries and len(rpm_libraries) != 2:
|
||||
raise ValueError("pass exactly two --rpm-library values: librpm and librpmio")
|
||||
for library in rpm_libraries:
|
||||
if not library.is_file():
|
||||
raise ValueError(f"RPM library not found: {library}")
|
||||
convert_local(output, args.lists_dir, rpm_include_dir, rpm_libraries)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -3,15 +3,19 @@ import argparse
|
||||
import ctypes
|
||||
import gc
|
||||
import os
|
||||
import shutil
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
BUILD = HERE / "build"
|
||||
LIBC = ctypes.CDLL(None)
|
||||
LIBC.free.argtypes = [ctypes.c_void_p]
|
||||
TIME_COMMAND = os.environ.get("TIME_COMMAND") or shutil.which("time") or "/usr/bin/time"
|
||||
TIME_FORMAT = "%U\t%S"
|
||||
|
||||
|
||||
class SetAPI:
|
||||
@@ -49,108 +53,132 @@ class SetAPI:
|
||||
return encoded
|
||||
|
||||
|
||||
def median_fini(api, symbols, bpp, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
sets = [api.new_with_symbols(symbols) for _ in range(calls)]
|
||||
results = []
|
||||
start = time.perf_counter_ns()
|
||||
for value in sets:
|
||||
results.append(api.lib.set_fini(value, bpp))
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if not all(results):
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
encoded = [ctypes.string_at(result) for result in results]
|
||||
if len(set(encoded)) != 1:
|
||||
raise RuntimeError("set_fini is not deterministic")
|
||||
for value, result in zip(sets, results):
|
||||
api.release(value, result)
|
||||
return statistics.median(samples)
|
||||
def make_symbols(args):
|
||||
symbols = tuple(
|
||||
f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols)
|
||||
)
|
||||
required = (
|
||||
symbols[::2]
|
||||
if args.required is None
|
||||
else tuple(symbols[i * args.symbols // args.required] for i in range(args.required))
|
||||
)
|
||||
return symbols, required
|
||||
|
||||
|
||||
def median_build(api, symbols, bpp, calls, rounds):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
sets = []
|
||||
results = []
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
def api_for(name):
|
||||
if name == "set9":
|
||||
return SetAPI(BUILD / "libset9.so")
|
||||
if name == "direct":
|
||||
return SetAPI(BUILD / "libdirect-hash.so")
|
||||
raise ValueError(f"unknown implementation: {name}")
|
||||
|
||||
|
||||
def child_main(args):
|
||||
symbols, required = make_symbols(args)
|
||||
api = api_for(args.time_child_impl)
|
||||
checksum = 0
|
||||
|
||||
if args.time_child_operation == "fini":
|
||||
# Measured by /usr/bin/time around this child process. The repeated work
|
||||
# is set construction plus set_fini; keeping construction in the same
|
||||
# child avoids Python-side subsection timers while still reporting CPU
|
||||
# user/system time from the external time utility.
|
||||
for _ in range(args.fini_calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
result = api.lib.set_fini(value, bpp)
|
||||
sets.append(value)
|
||||
results.append(result)
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if not all(results):
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
for value, result in zip(sets, results):
|
||||
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)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_add(api, symbols, calls, rounds):
|
||||
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:
|
||||
elif args.time_child_operation == "add":
|
||||
for _ in range(args.fini_calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
checksum ^= int(value)
|
||||
api.lib.set_free(value)
|
||||
return statistics.median(samples)
|
||||
|
||||
|
||||
def median_cmp(api, provider, requirement, calls, rounds):
|
||||
expected = api.lib.rpmsetcmp(provider, requirement)
|
||||
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
|
||||
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
|
||||
for _ in range(100):
|
||||
api.lib.rpmsetcmp(provider, requirement)
|
||||
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
checksum = 0
|
||||
start = time.perf_counter_ns()
|
||||
for _ in range(calls):
|
||||
elif args.time_child_operation == "build":
|
||||
for _ in range(args.fini_calls):
|
||||
value = api.new_with_symbols(symbols)
|
||||
result = api.lib.set_fini(value, args.bpp)
|
||||
if not result:
|
||||
raise RuntimeError("set_fini returned NULL")
|
||||
checksum ^= len(ctypes.string_at(result))
|
||||
api.release(value, result)
|
||||
elif args.time_child_operation == "cmp-cold":
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
for _ in range(args.cold_calls):
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
result = api.lib.rpmsetcmp(provider, requirement)
|
||||
os._exit(0 if result == 1 else 1)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
if status != 0:
|
||||
raise RuntimeError(f"cold rpmsetcmp child failed: status={status}")
|
||||
checksum += 1
|
||||
elif args.time_child_operation == "cmp-warm":
|
||||
provider = api.encode(symbols, args.bpp)
|
||||
requirement = api.encode(required, args.bpp)
|
||||
expected = api.lib.rpmsetcmp(provider, requirement)
|
||||
if expected != 1 or api.lib.rpmsetcmp(provider, provider) != 0:
|
||||
raise RuntimeError(f"unexpected rpmsetcmp result: {expected}")
|
||||
for _ in range(100):
|
||||
api.lib.rpmsetcmp(provider, requirement)
|
||||
for _ in range(args.cmp_calls):
|
||||
checksum += api.lib.rpmsetcmp(provider, requirement)
|
||||
samples.append((time.perf_counter_ns() - start) / calls)
|
||||
if checksum != calls:
|
||||
if checksum != args.cmp_calls:
|
||||
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):
|
||||
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):
|
||||
def measure_with_time(args, implementation, operation, calls):
|
||||
samples = []
|
||||
for _ in range(rounds):
|
||||
total = sum(cold_cmp_once(api, provider, requirement) for _ in range(calls))
|
||||
samples.append(total / calls)
|
||||
return statistics.median(samples)
|
||||
for _ in range(args.rounds):
|
||||
with tempfile.NamedTemporaryFile(prefix="arsv-time-", delete=False) as handle:
|
||||
time_path = Path(handle.name)
|
||||
command = [
|
||||
TIME_COMMAND,
|
||||
"-f",
|
||||
TIME_FORMAT,
|
||||
"-o",
|
||||
str(time_path),
|
||||
"--",
|
||||
sys.executable,
|
||||
str(Path(__file__).resolve()),
|
||||
"--skip-build",
|
||||
"--time-child",
|
||||
"--time-child-impl",
|
||||
implementation,
|
||||
"--time-child-operation",
|
||||
operation,
|
||||
"--symbols",
|
||||
str(args.symbols),
|
||||
"--bpp",
|
||||
str(args.bpp),
|
||||
"--rounds",
|
||||
"1",
|
||||
"--fini-calls",
|
||||
str(args.fini_calls),
|
||||
"--cmp-calls",
|
||||
str(args.cmp_calls),
|
||||
"--cold-calls",
|
||||
str(args.cold_calls),
|
||||
]
|
||||
if args.required is not None:
|
||||
command.extend(["--required", str(args.required)])
|
||||
try:
|
||||
subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
user_text, system_text = time_path.read_text().split()
|
||||
finally:
|
||||
time_path.unlink(missing_ok=True)
|
||||
samples.append((float(user_text) / calls, float(system_text) / calls))
|
||||
users = [sample[0] for sample in samples]
|
||||
systems = [sample[1] for sample in samples]
|
||||
return statistics.median(users), statistics.median(systems)
|
||||
|
||||
|
||||
def verify_complete_decoding(api, provider, requirement):
|
||||
@@ -167,11 +195,17 @@ def verify_complete_decoding(api, provider, requirement):
|
||||
raise RuntimeError("second operand was not decoded and validated completely")
|
||||
|
||||
|
||||
def format_time(ns):
|
||||
return f"{ns / 1000:.2f} us"
|
||||
def format_cpu_time(seconds):
|
||||
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.add_argument("--symbols", type=int, default=1000)
|
||||
parser.add_argument(
|
||||
@@ -185,6 +219,18 @@ def main():
|
||||
parser.add_argument("--cmp-calls", type=int, default=2000)
|
||||
parser.add_argument("--cold-calls", type=int, default=20)
|
||||
parser.add_argument("--skip-build", action="store_true")
|
||||
parser.add_argument("--time-child", action="store_true", help=argparse.SUPPRESS)
|
||||
parser.add_argument("--time-child-impl", choices=("set9", "direct"), help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--time-child-operation",
|
||||
choices=("fini", "add", "build", "cmp-cold", "cmp-warm"),
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
if args.symbols < 2 or not 10 <= args.bpp <= 32:
|
||||
parser.error("symbols must be >= 2 and bpp must be in 10..32")
|
||||
@@ -192,26 +238,27 @@ def main():
|
||||
parser.error("required must be in 1..symbols-1")
|
||||
if min(args.rounds, args.fini_calls, args.cmp_calls, args.cold_calls) < 1:
|
||||
parser.error("rounds and call counts must be positive")
|
||||
if args.time_child and not (args.time_child_impl and args.time_child_operation):
|
||||
parser.error("--time-child requires --time-child-impl and --time-child-operation")
|
||||
|
||||
if not args.skip_build:
|
||||
subprocess.run([str(HERE / "build.sh")], check=True)
|
||||
|
||||
symbols = tuple(
|
||||
f"symbol_{i:08d}_version_ALT_{i % 97}".encode() for i in range(args.symbols)
|
||||
)
|
||||
required = (
|
||||
symbols[::2]
|
||||
if args.required is None
|
||||
else tuple(symbols[i * args.symbols // args.required] for i in range(args.required))
|
||||
)
|
||||
if args.time_child:
|
||||
child_main(args)
|
||||
return
|
||||
|
||||
if not Path(TIME_COMMAND).is_file():
|
||||
raise RuntimeError(f"time executable not found: {TIME_COMMAND}")
|
||||
|
||||
symbols, required = make_symbols(args)
|
||||
apis = {
|
||||
"set9": SetAPI(BUILD / "libset9.so"),
|
||||
"direct": SetAPI(BUILD / "libdirect-hash.so"),
|
||||
"set9": api_for("set9"),
|
||||
"direct": api_for("direct"),
|
||||
}
|
||||
|
||||
gc.disable()
|
||||
try:
|
||||
timings = {name: [[] for _ in range(5)] for name in apis}
|
||||
lengths = {}
|
||||
encoded = {}
|
||||
for name, api in apis.items():
|
||||
@@ -221,31 +268,20 @@ def main():
|
||||
wire_format = "D1/base64" if provider.startswith(b"D1") else "golomb/base62"
|
||||
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():
|
||||
verify_complete_decoding(api, *encoded[name])
|
||||
|
||||
operations = (
|
||||
("set_fini child", "fini", args.fini_calls),
|
||||
("new+add child", "add", args.fini_calls),
|
||||
("new+add+fini child", "build", args.fini_calls),
|
||||
("rpmsetcmp cold", "cmp-cold", args.cold_calls),
|
||||
("rpmsetcmp warm", "cmp-warm", args.cmp_calls),
|
||||
)
|
||||
timings = {name: [] for name in apis}
|
||||
for label, operation, calls in operations:
|
||||
for name in apis:
|
||||
timings[name].append(measure_with_time(args, name, operation, calls))
|
||||
finally:
|
||||
gc.enable()
|
||||
|
||||
@@ -253,18 +289,15 @@ def main():
|
||||
print("implementation set_chars format")
|
||||
for name in apis:
|
||||
print(f"{name:<14} {lengths[name][0]:>9} {lengths[name][1]}")
|
||||
print("\noperation set9 direct direct/set9")
|
||||
labels = (
|
||||
"set_fini only",
|
||||
"new+add (ctypes)",
|
||||
"new+add+fini (ctypes)",
|
||||
"rpmsetcmp cold",
|
||||
"rpmsetcmp warm",
|
||||
)
|
||||
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")
|
||||
print("\noperation set9_user set9_sys direct_user direct_sys user_ratio sys_ratio")
|
||||
for index, (label, _operation, _calls) in enumerate(operations):
|
||||
old_user, old_sys = timings["set9"][index]
|
||||
new_user, new_sys = timings["direct"][index]
|
||||
print(
|
||||
f"{label:<22} {format_cpu_time(old_user):>10} {format_cpu_time(old_sys):>9} "
|
||||
f"{format_cpu_time(new_user):>11} {format_cpu_time(new_sys):>10} "
|
||||
f"{format_ratio(new_user, old_user):>10} {format_ratio(new_sys, old_sys):>9}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -34,6 +33,7 @@ struct set {
|
||||
char* strings;
|
||||
struct symbols {
|
||||
size_t offset;
|
||||
unsigned full_hash;
|
||||
unsigned hash;
|
||||
}* symbols_v;
|
||||
};
|
||||
@@ -47,11 +47,9 @@ struct decoded_set {
|
||||
enum {
|
||||
DECODED_CACHE_SIZE = 512,
|
||||
DECODED_CACHE_BUCKETS = 1024,
|
||||
PAIR_CACHE_SIZE = 4,
|
||||
PAIR_CACHE_SIZE = 16,
|
||||
};
|
||||
|
||||
struct decoded_cache_entry;
|
||||
|
||||
struct pair_cache_entry {
|
||||
uint64_t other_identity;
|
||||
int result;
|
||||
@@ -65,8 +63,8 @@ struct decoded_cache_entry {
|
||||
unsigned* hashes;
|
||||
size_t len;
|
||||
size_t count;
|
||||
uint32_t fingerprint;
|
||||
uint64_t identity;
|
||||
uint32_t fingerprint;
|
||||
unsigned bucket;
|
||||
unsigned target_bpp;
|
||||
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_oldest[2];
|
||||
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 atomic_flag decoded_cache_lock = ATOMIC_FLAG_INIT;
|
||||
|
||||
static unsigned hash(const char* str);
|
||||
|
||||
struct set* set_new(void) {
|
||||
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].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);
|
||||
set->strings_len = required;
|
||||
++set->cnt;
|
||||
@@ -241,18 +239,42 @@ static void base64_encode(const unsigned char* input, size_t input_len, char* ou
|
||||
return;
|
||||
}
|
||||
|
||||
static unsigned char* pack_hashes(const unsigned* hashes, size_t count, unsigned bpp,
|
||||
size_t* byte_count) {
|
||||
static size_t compact_unique_hashes(struct symbols* symbols, size_t count) {
|
||||
size_t unique_count = 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
while (i + 1 < count && symbols[i].hash == symbols[i + 1].hash) ++i;
|
||||
symbols[unique_count++].hash = symbols[i].hash;
|
||||
}
|
||||
|
||||
return unique_count;
|
||||
}
|
||||
|
||||
static unsigned char* pack_symbol_hashes(const struct symbols* symbols, size_t count,
|
||||
unsigned bpp, size_t* byte_count) {
|
||||
if (count > (SIZE_MAX - 7) / bpp) abort();
|
||||
size_t bit_count = count * bpp;
|
||||
*byte_count = (bit_count + 7) / 8;
|
||||
unsigned char* bytes = xmalloc(*byte_count);
|
||||
unsigned char* output = bytes;
|
||||
|
||||
#if UINT_MAX == UINT32_MAX && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \
|
||||
__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
if (bpp == 32 && sizeof(unsigned) == 4) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
unsigned hash = symbols[i].hash;
|
||||
memcpy(output, &hash, sizeof(hash));
|
||||
output += sizeof(hash);
|
||||
}
|
||||
assert((size_t)(output - bytes) == *byte_count);
|
||||
return bytes;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint64_t bits = 0;
|
||||
unsigned filled = 0;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
bits |= (uint64_t)hashes[i] << filled;
|
||||
bits |= (uint64_t)symbols[i].hash << filled;
|
||||
filled += bpp;
|
||||
|
||||
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;
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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;
|
||||
unsigned char* allocated_bytes = NULL;
|
||||
const unsigned char* bytes;
|
||||
#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 unique_count = compact_unique_hashes(set->symbols_v, set->cnt);
|
||||
unsigned char* bytes = pack_symbol_hashes(set->symbols_v, unique_count, (unsigned)bpp, &byte_count);
|
||||
size_t payload_len = base64_encoded_size(byte_count);
|
||||
char* output = xmalloc(FORMAT_HEADER_LEN + payload_len + 1);
|
||||
memcpy(output, FORMAT_PREFIX, sizeof(FORMAT_PREFIX) - 1);
|
||||
@@ -315,8 +319,7 @@ const char* set_fini(struct set* set, int bpp) {
|
||||
base64_encode(bytes, byte_count, output + FORMAT_HEADER_LEN);
|
||||
output[FORMAT_HEADER_LEN + payload_len] = '\0';
|
||||
|
||||
_free(allocated_bytes);
|
||||
_free(unique_hashes);
|
||||
_free(bytes);
|
||||
|
||||
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)) return -1;
|
||||
|
||||
/* 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. */
|
||||
/* With bpp >= 10, a valid direct set has at least three Base64 characters. */
|
||||
if (str[0] != FORMAT_PREFIX[0] || str[1] != FORMAT_PREFIX[1]) return -1;
|
||||
if (str[2] < '0' || str[2] > '9' || str[3] < '0' || str[3] > '9') return -1;
|
||||
if (str[4] == '\0' || str[5] == '\0' || str[6] == '\0') return -1;
|
||||
@@ -367,7 +369,7 @@ static int set_meta_init(const char* source, struct set_meta* meta) {
|
||||
if (bpp < 10 || bpp > 32) return -1;
|
||||
|
||||
meta->str = str;
|
||||
meta->len = 0;
|
||||
meta->len = strlen(str);
|
||||
meta->bpp = bpp;
|
||||
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) {
|
||||
const unsigned char* str = (const unsigned char*)meta->str;
|
||||
uint32_t fingerprint = (uint32_t)str[4] | ((uint32_t)str[5] << 8) |
|
||||
((uint32_t)str[6] << 16) | ((uint32_t)str[7] << 24);
|
||||
fingerprint ^= meta->bpp * UINT32_C(0x27d4eb2d);
|
||||
fingerprint ^= target_bpp * UINT32_C(0x85ebca6b);
|
||||
fingerprint ^= fingerprint >> 11;
|
||||
fingerprint *= UINT32_C(0x9e3779b1);
|
||||
uint32_t fingerprint = UINT32_C(2166136261);
|
||||
fingerprint = (fingerprint ^ (uint32_t)meta->len) * UINT32_C(16777619);
|
||||
fingerprint = (fingerprint ^ (meta->bpp * UINT32_C(0x27d4eb2d))) * UINT32_C(16777619);
|
||||
fingerprint = (fingerprint ^ (target_bpp * UINT32_C(0x85ebca6b))) * UINT32_C(16777619);
|
||||
|
||||
size_t prefix_len = meta->len < 8 ? meta->len : 8;
|
||||
for (size_t i = 0; i < prefix_len; ++i) {
|
||||
fingerprint = (fingerprint ^ str[i]) * UINT32_C(16777619);
|
||||
}
|
||||
size_t suffix_start = meta->len > 8 ? meta->len - 8 : prefix_len;
|
||||
for (size_t i = suffix_start; i < meta->len; ++i) {
|
||||
fingerprint = (fingerprint ^ str[i]) * UINT32_C(16777619);
|
||||
}
|
||||
|
||||
fingerprint ^= fingerprint >> 16;
|
||||
fingerprint *= UINT32_C(0x7feb352d);
|
||||
fingerprint ^= fingerprint >> 15;
|
||||
fingerprint *= UINT32_C(0x846ca68b);
|
||||
fingerprint ^= fingerprint >> 16;
|
||||
return fingerprint;
|
||||
}
|
||||
@@ -783,14 +797,23 @@ static void decoded_cache_remove(struct decoded_cache_entry* victim, unsigned ca
|
||||
_free(victim);
|
||||
}
|
||||
|
||||
static void decoded_cache_reset_pair_identities(void) {
|
||||
for (unsigned bucket = 0; bucket < DECODED_CACHE_BUCKETS; ++bucket) {
|
||||
for (struct decoded_cache_entry* provider = decoded_cache_buckets[0][bucket]; provider;
|
||||
provider = provider->bucket_next) {
|
||||
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) provider->pairs[i].other_identity = 0;
|
||||
static int pair_cache_lookup(const struct decoded_cache_entry* first,
|
||||
const struct decoded_cache_entry* second, int* result) {
|
||||
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) {
|
||||
if (first->pairs[i].other_identity == second->identity) {
|
||||
*result = first->pairs[i].result;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -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;
|
||||
entry = entry->bucket_next) {
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
size_t len = strlen(meta->str);
|
||||
|
||||
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) {
|
||||
_free(decoded.hashes);
|
||||
return -1;
|
||||
}
|
||||
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);
|
||||
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));
|
||||
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->len = len;
|
||||
entry->len = meta->len;
|
||||
entry->count = decoded.count;
|
||||
entry->fingerprint = fingerprint;
|
||||
if (decoded_cache_next_identity == 0) decoded_cache_reset_pair_identities();
|
||||
entry->identity = decoded_cache_next_identity++;
|
||||
if (entry->identity == 0) entry->identity = decoded_cache_next_identity++;
|
||||
entry->fingerprint = fingerprint;
|
||||
entry->bucket = bucket;
|
||||
entry->target_bpp = target_bpp;
|
||||
|
||||
@@ -912,31 +933,29 @@ static int sorted_subset(const unsigned* small, size_t small_count, const unsign
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int rpmsetcmp_locked(const char* str1, const char* str2) {
|
||||
int rpmsetcmp(const char* str1, const char* str2) {
|
||||
struct set_meta meta1;
|
||||
if (set_meta_init(str1, &meta1) < 0) return -3;
|
||||
|
||||
struct set_meta meta2;
|
||||
int meta2_status = set_meta_init(str2, &meta2);
|
||||
unsigned target_bpp =
|
||||
meta2_status == 0 && meta2.bpp < meta1.bpp ? meta2.bpp : meta1.bpp;
|
||||
if (set_meta_init(str2, &meta2) < 0) return -4;
|
||||
unsigned target_bpp = meta2.bpp < meta1.bpp ? meta2.bpp : meta1.bpp;
|
||||
|
||||
const unsigned* hashes1;
|
||||
size_t count1;
|
||||
struct decoded_cache_entry* entry1;
|
||||
if (cache_decode_set(&meta1, target_bpp, 0, &hashes1, &count1, &entry1) < 0) return -3;
|
||||
if (meta2_status < 0) return -4;
|
||||
|
||||
if (meta1.len == meta2.len && memcmp(meta1.str, meta2.str, meta1.len + 1) == 0) return 0;
|
||||
|
||||
const unsigned* hashes2;
|
||||
size_t count2;
|
||||
struct decoded_cache_entry* entry2;
|
||||
if (cache_decode_set(&meta2, target_bpp, 1, &hashes2, &count2, &entry2) < 0) return -4;
|
||||
|
||||
for (unsigned i = 0; i < PAIR_CACHE_SIZE; ++i) {
|
||||
if (entry1->pairs[i].other_identity == entry2->identity) return entry1->pairs[i].result;
|
||||
}
|
||||
|
||||
int result;
|
||||
if (pair_cache_lookup(entry1, entry2, &result)) return result;
|
||||
|
||||
if (count1 == count2)
|
||||
result = memcmp(hashes1, hashes2, count1 * sizeof(*hashes1)) == 0 ? 0 : -2;
|
||||
else if (count1 > count2)
|
||||
@@ -944,16 +963,6 @@ static int rpmsetcmp_locked(const char* str1, const char* str2) {
|
||||
else
|
||||
result = sorted_subset(hashes1, count1, hashes2, count2) ? -1 : -2;
|
||||
|
||||
struct pair_cache_entry* pair = &entry1->pairs[entry1->pair_next++ % PAIR_CACHE_SIZE];
|
||||
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);
|
||||
pair_cache_store(entry1, entry2, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -546,37 +546,6 @@ static int decode_set(const struct set_meta* meta, unsigned* hash_arr) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARSV_SET9_EXPORT
|
||||
/* Test-only bridge used to translate existing repository metadata without
|
||||
* reconstructing unavailable symbol names. The caller owns *hashes. */
|
||||
int arsv_set9_decode(const char* source, unsigned** hashes, size_t* count, unsigned* bpp) {
|
||||
if (!source || !hashes || !count || !bpp) return -EINVAL;
|
||||
|
||||
const char* str = source;
|
||||
if (strncmp(str, "set:", 4) == 0) str += 4;
|
||||
|
||||
struct set_meta meta;
|
||||
int rc = set_meta_init(str, &meta);
|
||||
if (rc < 0) return rc;
|
||||
rc = set_meta_fini(&meta);
|
||||
if (rc < 0) return rc;
|
||||
|
||||
unsigned* values = malloc((size_t)meta.value_capacity * sizeof(*values));
|
||||
if (!values) return -ENOMEM;
|
||||
|
||||
int decoded = decode_set(&meta, values);
|
||||
if (decoded <= 0) {
|
||||
free(values);
|
||||
return decoded < 0 ? decoded : -EINVAL;
|
||||
}
|
||||
|
||||
*hashes = values;
|
||||
*count = (size_t)decoded;
|
||||
*bpp = (unsigned)meta.bpp;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Bounded decoded-set cache: bucketed lookup plus O(1) LRU updates. */
|
||||
static int downsample_set(const unsigned* hash_pt, size_t hash_cnt, unsigned* dest_pt,
|
||||
int target_bpp);
|
||||
|
||||
@@ -16,6 +16,7 @@ PERF_EVENTS='task-clock,context-switches,cpu-migrations,page-faults,minor-faults
|
||||
PACKAGER='krosh <gudovdo@my.msu.ru>'
|
||||
APT_SOURCE=/etc/apt/sources.list.d/alt.list
|
||||
APT_GET=/usr/lib/apt/apt-get
|
||||
TIME_COMMAND=/usr/bin/time
|
||||
RPM_BUILD_GIT=https://git.altlinux.org/gears/r/rpm-build.git
|
||||
RPM_GIT=https://git.altlinux.org/gears/r/rpm.git
|
||||
|
||||
@@ -93,7 +94,7 @@ operation_label()
|
||||
|
||||
run_once()
|
||||
{
|
||||
local operation=$1 variant=$2 run=$3 start end status perf_stat
|
||||
local operation=$1 variant=$2 run=$3 status perf_stat time_file
|
||||
local libdir="$variant/lib/usr/lib64"
|
||||
local root="$COMMON/root"
|
||||
local raw="$variant/raw"
|
||||
@@ -117,8 +118,9 @@ run_once()
|
||||
)
|
||||
fi
|
||||
|
||||
start=$(date +%s%N)
|
||||
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \
|
||||
time_file="$raw/$operation.$run.time.tsv"
|
||||
if "$TIME_COMMAND" -f $'%U\t%S' -o "$time_file" -- \
|
||||
env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \
|
||||
LD_LIBRARY_PATH="$libdir" \
|
||||
taskset -c "$CPU" "${command[@]}" \
|
||||
>"$raw/$operation.$run.stdout" \
|
||||
@@ -127,10 +129,7 @@ run_once()
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
end=$(date +%s%N)
|
||||
|
||||
RUN_TIME=$(awk -v start="$start" -v end="$end" \
|
||||
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
|
||||
read -r RUN_USER_TIME RUN_SYSTEM_TIME <"$time_file"
|
||||
RUN_STATUS=$status
|
||||
}
|
||||
|
||||
@@ -228,8 +227,8 @@ benchmark_variant()
|
||||
{
|
||||
local variant=$1 result=$2 debug_file=$3 debuginfo_rpm=$4
|
||||
local runtime_file=$5 dso_name=$6
|
||||
local operation run average label status_text perf_result perf_dir
|
||||
local -a times statuses
|
||||
local operation run average_user average_system label status_text perf_result perf_dir
|
||||
local -a user_times system_times statuses
|
||||
local -a operations=(
|
||||
check
|
||||
autoremove
|
||||
@@ -241,7 +240,7 @@ benchmark_variant()
|
||||
)
|
||||
|
||||
mkdir -p "$variant/raw/perf-stat"
|
||||
printf 'command\taverage_seconds\trun1_seconds\trun2_seconds\trun3_seconds\texit_status\n' \
|
||||
printf 'command\taverage_user_seconds\taverage_system_seconds\trun1_user_seconds\trun1_system_seconds\trun2_user_seconds\trun2_system_seconds\trun3_user_seconds\trun3_system_seconds\texit_status\n' \
|
||||
>"$result"
|
||||
perf_result=${result%.tsv}.perf-stat.tsv
|
||||
perf_dir=${result%.tsv}.perf
|
||||
@@ -257,22 +256,27 @@ benchmark_variant()
|
||||
fi
|
||||
|
||||
for operation in "${operations[@]}"; do
|
||||
times=()
|
||||
user_times=()
|
||||
system_times=()
|
||||
statuses=()
|
||||
|
||||
for ((run = 1; run <= RUNS; ++run)); do
|
||||
run_once "$operation" "$variant" "$run"
|
||||
times+=("$RUN_TIME")
|
||||
user_times+=("$RUN_USER_TIME")
|
||||
system_times+=("$RUN_SYSTEM_TIME")
|
||||
statuses+=("$RUN_STATUS")
|
||||
if ((COLLECT_PERF)); then
|
||||
append_perf_stat "$operation" "$run" \
|
||||
"$variant/raw/perf-stat/$operation.$run.tsv" "$perf_result"
|
||||
fi
|
||||
printf '%s: run %d/%d: %ss, status=%s\n' \
|
||||
"$operation" "$run" "$RUNS" "$RUN_TIME" "$RUN_STATUS"
|
||||
printf '%s: run %d/%d: user=%ss system=%ss status=%s\n' \
|
||||
"$operation" "$run" "$RUNS" \
|
||||
"$RUN_USER_TIME" "$RUN_SYSTEM_TIME" "$RUN_STATUS"
|
||||
done
|
||||
|
||||
average=$(printf '%s\n' "${times[@]}" | \
|
||||
average_user=$(printf '%s\n' "${user_times[@]}" | \
|
||||
awk '{ total += $1 } END { printf "%.6f", total / NR }')
|
||||
average_system=$(printf '%s\n' "${system_times[@]}" | \
|
||||
awk '{ total += $1 } END { printf "%.6f", total / NR }')
|
||||
|
||||
status_text=${statuses[0]}
|
||||
@@ -284,9 +288,11 @@ benchmark_variant()
|
||||
done
|
||||
|
||||
label=$(operation_label "$operation")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$label" "$average" \
|
||||
"${times[0]}" "${times[1]}" "${times[2]}" "$status_text" \
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$label" "$average_user" "$average_system" \
|
||||
"${user_times[0]}" "${system_times[0]}" \
|
||||
"${user_times[1]}" "${system_times[1]}" \
|
||||
"${user_times[2]}" "${system_times[2]}" "$status_text" \
|
||||
>>"$result"
|
||||
|
||||
if ((COLLECT_PERF && PERF_RECORD)); then
|
||||
@@ -300,6 +306,7 @@ for command in git gear-hsh hsh rpm rpmquery rpm2cpio cpio apt-get apt-cache \
|
||||
taskset awk sed date sha256sum ldd readelf readlink eu-unstrip; do
|
||||
command -v "$command" >/dev/null || fail "required command not found: $command"
|
||||
done
|
||||
[[ -x $TIME_COMMAND ]] || fail "time executable not found: $TIME_COMMAND"
|
||||
if ((COLLECT_PERF)); then
|
||||
command -v perf >/dev/null || \
|
||||
fail "perf is not installed; install the ALT package: apt-get install perf"
|
||||
|
||||
@@ -1,936 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
export LC_ALL=C
|
||||
|
||||
# A/B benchmark:
|
||||
# set9: librpm из Sisyphus с reimplement/set9.c и исходными pkglist Sisyphus;
|
||||
# d1: librpm из того же commit с direct_hash/hash_set.c и теми же pkglist,
|
||||
# заранее преобразованными run_sisyphus_pkglist.py в формат set:D1.
|
||||
#
|
||||
# Все измеряемые команды работают без сети. Конвертация, сборка и gencaches
|
||||
# выполняются до таймера. Порядок замеров в каждом раунде: set9/d1/d1/set9.
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
SET9_C=${SET9_C:-$REPO_ROOT/reimplement/set9.c}
|
||||
D1_C=${D1_C:-$REPO_ROOT/new_version/direct_hash/hash_set.c}
|
||||
PKGLIST_CONVERTER=${PKGLIST_CONVERTER:-$REPO_ROOT/new_version/direct_hash/apt_benchmark/run_sisyphus_pkglist.py}
|
||||
SET_REWRITER_C=$REPO_ROOT/new_version/direct_hash/apt_benchmark/rewrite_sisyphus_pkglist.c
|
||||
SET_COMPAT_H=$REPO_ROOT/scripts/rpmsetcmp/newset_compat.h
|
||||
ORCHESTRATOR=$(realpath -e -- "${BASH_SOURCE[0]}")
|
||||
|
||||
WORK_ROOT=${WORK_ROOT:-$HOME/sisyphus-set9-d1-bench}
|
||||
RESULT_DIR=${RESULT_DIR:-$WORK_ROOT/results}
|
||||
SISYPHUS_MIRROR=${SISYPHUS_MIRROR:-https://ftp.altlinux.org/pub/distributions/ALTLinux}
|
||||
RPM_GIT=${RPM_GIT:-https://git.altlinux.org/gears/r/rpm.git}
|
||||
RPM_BRANCH=${RPM_BRANCH:-sisyphus}
|
||||
PACKAGER=${PACKAGER:-krosh <gudovdo@my.msu.ru>}
|
||||
CPU=${CPU:-0}
|
||||
ROUNDS=${ROUNDS:-2}
|
||||
RESET_WORK=${RESET_WORK:-1} # 0 — продолжить подготовку/сборки, 1 — начать заново.
|
||||
OPERATIONS=${OPERATIONS:-unmet install-rpm-build install-openuds-server install-password-store}
|
||||
APT_GET=${APT_GET:-/usr/lib/apt/apt-get}
|
||||
APT_CACHE=${APT_CACHE:-$(command -v apt-cache 2>/dev/null || true)}
|
||||
|
||||
usage()
|
||||
{
|
||||
cat <<'EOF'
|
||||
Usage: scripts/run-sisyphus-set9-d1-bench.sh
|
||||
|
||||
The script has no positional arguments. Configuration is passed through env:
|
||||
CPU=2 ROUNDS=3 RESET_WORK=1 ./scripts/run-sisyphus-set9-d1-bench.sh
|
||||
RESET_WORK=0 OPERATIONS='unmet check' ./scripts/run-sisyphus-set9-d1-bench.sh
|
||||
|
||||
Main variables:
|
||||
WORK_ROOT, RESULT_DIR, SISYPHUS_MIRROR, RPM_GIT, RPM_BRANCH, PACKAGER,
|
||||
SET9_C, D1_C, PKGLIST_CONVERTER, CPU, ROUNDS, RESET_WORK, OPERATIONS.
|
||||
|
||||
RESET_WORK=1 removes WORK_ROOT after running hsh --cleanup-only for old hasher
|
||||
workdirs. Timed runs never update repositories and never install packages.
|
||||
Snapshot preparation downloads x86_64 and noarch metadata from SISYPHUS_MIRROR
|
||||
into an isolated APT directory under WORK_ROOT; host APT configuration is untouched.
|
||||
EOF
|
||||
}
|
||||
|
||||
fail()
|
||||
{
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
safe_remove_work_root()
|
||||
{
|
||||
local marker="$WORK_ROOT/.arsv-sisyphus-set-bench-root"
|
||||
|
||||
[[ -n $WORK_ROOT && $WORK_ROOT == /* && $WORK_ROOT != / &&
|
||||
$WORK_ROOT != "$HOME_REAL" && $HOME_REAL != "$WORK_ROOT/"* ]] ||
|
||||
fail "unsafe WORK_ROOT for removal: $WORK_ROOT"
|
||||
[[ $WORK_ROOT != "$REPO_ROOT" && $REPO_ROOT != "$WORK_ROOT/"* &&
|
||||
$WORK_ROOT != "$REPO_ROOT/"* && $WORK_ROOT != "$CWD_REAL" &&
|
||||
$CWD_REAL != "$WORK_ROOT/"* && $WORK_ROOT != "$CWD_REAL/"* ]] ||
|
||||
fail "WORK_ROOT overlaps the source repository or current directory: $WORK_ROOT"
|
||||
[[ ! -e $WORK_ROOT || (-f $marker && ! -L $marker) ]] ||
|
||||
fail "refusing to remove unmarked WORK_ROOT: $WORK_ROOT"
|
||||
if [[ -f $marker ]]; then
|
||||
grep -Fx 'ARSV Sisyphus set9/D1 benchmark work root' "$marker" >/dev/null ||
|
||||
fail "invalid WORK_ROOT ownership marker: $marker"
|
||||
fi
|
||||
}
|
||||
|
||||
canonical_work_subdir()
|
||||
{
|
||||
local path=$1 label=$2 resolved
|
||||
[[ ! -L $path ]] || fail "$label must not be a symlink: $path"
|
||||
resolved=$(realpath -e -- "$path") || fail "$label does not resolve: $path"
|
||||
[[ $resolved == "$WORK_ROOT/"* ]] ||
|
||||
fail "$label resolves outside WORK_ROOT: $path -> $resolved"
|
||||
printf '%s' "$resolved"
|
||||
}
|
||||
|
||||
ensure_work_subdir()
|
||||
{
|
||||
local path=$1 label=$2 resolved
|
||||
[[ ! -L $path ]] || fail "$label must not be a symlink: $path"
|
||||
resolved=$(realpath -m -- "$path")
|
||||
[[ $resolved == "$WORK_ROOT/"* ]] ||
|
||||
fail "$label resolves outside WORK_ROOT: $path -> $resolved"
|
||||
mkdir -p -- "$path"
|
||||
canonical_work_subdir "$path" "$label" >/dev/null
|
||||
}
|
||||
|
||||
write_snapshot_fingerprint()
|
||||
{
|
||||
{
|
||||
printf 'sisyphus_mirror=%s\n' "$SISYPHUS_MIRROR"
|
||||
printf 'orchestrator=%s\n' "$(sha256sum "$ORCHESTRATOR" | awk '{print $1}')"
|
||||
printf 'converter=%s\n' "$(sha256sum "$PKGLIST_CONVERTER" | awk '{print $1}')"
|
||||
printf 'set9=%s\n' "$(sha256sum "$SET9_C" | awk '{print $1}')"
|
||||
printf 'd1=%s\n' "$(sha256sum "$D1_C" | awk '{print $1}')"
|
||||
printf 'rewrite=%s\n' "$(sha256sum "$REPO_ROOT/new_version/direct_hash/apt_benchmark/rewrite_sisyphus_pkglist.c" | awk '{print $1}')"
|
||||
printf 'compat=%s\n' "$(sha256sum "$REPO_ROOT/scripts/rpmsetcmp/newset_compat.h" | awk '{print $1}')"
|
||||
[[ ! -f $SNAPSHOT/devel-input-fingerprint.txt ]] ||
|
||||
cat "$SNAPSHOT/devel-input-fingerprint.txt"
|
||||
write_runtime_fingerprint
|
||||
}
|
||||
}
|
||||
|
||||
write_runtime_fingerprint()
|
||||
{
|
||||
local rpm_libdir library resolved owner
|
||||
rpm_libdir=$(rpm --eval '%{_libdir}')
|
||||
for library in librpm.so.7 librpmio.so.7; do
|
||||
resolved=$(realpath -e "$rpm_libdir/$library") ||
|
||||
fail "installed $library not found"
|
||||
owner=$(rpm -qf --qf '%{NAME}|%{SOURCERPM}|%{DISTTAG}' "$resolved") ||
|
||||
fail "cannot identify package owning $resolved"
|
||||
printf 'runtime_%s_owner=%s\n' "$library" "$owner"
|
||||
printf 'runtime_%s_sha256=%s\n' "$library" \
|
||||
"$(sha256sum "$resolved" | awk '{print $1}')"
|
||||
done
|
||||
}
|
||||
|
||||
validate_snapshot_reuse()
|
||||
{
|
||||
local current
|
||||
[[ -f $SNAPSHOT/.complete && -f $SNAPSHOT/input-fingerprint.txt &&
|
||||
-f $SNAPSHOT/devel-input-fingerprint.txt ]] || return 1
|
||||
current=$(mktemp)
|
||||
write_snapshot_fingerprint >"$current"
|
||||
if ! cmp -s "$current" "$SNAPSHOT/input-fingerprint.txt"; then
|
||||
rm -f "$current"
|
||||
fail 'snapshot inputs changed; use RESET_WORK=1'
|
||||
fi
|
||||
rm -f "$current"
|
||||
return 0
|
||||
}
|
||||
|
||||
write_source_fingerprint()
|
||||
{
|
||||
{
|
||||
printf 'rpm_git=%s\n' "$RPM_GIT"
|
||||
printf 'rpm_branch=%s\n' "$RPM_BRANCH"
|
||||
printf 'rpm_commit=%s\n' "$(git -C "$SOURCE_BASE" rev-parse HEAD)"
|
||||
}
|
||||
}
|
||||
|
||||
validate_source_reuse()
|
||||
{
|
||||
local current
|
||||
[[ -f $SOURCE_FINGERPRINT ]] ||
|
||||
fail 'source fingerprint missing; use RESET_WORK=1'
|
||||
[[ -z $(git -C "$SOURCE_BASE" status --porcelain) ]] ||
|
||||
fail 'RPM base source has local changes; use RESET_WORK=1'
|
||||
current=$(mktemp)
|
||||
write_source_fingerprint >"$current"
|
||||
if ! cmp -s "$current" "$SOURCE_FINGERPRINT"; then
|
||||
rm -f "$current"
|
||||
fail 'RPM source URL, branch, or commit changed; use RESET_WORK=1'
|
||||
fi
|
||||
rm -f "$current"
|
||||
}
|
||||
|
||||
prepare_spec()
|
||||
{
|
||||
local spec=$1 suffix=$2 version release new_release date
|
||||
|
||||
version=$(sed -n 's/^Version:[[:space:]]*//p' "$spec" | sed -n '1p')
|
||||
release=$(sed -n 's/^Release:[[:space:]]*//p' "$spec" | sed -n '1p')
|
||||
[[ -n $version && -n $release ]] || fail "cannot read Version/Release from $spec"
|
||||
|
||||
new_release="$release.$suffix"
|
||||
sed -i "0,/^Release:[[:space:]]*$release$/s//Release: $new_release/" "$spec"
|
||||
|
||||
date=$(date '+%a %b %d %Y')
|
||||
sed -i "/^%changelog/a\\
|
||||
* $date $PACKAGER $version-$new_release\\
|
||||
- Local Sisyphus set format benchmark build.\\
|
||||
" "$spec"
|
||||
}
|
||||
|
||||
prepare_d1_build_corpus()
|
||||
{
|
||||
local source=$1 spec helper_dir
|
||||
spec=$source/alt/rpm.spec
|
||||
helper_dir=$source/alt/arsv-d1-build
|
||||
mkdir -p "$helper_dir"
|
||||
cp "$SET9_C" "$helper_dir/set9.c"
|
||||
cp "$SET_REWRITER_C" "$helper_dir/rewrite_sisyphus_pkglist.c"
|
||||
cp "$SET_COMPAT_H" "$helper_dir/newset_compat.h"
|
||||
|
||||
python3 - "$spec" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
spec = Path(sys.argv[1])
|
||||
text = spec.read_text()
|
||||
needle = "join -o 1.3,2.3 P R |shuf >setcmp-data\n"
|
||||
replacement = needle + r'''# The buildroot RPM database contains legacy set9 values. This D1 build
|
||||
# preserves the same relation corpus, but converts both operands before the
|
||||
# format-specific setcmp/profile checks instead of feeding incompatible input.
|
||||
mkdir -p arsv-d1-compat
|
||||
touch arsv-d1-compat/rpmlib.h arsv-d1-compat/system.h arsv-d1-compat/set.h
|
||||
%__cc %optflags -std=gnu11 -Wall -Wextra -Werror -D_GNU_SOURCE -DARSV_SET9_EXPORT \
|
||||
-I arsv-d1-compat -include alt/arsv-d1-build/newset_compat.h \
|
||||
alt/arsv-d1-build/set9.c alt/arsv-d1-build/rewrite_sisyphus_pkglist.c \
|
||||
-o arsv-convert-set
|
||||
while read -r set1 set2; do
|
||||
d1_set1=$(./arsv-convert-set --convert-set "$set1")
|
||||
d1_set2=$(./arsv-convert-set --convert-set "$set2")
|
||||
printf '%%s %%s\n' "$d1_set1" "$d1_set2"
|
||||
done <setcmp-data >setcmp-data.d1
|
||||
test "$(wc -l <setcmp-data.d1)" -eq "$(wc -l <setcmp-data)"
|
||||
mv setcmp-data.d1 setcmp-data
|
||||
'''
|
||||
if text.count(needle) != 1:
|
||||
raise SystemExit(f"expected exactly one setcmp corpus creation in {spec}")
|
||||
spec.write_text(text.replace(needle, replacement))
|
||||
PY
|
||||
git -C "$source" add alt/arsv-d1-build
|
||||
}
|
||||
|
||||
apt_options()
|
||||
{
|
||||
local variant=$1
|
||||
APT_OPTIONS=(
|
||||
-o 'Debug::NoLocking=true'
|
||||
-o 'Dir::Etc::main=-'
|
||||
-o 'Dir::Etc::parts=-'
|
||||
-o "Dir::Etc::sourcelist=$SNAPSHOT/etc-apt/sources.list"
|
||||
-o "Dir::Etc::sourceparts=$SNAPSHOT/etc-apt/sources.list.d"
|
||||
-o 'Dir::Etc::preferences=-'
|
||||
-o 'Dir::Etc::preferencesparts=-'
|
||||
-o "Dir::State::lists=$variant/apt/lists/"
|
||||
-o "Dir::State::status=$COMMON/status"
|
||||
-o "Dir::Cache=$variant/apt/cache/"
|
||||
-o "Dir::Cache::archives=$variant/apt/cache/archives"
|
||||
-o "Dir::Cache::pkgcache=$variant/apt/cache/pkgcache.bin"
|
||||
-o "Dir::Cache::srcpkgcache=$variant/apt/cache/srcpkgcache.bin"
|
||||
-o "RPM::RootDir=$COMMON/root"
|
||||
)
|
||||
}
|
||||
|
||||
operation_command()
|
||||
{
|
||||
local operation=$1 variant=$2
|
||||
apt_options "$variant"
|
||||
|
||||
case $operation in
|
||||
unmet)
|
||||
COMMAND=("$APT_CACHE" -q "${APT_OPTIONS[@]}" unmet)
|
||||
;;
|
||||
check)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s check)
|
||||
;;
|
||||
autoremove)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s autoremove)
|
||||
;;
|
||||
install-rpm-build)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s install rpm-build)
|
||||
;;
|
||||
install-openuds-server)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s install openuds-server)
|
||||
;;
|
||||
install-password-store)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s install password-store)
|
||||
;;
|
||||
upgrade)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -o APT::Get::EnableUpgrade=true -s upgrade)
|
||||
;;
|
||||
dist-upgrade)
|
||||
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s dist-upgrade)
|
||||
;;
|
||||
*)
|
||||
fail "unknown operation: $operation"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
variant_dir()
|
||||
{
|
||||
case $1 in
|
||||
set9) printf '%s' "$SET9_VARIANT" ;;
|
||||
d1) printf '%s' "$D1_VARIANT" ;;
|
||||
*) fail "unknown variant: $1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
variant_libdir()
|
||||
{
|
||||
local variant
|
||||
variant=$(variant_dir "$1")
|
||||
printf '%s' "$variant/lib/usr/lib64"
|
||||
}
|
||||
|
||||
prepare_command()
|
||||
{
|
||||
local operation=$1 variant_name=$2 variant
|
||||
variant=$(variant_dir "$variant_name")
|
||||
RUN_LIBDIR=$(variant_libdir "$variant_name")
|
||||
operation_command "$operation" "$variant"
|
||||
}
|
||||
|
||||
execute_command()
|
||||
{
|
||||
local stdout=$1 stderr=$2 status
|
||||
|
||||
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" LD_LIBRARY_PATH="$RUN_LIBDIR" \
|
||||
taskset -c "$CPU" "${COMMAND[@]}" >"$stdout" 2>"$stderr"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
printf '%s' "$status"
|
||||
}
|
||||
|
||||
run_once()
|
||||
{
|
||||
local operation=$1 variant_name=$2 sequence=$3 sample=$4
|
||||
local variant raw stdout stderr start end status elapsed
|
||||
local stdout_sha stderr_sha stdout_bytes stderr_bytes
|
||||
variant=$(variant_dir "$variant_name")
|
||||
raw="$RESULT_DIR/raw/$operation/$variant_name"
|
||||
mkdir -p "$raw"
|
||||
stdout="$raw/$sample.stdout"
|
||||
stderr="$raw/$sample.stderr"
|
||||
|
||||
prepare_command "$operation" "$variant_name"
|
||||
start=$(date +%s%N)
|
||||
status=$(execute_command "$stdout" "$stderr")
|
||||
end=$(date +%s%N)
|
||||
elapsed=$(awk -v start="$start" -v end="$end" \
|
||||
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
|
||||
|
||||
stdout_sha=$(sha256sum "$stdout" | awk '{print $1}')
|
||||
stderr_sha=$(sha256sum "$stderr" | awk '{print $1}')
|
||||
stdout_bytes=$(stat -c %s "$stdout")
|
||||
stderr_bytes=$(stat -c %s "$stderr")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$operation" "$sequence" "$variant_name" "$sample" "$elapsed" "$status" \
|
||||
"$stdout_sha" "$stdout_bytes" "$stderr_sha" "$stderr_bytes" >>"$RAW_RESULTS"
|
||||
printf '%-28s sequence=%-2s %-4s sample=%-2s %ss status=%s\n' \
|
||||
"$operation" "$sequence" "$variant_name" "$sample" "$elapsed" "$status"
|
||||
}
|
||||
|
||||
prepare_snapshot()
|
||||
{
|
||||
local converter_output devel_cache devel_root rpm_libdir rpm_library rpmio_library
|
||||
local devel_source runtime_source rpmfile
|
||||
local -a update_options
|
||||
local -a rpm_devel_rpms popt_devel_rpms
|
||||
validate_snapshot_reuse && return
|
||||
|
||||
printf '\n===== Preparing one immutable Sisyphus metadata snapshot =====\n'
|
||||
rm -rf "$SNAPSHOT"
|
||||
mkdir -p "$SNAPSHOT/lists/partial" \
|
||||
"$SNAPSHOT/etc-apt/sources.list.d" \
|
||||
"$SNAPSHOT/download-cache/archives/partial"
|
||||
printf '%s\n' \
|
||||
"rpm [alt] $SISYPHUS_MIRROR Sisyphus/x86_64 classic" \
|
||||
"rpm [alt] $SISYPHUS_MIRROR Sisyphus/noarch classic" \
|
||||
>"$SNAPSHOT/etc-apt/sources.list"
|
||||
|
||||
update_options=(
|
||||
-o 'Dir::Etc::main=-'
|
||||
-o 'Dir::Etc::parts=-'
|
||||
-o "Dir::Etc::sourcelist=$SNAPSHOT/etc-apt/sources.list"
|
||||
-o "Dir::Etc::sourceparts=$SNAPSHOT/etc-apt/sources.list.d"
|
||||
-o 'Dir::Etc::preferences=-'
|
||||
-o 'Dir::Etc::preferencesparts=-'
|
||||
-o "Dir::State::lists=$SNAPSHOT/lists"
|
||||
-o "Dir::State::status=$COMMON/status"
|
||||
-o "Dir::Cache=$SNAPSHOT/download-cache"
|
||||
-o "Dir::Cache::archives=$SNAPSHOT/download-cache/archives"
|
||||
-o "Dir::Cache::pkgcache=$SNAPSHOT/download-cache/pkgcache.bin"
|
||||
-o "Dir::Cache::srcpkgcache=$SNAPSHOT/download-cache/srcpkgcache.bin"
|
||||
-o "RPM::RootDir=$COMMON/root"
|
||||
)
|
||||
APT_CONFIG="$COMMON/apt.conf" "$APT_GET" -qq \
|
||||
"${update_options[@]}" update
|
||||
|
||||
# The converter needs C headers, but the benchmark host intentionally need
|
||||
# not have development packages installed. Download the p11 packages that
|
||||
# match the host's installed librpm into a private cache and extract only
|
||||
# their headers; no system package is installed or changed.
|
||||
devel_cache="$SNAPSHOT/devel-cache"
|
||||
devel_root="$SNAPSHOT/devel-root"
|
||||
mkdir -p "$devel_cache/archives/partial" "$devel_root"
|
||||
env -u APT_CONFIG "$APT_GET" -qq -y -d \
|
||||
-o "Dir::Cache=$devel_cache" \
|
||||
-o "Dir::Cache::archives=$devel_cache/archives" \
|
||||
-o "Dir::Cache::pkgcache=$devel_cache/pkgcache.bin" \
|
||||
-o "Dir::Cache::srcpkgcache=$devel_cache/srcpkgcache.bin" \
|
||||
install librpm-devel
|
||||
shopt -s nullglob
|
||||
rpm_devel_rpms=("$devel_cache"/archives/librpm-devel_*.rpm)
|
||||
popt_devel_rpms=("$devel_cache"/archives/libpopt-devel_*.rpm)
|
||||
shopt -u nullglob
|
||||
((${#rpm_devel_rpms[@]} == 1)) ||
|
||||
fail "expected one downloaded librpm-devel RPM, got ${#rpm_devel_rpms[@]}"
|
||||
((${#popt_devel_rpms[@]} == 1)) ||
|
||||
fail "expected one downloaded libpopt-devel RPM, got ${#popt_devel_rpms[@]}"
|
||||
devel_source=$(rpm -qp --qf '%{SOURCERPM}|%{DISTTAG}' "${rpm_devel_rpms[0]}") ||
|
||||
fail 'cannot identify downloaded librpm-devel source package'
|
||||
for rpmfile in "${rpm_devel_rpms[@]}" "${popt_devel_rpms[@]}"; do
|
||||
(cd "$devel_root" && rpm2cpio "$rpmfile" | cpio -idm --quiet './usr/include/*')
|
||||
done
|
||||
[[ -f $devel_root/usr/include/rpm/header.h && -f $devel_root/usr/include/popt.h ]] ||
|
||||
fail 'failed to extract RPM development headers'
|
||||
rpm_libdir=$(rpm --eval '%{_libdir}')
|
||||
rpm_library=$(realpath -e "$rpm_libdir/librpm.so.7") ||
|
||||
fail 'installed librpm.so.7 not found'
|
||||
rpmio_library=$(realpath -e "$rpm_libdir/librpmio.so.7") ||
|
||||
fail 'installed librpmio.so.7 not found'
|
||||
for rpmfile in "$rpm_library" "$rpmio_library"; do
|
||||
runtime_source=$(rpm -qf --qf '%{SOURCERPM}|%{DISTTAG}' "$rpmfile") ||
|
||||
fail "cannot identify runtime package owning $rpmfile"
|
||||
[[ $runtime_source == "$devel_source" ]] ||
|
||||
fail "downloaded librpm-devel ($devel_source) does not match $rpmfile ($runtime_source)"
|
||||
done
|
||||
{
|
||||
printf 'devel_librpm_identity=%s|%s\n' \
|
||||
"$(rpm -qp --qf '%{NAME}|%{EVR}|%{DISTTAG}|%{ARCH}' "${rpm_devel_rpms[0]}")" \
|
||||
"$devel_source"
|
||||
printf 'devel_librpm_sha256=%s\n' \
|
||||
"$(sha256sum "${rpm_devel_rpms[0]}" | awk '{print $1}')"
|
||||
printf 'devel_popt_identity=%s\n' \
|
||||
"$(rpm -qp --qf '%{NAME}|%{EVR}|%{DISTTAG}|%{ARCH}' "${popt_devel_rpms[0]}")"
|
||||
printf 'devel_popt_sha256=%s\n' \
|
||||
"$(sha256sum "${popt_devel_rpms[0]}" | awk '{print $1}')"
|
||||
} >"$SNAPSHOT/devel-input-fingerprint.txt"
|
||||
|
||||
converter_output="$SNAPSHOT/conversion"
|
||||
python3 "$PKGLIST_CONVERTER" "$converter_output" \
|
||||
--lists-dir "$SNAPSHOT/lists" \
|
||||
--rpm-include-dir "$devel_root/usr/include" \
|
||||
--rpm-library "$rpm_library" \
|
||||
--rpm-library "$rpmio_library"
|
||||
mv "$converter_output/d1-pkglists/manifest.json" "$SNAPSHOT/manifest.json"
|
||||
mkdir -p "$SNAPSHOT/d1-pkglists"
|
||||
mv "$converter_output/d1-pkglists/"*.classic "$SNAPSHOT/d1-pkglists/"
|
||||
rmdir "$converter_output/d1-pkglists" "$converter_output"
|
||||
rm -rf "$SNAPSHOT/download-cache"
|
||||
rm -rf "$devel_cache" "$devel_root"
|
||||
rm -f "$SNAPSHOT/lists/lock"
|
||||
rm -rf "$SNAPSHOT/lists/partial"
|
||||
mkdir -p "$SNAPSHOT/lists/partial"
|
||||
write_snapshot_fingerprint >"$SNAPSHOT/input-fingerprint.txt"
|
||||
: >"$SNAPSHOT/.complete"
|
||||
}
|
||||
|
||||
prepare_variant_lists()
|
||||
{
|
||||
local variant=$1 format=$2 variant_apt
|
||||
if [[ -d $variant/apt ]]; then
|
||||
variant_apt=$(canonical_work_subdir "$variant/apt" 'APT variant directory')
|
||||
chmod -R u+w "$variant_apt"
|
||||
fi
|
||||
rm -rf "$variant/apt"
|
||||
mkdir -p "$variant/apt/lists" "$variant/apt/cache/archives/partial"
|
||||
cp -a "$SNAPSHOT/lists/." "$variant/apt/lists/"
|
||||
|
||||
if [[ $format == d1 ]]; then
|
||||
python3 - "$SNAPSHOT/manifest.json" "$SNAPSHOT/lists" \
|
||||
"$SNAPSHOT/d1-pkglists" "$variant/apt/lists" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
manifest_path, original_dir, d1_dir, target_dir = map(Path, sys.argv[1:])
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
for architecture, data in manifest["architectures"].items():
|
||||
source = original_dir / data["source"]["path"]
|
||||
converted = d1_dir / data["output"]["path"]
|
||||
target = target_dir / data["source"]["path"]
|
||||
if hashlib.sha256(source.read_bytes()).hexdigest() != data["source"]["sha256"]:
|
||||
raise SystemExit(f"source checksum mismatch for {architecture}: {source}")
|
||||
if hashlib.sha256(converted.read_bytes()).hexdigest() != data["output"]["sha256"]:
|
||||
raise SystemExit(f"D1 checksum mismatch for {architecture}: {converted}")
|
||||
shutil.copyfile(converted, target)
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
build_variant()
|
||||
{
|
||||
local name=$1 source_c=$2 suffix=$3 variant source hasher repo spec
|
||||
local expected_fingerprint artifact_fingerprint
|
||||
local -a librpm_rpms
|
||||
variant=$(variant_dir "$name")
|
||||
source="$variant/src/rpm"
|
||||
hasher="$variant/hasher"
|
||||
repo="$hasher/repo/x86_64/RPMS.hasher"
|
||||
spec="$source/alt/rpm.spec"
|
||||
|
||||
printf '\n===== Building %s with %s =====\n' "$name" "$source_c"
|
||||
mkdir -p "$variant/src" "$variant/logs" "$hasher"
|
||||
shopt -s nullglob
|
||||
librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm)
|
||||
expected_fingerprint=$(mktemp)
|
||||
{
|
||||
printf 'base_commit=%s\n' "$(git -C "$SOURCE_BASE" rev-parse HEAD)"
|
||||
printf 'orchestrator=%s\n' "$(sha256sum "$ORCHESTRATOR" | awk '{print $1}')"
|
||||
printf 'set_source=%s\n' "$(sha256sum "$source_c" | awk '{print $1}')"
|
||||
printf 'suffix=%s\n' "$suffix"
|
||||
printf 'packager=%s\n' "$PACKAGER"
|
||||
if [[ $name == d1 ]]; then
|
||||
printf 'set9_decoder=%s\n' "$(sha256sum "$SET9_C" | awk '{print $1}')"
|
||||
printf 'set_rewriter=%s\n' "$(sha256sum "$SET_REWRITER_C" | awk '{print $1}')"
|
||||
printf 'set_compat=%s\n' "$(sha256sum "$SET_COMPAT_H" | awk '{print $1}')"
|
||||
fi
|
||||
} >"$expected_fingerprint"
|
||||
if [[ -f $variant/input-fingerprint.txt ]]; then
|
||||
if ! cmp -s "$expected_fingerprint" "$variant/input-fingerprint.txt"; then
|
||||
rm -f "$expected_fingerprint"
|
||||
fail "$name build inputs changed; use RESET_WORK=1"
|
||||
fi
|
||||
elif ((${#librpm_rpms[@]} > 0)); then
|
||||
rm -f "$expected_fingerprint"
|
||||
fail "$name RPM exists without its input fingerprint; use RESET_WORK=1"
|
||||
fi
|
||||
if ((${#librpm_rpms[@]} == 0)) && [[ -d $source ]]; then
|
||||
rm -rf "$source"
|
||||
fi
|
||||
if [[ ! -d $source/.git ]]; then
|
||||
git clone --local "$SOURCE_BASE" "$source"
|
||||
cp "$source_c" "$source/lib/set.c"
|
||||
prepare_spec "$spec" "$suffix"
|
||||
if [[ $name == d1 ]]; then
|
||||
prepare_d1_build_corpus "$source"
|
||||
fi
|
||||
sha256sum "$spec" >"$variant/prepared-spec.sha256"
|
||||
cp "$expected_fingerprint" "$variant/input-fingerprint.txt"
|
||||
else
|
||||
[[ -f $variant/prepared-spec.sha256 ]] &&
|
||||
sha256sum -c "$variant/prepared-spec.sha256" >/dev/null ||
|
||||
fail "$name prepared RPM spec changed; use RESET_WORK=1"
|
||||
cmp -s "$source_c" "$source/lib/set.c" ||
|
||||
fail "$source_c changed; use RESET_WORK=1"
|
||||
if [[ $name == d1 ]]; then
|
||||
cmp -s "$SET9_C" "$source/alt/arsv-d1-build/set9.c" &&
|
||||
cmp -s "$SET_REWRITER_C" \
|
||||
"$source/alt/arsv-d1-build/rewrite_sisyphus_pkglist.c" &&
|
||||
cmp -s "$SET_COMPAT_H" "$source/alt/arsv-d1-build/newset_compat.h" ||
|
||||
fail 'D1 build corpus converter changed; use RESET_WORK=1'
|
||||
fi
|
||||
fi
|
||||
rm -f "$expected_fingerprint"
|
||||
|
||||
librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm)
|
||||
if ((${#librpm_rpms[@]} == 0)); then
|
||||
(
|
||||
cd "$source"
|
||||
gear-hsh \
|
||||
--commit \
|
||||
--with-stuff \
|
||||
--packager="$PACKAGER" \
|
||||
--no-sisyphus-check=changelog,packager,gpg \
|
||||
--mountpoints=/proc \
|
||||
--workdir="$hasher" \
|
||||
--target=x86_64
|
||||
) 2>&1 | tee "$variant/logs/rpm.log"
|
||||
else
|
||||
printf 'already built: %s\n' "${librpm_rpms[0]}"
|
||||
fi
|
||||
|
||||
librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm)
|
||||
((${#librpm_rpms[@]} == 1)) ||
|
||||
fail "expected one librpm7 package for $name, got ${#librpm_rpms[@]}"
|
||||
artifact_fingerprint=$(sha256sum "${librpm_rpms[0]}")
|
||||
if [[ -f $variant/librpm-artifact.sha256 ]]; then
|
||||
[[ $artifact_fingerprint == "$(<"$variant/librpm-artifact.sha256")" ]] ||
|
||||
fail "$name librpm artifact changed; use RESET_WORK=1"
|
||||
else
|
||||
printf '%s\n' "$artifact_fingerprint" >"$variant/librpm-artifact.sha256"
|
||||
fi
|
||||
|
||||
rm -rf "$variant/lib"
|
||||
mkdir -p "$variant/lib"
|
||||
(
|
||||
cd "$variant/lib"
|
||||
rpm2cpio "${librpm_rpms[0]}" | cpio -idm --quiet
|
||||
)
|
||||
[[ -e $variant/lib/usr/lib64/librpm.so.7 && -e $variant/lib/usr/lib64/librpmio.so.7 ]] ||
|
||||
fail "librpm libraries were not extracted for $name"
|
||||
for apt_binary in "$APT_GET" "$APT_CACHE"; do
|
||||
LD_LIBRARY_PATH="$variant/lib/usr/lib64" ldd "$apt_binary" |
|
||||
awk -v expected="$variant/lib/usr/lib64/librpm.so.7" '
|
||||
index($0, expected) { found = 1 }
|
||||
END { exit found ? 0 : 1 }
|
||||
' || fail "$apt_binary does not load the built librpm for $name"
|
||||
done
|
||||
}
|
||||
|
||||
build_cache()
|
||||
{
|
||||
local name=$1 variant libdir start end elapsed stdout stderr status
|
||||
variant=$(variant_dir "$name")
|
||||
libdir=$(variant_libdir "$name")
|
||||
apt_options "$variant"
|
||||
stdout="$variant/logs/gencaches.stdout"
|
||||
stderr="$variant/logs/gencaches.stderr"
|
||||
|
||||
chmod -R u+w "$variant/apt/cache"
|
||||
rm -f "$variant/apt/cache/pkgcache.bin" "$variant/apt/cache/srcpkgcache.bin"
|
||||
start=$(date +%s%N)
|
||||
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" LD_LIBRARY_PATH="$libdir" \
|
||||
"$APT_CACHE" -q "${APT_OPTIONS[@]}" gencaches >"$stdout" 2>"$stderr"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
end=$(date +%s%N)
|
||||
elapsed=$(awk -v start="$start" -v end="$end" \
|
||||
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
|
||||
printf '%s\t%s\t%s\n' "$name" "$elapsed" "$status" >>"$CACHE_RESULTS"
|
||||
[[ $status -eq 0 && -s $variant/apt/cache/pkgcache.bin ]] ||
|
||||
fail "gencaches failed for $name; see $stderr"
|
||||
chmod -R a-w "$variant/apt/cache"
|
||||
}
|
||||
|
||||
record_cache_checksums()
|
||||
{
|
||||
local name variant
|
||||
: >"$RESULT_DIR/cache-files.before.txt"
|
||||
for name in set9 d1; do
|
||||
variant=$(variant_dir "$name")
|
||||
for cache_file in "$variant/apt/cache/pkgcache.bin" \
|
||||
"$variant/apt/cache/srcpkgcache.bin"; do
|
||||
stat -c '%n\t%D\t%i\t%s\t%Y\t%A' "$cache_file"
|
||||
sha256sum "$cache_file"
|
||||
done >>"$RESULT_DIR/cache-files.before.txt"
|
||||
done
|
||||
}
|
||||
|
||||
verify_cache_checksums()
|
||||
{
|
||||
local name variant
|
||||
: >"$RESULT_DIR/cache-files.after.txt"
|
||||
for name in set9 d1; do
|
||||
variant=$(variant_dir "$name")
|
||||
for cache_file in "$variant/apt/cache/pkgcache.bin" \
|
||||
"$variant/apt/cache/srcpkgcache.bin"; do
|
||||
stat -c '%n\t%D\t%i\t%s\t%Y\t%A' "$cache_file"
|
||||
sha256sum "$cache_file"
|
||||
done >>"$RESULT_DIR/cache-files.after.txt"
|
||||
done
|
||||
cmp -s "$RESULT_DIR/cache-files.before.txt" \
|
||||
"$RESULT_DIR/cache-files.after.txt" ||
|
||||
fail "APT cache changed during timed runs"
|
||||
}
|
||||
|
||||
write_provenance()
|
||||
{
|
||||
{
|
||||
printf 'rpm_git=%s\n' "$RPM_GIT"
|
||||
printf 'rpm_branch=%s\n' "$RPM_BRANCH"
|
||||
printf 'rpm_commit=%s\n' "$(git -C "$SOURCE_BASE" rev-parse HEAD)"
|
||||
printf 'sisyphus_mirror=%s\n' "$SISYPHUS_MIRROR"
|
||||
printf 'apt=%s\n' "$(rpmquery --qf '%{VERSION}-%{RELEASE}' apt)"
|
||||
printf 'rpm=%s\n' "$(rpm --version)"
|
||||
printf 'cpu=%s\n' "$CPU"
|
||||
printf 'rounds=%s\n' "$ROUNDS"
|
||||
printf 'operations=%s\n' "$OPERATIONS"
|
||||
sha256sum "$ORCHESTRATOR" "$SET9_C" "$D1_C" "$PKGLIST_CONVERTER" \
|
||||
"$REPO_ROOT/new_version/direct_hash/apt_benchmark/rewrite_sisyphus_pkglist.c" \
|
||||
"$REPO_ROOT/scripts/rpmsetcmp/newset_compat.h"
|
||||
cat "$SNAPSHOT/devel-input-fingerprint.txt"
|
||||
write_runtime_fingerprint
|
||||
printf 'set9_librpm='; cat "$SET9_VARIANT/librpm-artifact.sha256"
|
||||
printf 'd1_librpm='; cat "$D1_VARIANT/librpm-artifact.sha256"
|
||||
} >"$RESULT_DIR/provenance.txt"
|
||||
cp "$SNAPSHOT/manifest.json" "$RESULT_DIR/pkglist-manifest.json"
|
||||
}
|
||||
|
||||
summarize_results()
|
||||
{
|
||||
python3 - "$RAW_RESULTS" "$RESULT_DIR/summary.tsv" "$RESULT_DIR/summary.md" <<'PY'
|
||||
import csv
|
||||
import statistics
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
raw_path, tsv_path, markdown_path = map(Path, sys.argv[1:])
|
||||
with raw_path.open(newline="") as stream:
|
||||
rows = list(csv.DictReader(stream, delimiter="\t"))
|
||||
|
||||
groups = defaultdict(list)
|
||||
for row in rows:
|
||||
groups[row["operation"]].append(row)
|
||||
|
||||
summary = []
|
||||
all_equivalent = True
|
||||
for operation, items in groups.items():
|
||||
by_variant = defaultdict(list)
|
||||
signatures = set()
|
||||
statuses = set()
|
||||
for item in items:
|
||||
by_variant[item["variant"]].append(float(item["seconds"]))
|
||||
statuses.add(item["status"])
|
||||
signatures.add(
|
||||
(
|
||||
item["status"],
|
||||
item["stdout_sha256"],
|
||||
item["stdout_bytes"],
|
||||
item["stderr_sha256"],
|
||||
item["stderr_bytes"],
|
||||
)
|
||||
)
|
||||
if set(by_variant) != {"set9", "d1"}:
|
||||
raise SystemExit(f"missing variant samples for {operation}")
|
||||
if len(by_variant["set9"]) != len(by_variant["d1"]):
|
||||
raise SystemExit(f"unbalanced variant samples for {operation}")
|
||||
set9 = statistics.median(by_variant["set9"])
|
||||
d1 = statistics.median(by_variant["d1"])
|
||||
equivalent = len(signatures) == 1
|
||||
all_equivalent &= equivalent
|
||||
status = next(iter(statuses)) if len(statuses) == 1 else "mixed:" + ",".join(sorted(statuses))
|
||||
path = "normal" if status == "0" else "failure-path"
|
||||
summary.append(
|
||||
(
|
||||
operation,
|
||||
len(by_variant["set9"]),
|
||||
set9,
|
||||
min(by_variant["set9"]),
|
||||
max(by_variant["set9"]),
|
||||
d1,
|
||||
min(by_variant["d1"]),
|
||||
max(by_variant["d1"]),
|
||||
d1 / set9,
|
||||
status,
|
||||
path,
|
||||
equivalent,
|
||||
)
|
||||
)
|
||||
|
||||
with tsv_path.open("w", newline="") as stream:
|
||||
writer = csv.writer(stream, delimiter="\t", lineterminator="\n")
|
||||
writer.writerow(
|
||||
[
|
||||
"operation", "runs_per_variant", "set9_median_seconds", "set9_min_seconds",
|
||||
"set9_max_seconds", "d1_median_seconds", "d1_min_seconds", "d1_max_seconds",
|
||||
"d1/set9", "exit_status", "benchmark_path", "outputs_equal"
|
||||
]
|
||||
)
|
||||
for operation, count, set9, set9_min, set9_max, d1, d1_min, d1_max, ratio, status, path, equivalent in summary:
|
||||
writer.writerow(
|
||||
[
|
||||
operation, count, f"{set9:.6f}", f"{set9_min:.6f}", f"{set9_max:.6f}",
|
||||
f"{d1:.6f}", f"{d1_min:.6f}", f"{d1_max:.6f}", f"{ratio:.4f}",
|
||||
status, path, "yes" if equivalent else "NO"
|
||||
]
|
||||
)
|
||||
|
||||
lines = [
|
||||
"# Sisyphus set9 vs D1 benchmark",
|
||||
"",
|
||||
"Timed scope: resolver commands only; conversion, builds and `gencaches` are excluded.",
|
||||
"Each round uses the balanced order `set9 / d1 / d1 / set9` after one warm-up per variant.",
|
||||
"",
|
||||
"| operation | runs/variant | set9 median [min–max], s | D1 median [min–max], s | D1/set9 | status/path | output equivalence |",
|
||||
"|---|---:|---:|---:|---:|:---:|:---:|",
|
||||
]
|
||||
for operation, count, set9, set9_min, set9_max, d1, d1_min, d1_max, ratio, status, path, equivalent in summary:
|
||||
lines.append(
|
||||
f"| `{operation}` | {count} | {set9:.6f} [{set9_min:.6f}–{set9_max:.6f}] | "
|
||||
f"{d1:.6f} [{d1_min:.6f}–{d1_max:.6f}] | {ratio:.4f} | "
|
||||
f"{status}/{path} | {'yes' if equivalent else '**NO**'} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"`D1/set9 < 1` means that the D1 variant was faster.",
|
||||
"Rows with non-zero status are explicitly labelled `failure-path`; do not treat them as successful resolver workloads.",
|
||||
"Output equivalence includes exit status plus exact stdout and stderr hashes for every repeat and both variants.",
|
||||
]
|
||||
)
|
||||
markdown_path.write_text("\n".join(lines) + "\n")
|
||||
if not all_equivalent:
|
||||
raise SystemExit(2)
|
||||
PY
|
||||
}
|
||||
|
||||
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
(($# == 0)) || fail "unexpected arguments; use --help"
|
||||
|
||||
for command in git gear-hsh hsh rpm rpmquery rpm2cpio cpio taskset awk sed cc \
|
||||
pkglist-query date sha256sum stat ldd python3 cmp cp mv tee realpath mktemp grep; do
|
||||
command -v "$command" >/dev/null || fail "required command not found: $command"
|
||||
done
|
||||
[[ -n $APT_CACHE && -x $APT_CACHE ]] || fail "apt-cache not found: $APT_CACHE"
|
||||
[[ -x $APT_GET ]] || fail "apt-get executable not found: $APT_GET"
|
||||
[[ -f $SET9_C ]] || fail "set9 source not found: $SET9_C"
|
||||
[[ -f $D1_C ]] || fail "D1 source not found: $D1_C"
|
||||
[[ -f $PKGLIST_CONVERTER ]] || fail "pkglist converter not found: $PKGLIST_CONVERTER"
|
||||
[[ $ROUNDS =~ ^[1-9][0-9]*$ ]] || fail "ROUNDS must be a positive integer"
|
||||
[[ $RESET_WORK =~ ^[01]$ ]] || fail "RESET_WORK must be 0 or 1"
|
||||
taskset -c "$CPU" true >/dev/null 2>&1 || fail "CPU $CPU is unavailable to taskset"
|
||||
read -r -a OPERATION_LIST <<<"$OPERATIONS"
|
||||
((${#OPERATION_LIST[@]} > 0)) || fail "OPERATIONS is empty"
|
||||
for operation in "${OPERATION_LIST[@]}"; do
|
||||
case $operation in
|
||||
unmet|check|autoremove|install-rpm-build|install-openuds-server|install-password-store|upgrade|dist-upgrade) ;;
|
||||
*) fail "unknown operation in OPERATIONS: $operation" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
HOME_REAL=$(realpath -m "$HOME")
|
||||
CWD_REAL=$(realpath -m "$PWD")
|
||||
REPO_ROOT=$(realpath -m "$REPO_ROOT")
|
||||
SET9_C=$(realpath -m "$SET9_C")
|
||||
D1_C=$(realpath -m "$D1_C")
|
||||
PKGLIST_CONVERTER=$(realpath -m "$PKGLIST_CONVERTER")
|
||||
WORK_ROOT=$(realpath -m "$WORK_ROOT")
|
||||
RESULT_DIR=$(realpath -m "$RESULT_DIR")
|
||||
[[ $RESULT_DIR == "$WORK_ROOT"/* ]] ||
|
||||
fail "RESULT_DIR must be inside WORK_ROOT: $RESULT_DIR"
|
||||
|
||||
COMMON="$WORK_ROOT/common"
|
||||
SNAPSHOT="$COMMON/snapshot"
|
||||
SOURCE_BASE="$WORK_ROOT/src/rpm-base"
|
||||
SOURCE_FINGERPRINT="$WORK_ROOT/src/rpm-base.fingerprint.txt"
|
||||
SET9_VARIANT="$WORK_ROOT/variants/set9"
|
||||
D1_VARIANT="$WORK_ROOT/variants/d1"
|
||||
|
||||
WORK_ROOT_EXISTED=0
|
||||
[[ -e $WORK_ROOT ]] && WORK_ROOT_EXISTED=1
|
||||
if ((RESET_WORK)); then
|
||||
safe_remove_work_root
|
||||
for old_hasher in "$SET9_VARIANT/hasher" "$D1_VARIANT/hasher"; do
|
||||
[[ -d $old_hasher ]] || continue
|
||||
old_hasher=$(canonical_work_subdir "$old_hasher" 'hasher workdir')
|
||||
hsh --cleanup-only --workdir="$old_hasher" ||
|
||||
fail "hasher cleanup failed: $old_hasher"
|
||||
done
|
||||
for old_apt in "$SET9_VARIANT/apt" "$D1_VARIANT/apt"; do
|
||||
[[ -d $old_apt ]] || continue
|
||||
old_apt=$(canonical_work_subdir "$old_apt" 'APT workdir')
|
||||
chmod -R u+w "$old_apt"
|
||||
done
|
||||
rm -rf "$WORK_ROOT"
|
||||
WORK_ROOT_EXISTED=0
|
||||
elif ((WORK_ROOT_EXISTED)); then
|
||||
marker="$WORK_ROOT/.arsv-sisyphus-set-bench-root"
|
||||
[[ -f $marker && ! -L $marker ]] ||
|
||||
fail "refusing unmarked existing WORK_ROOT: $WORK_ROOT"
|
||||
grep -Fx 'ARSV Sisyphus set9/D1 benchmark work root' "$marker" >/dev/null ||
|
||||
fail "invalid WORK_ROOT ownership marker: $marker"
|
||||
fi
|
||||
mkdir -p "$WORK_ROOT"
|
||||
MARKER="$WORK_ROOT/.arsv-sisyphus-set-bench-root"
|
||||
if [[ -e $MARKER ]]; then
|
||||
grep -Fx 'ARSV Sisyphus set9/D1 benchmark work root' "$MARKER" >/dev/null ||
|
||||
fail "invalid WORK_ROOT ownership marker: $MARKER"
|
||||
else
|
||||
printf '%s\n' 'ARSV Sisyphus set9/D1 benchmark work root' >"$MARKER"
|
||||
fi
|
||||
ensure_work_subdir "$WORK_ROOT/src" 'source directory'
|
||||
ensure_work_subdir "$WORK_ROOT/variants" 'variants directory'
|
||||
ensure_work_subdir "$COMMON/root/var/lib/rpm" 'isolated RPM database directory'
|
||||
ensure_work_subdir "$RESULT_DIR" 'results directory'
|
||||
: >"$COMMON/apt.conf"
|
||||
: >"$COMMON/status"
|
||||
|
||||
prepare_snapshot
|
||||
if [[ ! -d $SOURCE_BASE/.git ]]; then
|
||||
git clone --branch "$RPM_BRANCH" --single-branch "$RPM_GIT" "$SOURCE_BASE"
|
||||
write_source_fingerprint >"$SOURCE_FINGERPRINT"
|
||||
else
|
||||
validate_source_reuse
|
||||
fi
|
||||
|
||||
build_variant set9 "$SET9_C" arsvset9
|
||||
build_variant d1 "$D1_C" arsvd1
|
||||
prepare_variant_lists "$SET9_VARIANT" set9
|
||||
prepare_variant_lists "$D1_VARIANT" d1
|
||||
|
||||
mkdir -p "$RESULT_DIR/raw"
|
||||
CACHE_RESULTS="$RESULT_DIR/cache-build.tsv"
|
||||
printf 'variant\tseconds\texit_status\n' >"$CACHE_RESULTS"
|
||||
build_cache set9
|
||||
build_cache d1
|
||||
record_cache_checksums
|
||||
write_provenance
|
||||
|
||||
RAW_RESULTS="$RESULT_DIR/raw.tsv"
|
||||
printf 'operation\tsequence\tvariant\tsample\tseconds\tstatus\tstdout_sha256\tstdout_bytes\tstderr_sha256\tstderr_bytes\n' >"$RAW_RESULTS"
|
||||
declare -A SAMPLE_COUNTS=([set9]=0 [d1]=0)
|
||||
sequence=0
|
||||
|
||||
for operation in "${OPERATION_LIST[@]}"; do
|
||||
printf '\n===== Warm-up: %s =====\n' "$operation"
|
||||
mkdir -p "$RESULT_DIR/warmup/$operation"
|
||||
for variant_name in set9 d1; do
|
||||
prepare_command "$operation" "$variant_name"
|
||||
warm_status=$(execute_command \
|
||||
"$RESULT_DIR/warmup/$operation/$variant_name.stdout" \
|
||||
"$RESULT_DIR/warmup/$operation/$variant_name.stderr")
|
||||
printf '%s\n' "$warm_status" >"$RESULT_DIR/warmup/$operation/$variant_name.status"
|
||||
done
|
||||
|
||||
printf '===== Timed ABBA: %s =====\n' "$operation"
|
||||
for ((round = 1; round <= ROUNDS; ++round)); do
|
||||
for variant_name in set9 d1 d1 set9; do
|
||||
sequence=$((sequence + 1))
|
||||
SAMPLE_COUNTS[$variant_name]=$((SAMPLE_COUNTS[$variant_name] + 1))
|
||||
run_once "$operation" "$variant_name" "$sequence" "${SAMPLE_COUNTS[$variant_name]}"
|
||||
done
|
||||
done
|
||||
SAMPLE_COUNTS[set9]=0
|
||||
SAMPLE_COUNTS[d1]=0
|
||||
done
|
||||
|
||||
verify_cache_checksums
|
||||
if ! summarize_results; then
|
||||
fail "variant/repeat outputs differ; inspect $RESULT_DIR/raw and $RESULT_DIR/summary.md"
|
||||
fi
|
||||
|
||||
printf '\nDone. Results:\n'
|
||||
printf ' %s\n' "$RESULT_DIR/summary.md" "$RESULT_DIR/summary.tsv" \
|
||||
"$RESULT_DIR/raw.tsv" "$RESULT_DIR/cache-build.tsv" "$RESULT_DIR/provenance.txt" \
|
||||
"$RESULT_DIR/pkglist-manifest.json"
|
||||