Хэш-функция — это функция, которая преобразует любые цифровые данные в выходную строку с фиксированным количеством символов. Мы говорим, что элемент с ключом
Рис. 1. Хеш функция. Иллюстрация взята из [1].
Коллизия - несколько ключей отображается в один и тот же индекс массива. В данном случае
При разрешении коллизий с помощью цепочек мы помещаем все элементы, хешированные в одну и ту же ячейку, в связанный список. (Рис. 2.) Ячейка j содержит указатель на заголовок списка всех элементов, хеш-значение ключа которых равно j; если таких элементов нет, ячейка содержит значение NULL.
Рис. 2. Метод цепочек. Иллюстрация взята из [1].
Каждая ячейка хеш-таблицы
Определим коэффициент заполнения
Задача работы оптимизировать хеш-таблицу с завышенным значением load factor. Будем использовать метод цепочек для решения коллизий. Для данной реализации лучшую производительность дает load factor в диапазоне
| Общее количество слов | |
| Уникальных слов |
Исходя из этих данных выберем размер таблицы простым числом 1483, для того чтобы load factor был равен 15.
1 загрузка
Оцениваем функции по визуально по графикам, а также посчитаем дисперсию высот столбиков по формуле
Рис. 3. Длина строки.
Рис. 4. Сумма ASCII кодов букв.
Murmur хеш. (Рис. 5.) Описание
Рис. 5. Murmur хеш.
Полиномиальный хеш. (Рис. 6.) Описание
Рис. 6. Полиномиальный хеш.
crc32. (Рис. 7.) Описание
Рис. 7. crc32.
| Хеш функция | ||
|---|---|---|
| 1 | Длина строки | 32925.14 |
| 2 | Сумма ASCII кодов букв | 252.06 |
| 3 | Murmur хеш | 16.03 |
| 4 | Полиномиальный | 15.63 |
| 5 | crc32 | 14.13 |
Остановимся на хеш-функции с наилучшими результатами - crc32.
Время выполнения измеряется всей программы в тиках процессора с помощью __rdtsc (). Определения горячих точек происходит с помощью профилировщика perf. Тесты проводились с включенным зарядным устройством, с закрытыми посторонними приложениями (включая браузеры). Перенос выполнения программы на одно ядро не производился. Perf предоставляет отчет на ядрах cpu_core и cpu_atom, берем статистику из cpu_core, поскольку на нем выполняется значительная часть программы.
- Fedora Linux 41 (Workstation Edition)
- Intel(R) Core(TM) Ultra 7 155H
- CPU average MHz: 4000
- Поддержка AVX, AVX2
Запуск программы происходил
Перед каждой таблицей с результатами расчетов предоставлены результаты тестов (исходные данные).
makeПроведем запуск начальной версии программы без каких-либо флагов оптимизаций.
Посмотреть результаты тестов (исходные данные)
| Номер измерения | Время, тики |
|---|---|
| 23466692641 | |
| 23077921583 | |
| 23471561823 | |
| 23402952157 | |
| 23307990888 | |
| 23410189821 | |
| 23840495091 | |
| 23368410816 | |
| 23700236068 | |
| 23052966716 | |
| 23126849844 | |
| 23137603997 | |
| 23880014460 | |
| 23036599355 | |
| 22909379699 | |
| 23414836954 | |
| 23090452944 | |
| 23712448393 | |
| 23495527095 | |
| 23173797711 |
|
|
|
|
|
|
|---|---|---|---|---|
Рис. 8. Запуск 0.
make CFLAGS="-O3"Проведем самую простейшую оптимизацию с флагом
Посмотреть результаты тестов (исходные данные)
| Номер измерения | Время, тики |
|---|---|
| 17453254923 | |
| 17735716232 | |
| 17495298507 | |
| 17398450056 | |
| 17311463366 | |
| 17442219420 | |
| 17557579741 | |
| 17630493839 | |
| 17343682360 | |
| 17350889529 | |
| 17601048369 | |
| 17681517527 | |
| 17683704188 | |
| 17713193522 | |
| 17439706662 | |
| 17438478128 | |
| 17645289967 | |
| 17372628168 | |
| 17611274579 | |
| 17084638759 |
|
|
|
|
|
|
|---|---|---|---|---|
Рис. 9. Запуск 1.
Применив только флаг оптимизации
make CFLAGS="-O3 -DOPT_STRCMP"По отчету профилировщика (Рис. 9.) горячей точкой является функция FindElement и вызываемая в ней strcmp, поэтому приступим к их оптимизации. Заменим strcmp на SIMD инструкции. Искомую строку будем загружать в __m256i только один раз перед циклом. Цикл проходится по списку. Для такой реализации необходимо чтобы каждый список был выровнен по 32-байтовой границе, иначе будет segmentation fault.
// исходный
for ()
{
if (!strcmp (list->data[i].str, value))
return list->data[i].n_repeat;
...
}
// оптимизированный
uint8_t buf[32] = {};
memcpy (buf, value, len);
__m256i vec = _mm256_loadu_si256 ((__m256i*) buf);
for ()
{
__m256i cmp = _mm256_cmpeq_epi32 (vec, list->data[i].avx);
int mask = _mm256_movemask_epi8 (cmp);
if (mask == 0xFFFFFFFF)
return list->data[i].n_repeat;
}
Посмотреть результаты тестов (исходные данные)
| Номер измерения | Время, тики |
|---|---|
| 8179294983 | |
| 8071579155 | |
| 8032195144 | |
| 8061217418 | |
| 8083556033 | |
| 8050124942 | |
| 8088366630 | |
| 8161757281 | |
| 8085109057 | |
| 8046539309 | |
| 8090590291 | |
| 8102750737 | |
| 8110495105 | |
| 8446868934 | |
| 8048978805 | |
| 7976661243 | |
| 8032484858 | |
| 7950303139 | |
| 7910392967 | |
| 8378886373 |
|
|
|
|
|
|
|---|---|---|---|---|
Рис. 10. Запуск 2.
Удалось увеличить скорость выполнения на
В отчете perf (Рис. 10.) видно что вызывается функция __memmove_avx_unaligned_erms, которая позволяет копировать данные несмотря на выравнивание. Попробуем копировать данные из невыровненного буфера в выровненный. Для этого выровняем буфер в который копируем, заменим функцию загрузки loadu -> load.
alignas (32) uint8_t buf[32] = {};
memcpy (buf, value, len);
__m256i vec = _mm256_load_si256 ((__m256i*) buf);
Рис. 11. Запуск 3.
Общее время выполнения не изменилось, поэтому нет смысла делать выравнивание в этом случае. Оставляем функцию _mm256_loadu_si256. Необходимо только выравнивание списков для хранения строк в формате __m256i.
Сделаем inline функции, на которую уходит больше половины времени выполнения функции Test. (Рис. 11.)
Рис. 12. Запуск 4.
Время выполнения не изменилось, поэтому не будем оставлять данную оптимизацию.
make CFLAGS="-O3 -DOPT_STRCMP -DOPT_CRC32" Следующая по очереди (Рис. 12.) функция для оптимизации - crc32. Используем ассемблерную вставку для оптимизации. Заменим нашу функцию crc32, работающую со статической таблицей, на ассемблерную инструкцию. Будем обрабатывать строки по
// исходный
while (buf[n])
{
crc = (crc << 8) ^ crc32_table[((crc >> 24) ^ (unsigned) buf[n]) & 255];
n++;
}
// оптимизированный
asm
(
".intel_syntax noprefix \n\t"
" mov eax, -1 \n\t"
" mov rsi, %[buf] \n\t"
" mov ecx, %[len] \n\t"
"1: cmp ecx, 4 \n\t"
" jb 3f \n\t"
" crc32 eax, dword ptr [rsi] \n\t"
" add rsi, 4 \n\t"
" sub ecx, 4 \n\t"
" jmp 1b \n\t"
"3: test ecx, ecx \n\t"
" jz 2f \n\t"
"4: crc32 eax, byte ptr [rsi] \n\t"
" inc rsi \n\t"
" dec rcx \n\t"
" jnz 4b \n\t"
"2: xor eax, -1 \n\t"
" mov %[crc], eax \n\t"
".att_syntax prefix \n\t"
: [crc] "=r" (crc)
: [buf] "r" (buf), [len] "r" (len)
: "rax", "rcx", "rsi", "cc", "memory"
);
Посмотреть результаты тестов (исходные данные)
| Номер измерения | Время, тики |
|---|---|
| 5860510679 | |
| 5728193338 | |
| 5704987946 | |
| 5714056019 | |
| 5711553174 | |
| 5854082933 | |
| 5815229203 | |
| 5866243115 | |
| 5863485018 | |
| 5793117547 | |
| 5851516154 | |
| 5860833565 | |
| 5886068439 | |
| 5759982821 | |
| 5834903810 | |
| 5855912834 | |
| 5874179433 | |
| 5793835793 | |
| 5755881136 | |
| 5823154499 |
|
|
|
|
|
|
|---|---|---|---|---|
Рис. 13. Запуск 5.
Удалось увеличить скорость выполнения на
Попробуем еще оптимизировать вычисление хеша. Такой версией, которая не содержит циклов.
uint8_t value[32] = {};
memcpy (value, buf, len);
crc = _mm_crc32_u64 (crc, *((uint64_t*) value));
crc = _mm_crc32_u64 (crc, *(((uint64_t*) value)+1));
crc = _mm_crc32_u64 (crc, *(((uint64_t*) value)+2));
crc = _mm_crc32_u64 (crc, *(((uint64_t*) value)+3));
Рис. 14. Запуск 6.
crc32HashFunc(char*, int):
push rbp
vpxor xmm0, xmm0, xmm0
movsx rdx, esi
mov rsi, rdi
mov rbp, rsp
and rsp, -32
sub rsp, 32
vmovdqa YMMWORD PTR [rsp], ymm0
mov rdi, rsp
vzeroupper
call memcpy
mov eax, -1
crc32 rax, QWORD PTR [rsp]
crc32 rax, QWORD PTR [rsp+8]
crc32 rax, QWORD PTR [rsp+16]
crc32 rax, QWORD PTR [rsp+24]
leave
ret
По отчету (Рис. 14.) видно, что почти все время уходит на функцию _mm_crc32_u64. Средняя длина слов -
Eще раз попробуем оптимизировать вычисление хеша без циклов ассемблерной вставкой. Сделано ради интереса сравнить два варианта одного кода.
uint8_t value[32] = {};
memcpy (value, buf, len % 31);
asm
(
".intel_syntax noprefix \n\t"
" mov rax, -1 \n\t"
" mov rsi, %[buf] \n\t"
" crc32 rax, qword ptr [rsi] \n\t"
" add rsi, 8 \n\t"
" crc32 rax, qword ptr [rsi] \n\t"
" add rsi, 8 \n\t"
" crc32 rax, qword ptr [rsi] \n\t"
" add rsi, 8 \n\t"
" crc32 rax, qword ptr [rsi] \n\t"
" xor rax, -1 \n\t"
" mov %[crc], rax \n\t"
".att_syntax prefix \n\t"
: [crc] "=r" (crc)
: [buf] "r" (value)
: "rax", "rsi", "cc", "memory"
);
Рис. 15. Запуск 7.
Результат такой же как и в
Перепишем функция полностью на ассемблере и слинкуем при сборке бинарников.
my_crc32:
push rsi
push rdi
mov eax, -1
.1: cmp edi, 4
jb .3
crc32 eax, dword [rsi]
add rsi, 4
sub edi, 4
jmp .1
.3: test edi, edi
jz .2
.4: crc32 eax, byte [rsi]
inc rsi
dec edi
jnz .4
.2: xor eax, -1
pop rdi
pop rsi
ret
Рис. 16. Запуск 8.
Результат получился такой же как и в
| Версия | Тики | Относительно 0 запуска |
|---|---|---|
| 0 (без оптимизаций) | 0% | |
| 1 (флаг -O3) | + 25.1% | |
| 2 (оптимизация FindElement и strcmp) | + 65.3% | |
| 5 (оптимизация crc32) | + 75.2% |
- Кормен Т.Х. и др. Алгоритмы: построение и анализ, 3-е изд. : Пер. с англ. — М. : ООО “И. Д. Вильямс”, 2013. — 1328 с. : ил. — Парал. тит. англ.
- Лоо А. Hash Function: [Электронный ресурс]. URL: https://corporatefinanceinstitute.com/resources/cryptocurrency/hash-function/ (Дата обращения 30.04.2025)
- perf: Linux profiling with performance counters: [Электронный ресурс]. URL: https://perfwiki.github.io/main/ (Дата обращения 30.04.2025)
- Хеш-функция MurmurHash: [Электронный ресурс]. URL: https://en.wikipedia.org/wiki/MurmurHash (Дата обращения 30.04.2025)
- Слотин С. Полиномиальное хеширование: [Электронный ресурс]. URL: https://ru.algorithmica.org/cs/hashing/polynomial/ (Дата обращения 30.04.2025)
- Исходники crc32.c: [Электронный ресурс]. URL: https://github.com/gcc-mirror/gcc/blob/master/libiberty/crc32.c (Дата обращения 30.04.2025)















