Дед Нечипор
Дед Нечипор личный блог
22 августа 2024, 09:45

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


Здравствуйте.
Требуется кто-то, кто может дать ссылку на статью или ключевые слова для поиска, или в общих чертах обрисовать, как математики конструируют хеш-функции (как подбирают последовательность операций), чтобы при невысоких вычислительных затратах получить разумное количество коллизий (безопасность хеша значение не имеет).
К примеру (взято с потолка): посчитать корреляции соответствующих битов каждой пары последовательных символов, корреляции при условии, что бит по смещению Х является 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 Комментариев
  • Auximen
    22 августа 2024, 09:59
    Спросите у ChatGPT 4o, на ваш запрос выдаёт несколько вариантов с кодом

    chatgpt.com/?model=gpt-4o
      • Auximen
        22 августа 2024, 11:24
        Дед Нечипор, фантазий на запросы технической направленности у ChatGPT не встречал, тем более, что такие вещи проверяются.
  • David Taylor
    22 августа 2024, 11:03
    Никак свою криптовалюту решил запустить)
  • Кирилл Гудков
    22 августа 2024, 12:24

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


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


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


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


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

      • Кирилл Гудков
        22 августа 2024, 14:43

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

         

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

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн