Блог им. AgentSmith

Нужна помощь математиков


Здравствуйте.
Требуется кто-то, кто может дать ссылку на статью или ключевые слова для поиска, или в общих чертах обрисовать, как математики конструируют хеш-функции (как подбирают последовательность операций), чтобы при невысоких вычислительных затратах получить разумное количество коллизий (безопасность хеша значение не имеет).
К примеру (взято с потолка): посчитать корреляции соответствующих битов каждой пары последовательных символов, корреляции при условии, что бит по смещению Х является 1, более сложные условные корреляции и по результатам что-то сварганить.
Или все намного банальнее, и конструкторы хеш-функций просто берут случайные операции в случайном порядке со случайно выбранными простыми числами, поднимают руки в молитвенном жесте богу рандома, произносят «ахалай-махалай» и просто надеются, что в этот раз точно получится что-то приемлемое?

Вопрос возник в связи со следующей ситуацией.
В программе есть словарь трансляции имен биржевых тикеров <Датафидовское имя> -> <Пользовательское имя>.
Ключевой момент: Словарь создается (заполняется) один раз и используется (поиск набора тикеров) только один раз за все время работы программы, так что реальной надобности в эффективности поиска в словаре нет, а цена создания словаря имеет не меньшее значение, чем цена перевода набора.
Но червь перфекционизма грызет не переставая «сделай оптимально! Ну сделай, тебе что, трудно?». Предполагаю переделать на таблицу, где хеш выполняет роль индекса ячейки, указывающей на начало очереди тикеров с общим хешем (набор коллизий).
Желаемый размер хеша — 10-13 битов. Подавляющее число хешируемых тикеров имеют длину 3-4 символа, которые можно ужать примерно до 16-21 битов, ограничив используемый алфавит. Задача в том, чтобы из этого количества (представленного подавляющим большинством тикеров) получить хеш длиной 13 бит с разумным распределением коллизий.


Исходные данные:
Словарь порядка 550 тикеров (будет увеличиваться, но не сильно, 1000+ в обозримом будущем не вижу)
Подавляющее количество тикеров (за редким исключением, так что правила не являются императивными):
— имеет длину 3-4 символа
— используются только заглавные буквы латинского алфавита и цифры
— первый символ либо "@" либо «Q»
— второй символ не цифра
— если среди символов есть цифра, то только одна и в пределах «1»-«5»

N — размер словаря
Q — размер набора для поиска в словаре
При создании словаря вносятся тикеры и для каждого из них проверяется, не является ли он дубликатом уже внесенного в словарь тикера: N вызовов поиска
При переводе происходит Q вызовов поиска.
Все.

В данный момент все реализовано банально просто: линейный последовательный поиск. В связи с экстремально дешевой операцией сравнения на равенство (всего: два считывания, три логические инструкции, одно сравнение, один условный переход (not taken)) и последовательным доступом к памяти достаточно эффективно для данного размера словаря.
При заполнении N*(N+1)/2 сравнений + при переводе (worst case) N*N — Q*(Q-1)/2. В самом запущенном случае, при Q=N, в общей сложенности получаем N*(N+1) сравнений за все время работы программы.

Хоть двоичный поиск и самый эффективный, польза его реализации в данном случае вызывает некоторые сомнения, его не рассматривал, так как интуитивно склоняюсь к далее рассматриваемому варианту.
При заполнении: N*Log2(N) сравнений на «больше»/«меньше» (чуть затратнее сравнения на равенство) + (не знаю как аналитически оценить количество и стоимость в «попугаях») N*<перемещений относительно больших блоков памяти> для вставки
+ При переводе: Q*Log2(N) сравнений на «больше»/«меньше»
При малых значениях словаря (как у меня), выигрыш кажется сомнительным, учитывая необходимость тасовать массив данных для сортировки.

И, наконец, предполагаемый вариант (таблица, для которой хеш играет роль индекса; защищенность хеша не играет роли):
При заполнении: N вычислений хеша + N выделений памяти для элементов списков коллизий (можно оптимизировать) + (упрощенно) N*<усредненное количество коллизий> сравнений на равенство
+При переводе: (упрощенно) Q*<усредненное количество коллизий> сравнений на равенство
+дополнительные расходы (чтение) на усложнившуюся адресацию

Оценить в «попугаях» эффективность алгоритмов (помимо практической реализации и замеров времени) не вижу возможности, так как накладные расходы, появляющиеся при реализации вариантов с двоичным поиском и хеш-таблицей вносят существенный вклад в суммарную цену.
Единственный вариант, при котором есть смысл потратить время во имя перфекционизма, вижу только при возможности конструирования очень дешевой хеш-функции заданной длины с очень демократичным распределением коллизий.

С Мурмуром3 и Сипом все понятно, но, возможно, есть методики как подойти к подбору операций для конструирования хеш-функции при известном составе словаря и особенностей (закономерностей) его элементов?
Естественно, если методика не предполагает штудирования талмудов по криптографии — задача явно не стОит траты существенных усилий и времени.

8 комментариев
Спросите у ChatGPT 4o, на ваш запрос выдаёт несколько вариантов с кодом

chatgpt.com/?model=gpt-4o
avatar
Auximen, благодарю, в принципе полезно. Но то, что он мне предложил, скорее ближе к «методу научного тыка» — ксорим, множим на степени простых чисел и молимся. А мне бы хотелось узнать, как это делают практики более осознанно, с учетом корреляций и частот, чтобы получить равномерное распределение коллизий. Если помучить чудо вражеской мысли поактивней, полагаю и в этом он мне может помочь. Вы не в курсе, у этого бесплатного публично открытого ЧатГПТ «коэффициент сказочности» выкручен в 0, или он все же будет фантазировать при выдаче ответов?
avatar
Дед Нечипор, фантазий на запросы технической направленности у ChatGPT не встречал, тем более, что такие вещи проверяются.
avatar
Никак свою криптовалюту решил запустить)
avatar

Трудно придумать приложение, где это бы реально имелo хоть какой-то смысл. Когда размеры хэш тейбла невелики, проще бороться с коллизиями увеличением размера самого тейбла, заполнение на 50-70% по учебникам на деле нафиг не уперлось. Память ничего не стоит, пока она влезает в L1D.


Когда нужна быстрая но приличная хэш функция для значения, влезающего в 32 бита, использую обычно такой шаблон:


const uint64_t MAGIC = 0x01000193;
size_t hash = value*MAGIC >> N;


N подбирается на типовом наборе хэшируемых значений по к-ву коллизий на заданном размере тейбла, обычно оно в диапазоне 10-20.


Это не то же самое, что хэш FNV, откуда константа позаимствована. Оно работает много быстрее FNV, но при разумном применении дает небольшое к-во коллизий.

avatar
Кирилл Гудков, спасибо, начинаю склоняться к тому, что простая  функция с терпимым качеством коллизий в моем случае буде лучше высокоинтеллектуальной, но медленной, которая может съесть весь прирост.
Тут до меня дошло, что на самом деле я ищу алгоритм сжатия с потерями, хочу повозиться с логикой наподобие интервального кодирования.
По поводу урезания 32-битного хеша до нужного — есть ли разница из какого участка брать? Я раньше половину брал из самых старших битов, половину — из самых младших, не знаю, было ли в этом преимущество перед вариантом как у Вас — брать все самые старшие
avatar

Дед Нечипор, на упомянутую мной формулу нужно наложить маску по размеру хэштейбла. Т.е. размер тейбла берется кратным степени двойки. Морочиться с делением на простое число не надо, это замедлит функцию, если число неизвестно в compile time.

 

Так что сдвиг N именно таки и определяет, с какого участка брать. После умножения на 0x1000193 распределение хаоса по битам будет подобно гауссиане, брать биты по краям — плохая идея.

avatar
Подтверждаю — вариант, предложенный Кириллом Гудковым, дает более чем отличный результат для моего набора данных.

Для оценки использовал коэффициент качества K=Ss/Sh,
где S — суммарное количество элементов словаря, которые нужно «посетить» и сравнить на равенство, при условии, что по одному разу будет искаться каждое слово, присутствующее в словаре (набор для поиска = содержимому словаря)

Ss (sequential) = N*(N+1)/2
Sh (hash) = сумма всех Mi*(Mi+1)/2, где Mi — количество элементов словаря, для которых значение хеша=i (i=[0;MaxHashValue]); граничные значения: N (best case); N*(N+1)/2 (worst case)
Теоретический максимум коэффициента качества — (N+1)/2, где N — размер словаря.
Грубо говоря, показывает, во сколько раз вариант с использованием хеш-таблицы быстрее варианта последовательного перебора по всему словарю (если не учитывать много важных факторов)

Как упоминал Кирилл, нужно подбирать, на какое количество бит нужно сместить результат умножения.
Если не производить предобработки строки перед хешированием, распределение в координатах «Y-качество»-«X-смещение» напоминает гауссову кривую с двумя пиками.
Если «ужать» строку, ограничив алфавит до «0»-«Z» — становится однопиковой с чуть более высоким уровнем вершины, но разница с вариантом без предобработки небольшая — порядка 3%

Протестировал на моем наборе:
Словарь — 311 тикеров опционов на фьючерсы
Размер хеша — 11 бит (хеш-таблица на 2048 элементов)
В результате: качество — 94.2% от теор. максимума без предобработки, 97.4% от теор. максимума с предобработкой

если урезать размер хеша до 9 бит (хеш-таблица на 512 элементов): качество — 80% от теор. максимума без предобработки
если урезать размер хеша до 10 бит (хеш-таблица на 1024 элементов): качество — 89,7% от теор. максимума без предобработки
avatar

теги блога Дед Нечипор

....все тэги



UPDONW
Новый дизайн