Какими простыми способами можно хэшировать 32-разрядное целое число (например, IP-адрес, например, Unix time_t и т. Д.) До 16-разрядного целого числа?

Например. hash_32b_to_16b(0x12345678) может вернуть 0xABCD.

Давайте начнем с этого как ужасного, но функционального примера решения:

function hash_32b_to_16b(val32b) {
    return val32b % 0xffff;
}

Вопрос касается, в частности, JavaScript, но вы можете свободно добавлять любые не зависящие от языка решения, желательно без использования библиотечных функций.

Контекстом для этого вопроса является генерация уникальных идентификаторов (например, 64-битный идентификатор может состоять из нескольких 16-битных хэшей различных 32-битных значений). Важно избегать столкновений.

Просто = хорошо. Дурацкий + запутанный = забавный.

18
dkamins 17 Июн 2010 в 04:32

6 ответов

Лучший ответ

Это зависит от природы целых чисел. Если они могут содержать несколько битовых масок или могут отличаться степенями двух, то простые XOR будут иметь высокую вероятность коллизий. Вы можете попробовать что-то вроде (i>>16) ^ ((i&0xffff) * p), где p - простое число.

Все хеши безопасности, такие как MD5, хороши, но они явно излишни. Что-нибудь более сложное, чем CRC16, является излишним.

3
Rotsor 17 Июн 2010 в 01:16

Предполагая, что вы ожидаете, что младшие значащие биты будут «меняться» больше всего, я думаю, что вы, вероятно, получите достаточно хорошее распределение, просто используя младшие 16 битов значения в качестве хэша.

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

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

2
Michael Burr 17 Июн 2010 в 00:53

Я думаю, что это лучшее, что вы собираетесь получить. Вы можете сжать код до одной строки, но в качестве документации представлены переменные:

function hash_32b_to_16b(val32b) {
    var rightBits = val32b & 0xffff; // Left-most 16 bits
    var leftBits = val32b & 0xffff0000; // Right-most 16 bits

    leftBits = leftBits >>> 16; // Shift the left-most 16 bits to a 16-bit value

    return rightBits ^ leftBits; // XOR the left-most and right-most bits
}

Учитывая параметры задачи, решение best будет иметь каждый 16-битный хэш, точно соответствующий 2 ^ 16 32-битным числам. Это также могло бы по-разному хэшировать 32-битные числа IMO. Если я что-то упустил, я верю, что это решение делает эти две вещи.

Я бы сказал, что безопасность не может рассматриваться в этой проблеме, поскольку хешированное значение слишком мало. Я считаю, что решение, которое я дал, обеспечивает равномерное распределение 32-битных чисел по 16-битным хэшам

5
John Bledsoe 17 Июн 2010 в 01:05

Ключом к максимальному сохранению энтропии некоторого исходного 32-битного «сигнала» является обеспечение того, чтобы каждый из 32 входных битов обладал независимой и равной способностью изменять значение 16-битного выходного слова.

Поскольку OP запрашивает размер в битах, который составляет ровно половину от исходного, самый простой способ удовлетворить этот критерий - XOR верхняя и нижняя половины, как уже упоминали другие. Использование XOR является оптимальным, поскольку, как и очевидно определение XOR - независимое переключение любого из 32 входных битов гарантированно изменит значение 16-разрядного вывода.

Проблема становится более интересной, когда вам нужно дальнейшее сокращение, превышающее половину размера , скажем, от 32-битного ввода до, скажем, 2- битовый вывод . Помните, что цель состоит в том, чтобы сохранить как можно больше энтропии от источника, поэтому решения, которые включают в себя наивное маскирование двух младших битов с (i & 3), как правило, движутся в неправильном направлении; выполнение этого гарантирует , что никакие биты , кроме , не могут маскировать биты, влияющие на результат, и это обычно означает, что есть произвольная, возможно, полезная часть сигнала времени выполнения, которая будучи в итоге отвергнут без принципа.

Исходя из предыдущего параграфа, вы, конечно, могли бы выполнить итерацию с XOR еще три раза, чтобы получить 2-битный вывод с желаемым свойством одинакового влияния на каждый / любой из входные биты. Конечно, это решение по-прежнему оптимально правильно, но включает в себя циклы или несколько развернутых операций, которые, как оказывается, не нужны!

К счастью, есть хорошая техника, состоящая только из двух операций , которая дает доказуемо-оптимальный результат для этой ситуации. Как и в случае XOR , он не только гарантирует, что при любом заданном 32-разрядном значении изменение любого одного из входных битов приведет к изменению (например) 2-разрядного выходного значения, но также что при равномерном распределении входных значений распределение 2-битных выходных значений также будет совершенно равномерным. Например, для 4,294,967,296 возможных входных значений метод дает ровно 1,073,741,824 каждого из четырех возможных 2-битных результатов хеширования { 0, 1, 2, 3 }.

В методе, о котором я здесь упоминаю, используются особые магические значения, которые я обнаружил с помощью исчерпывающего поиска и которые, по-видимому, не обсуждаются в других местах в Интернете, по крайней мере, для конкретного обсуждаемого здесь использования (т. Е. Для обеспечения равномерного распределения хэшей, которое максимально сохраняющий энтропию). Любопытно, что в соответствии с этим же исчерпывающим поиском магические значения на самом деле уникальны. Это означает, что для каждой целевой ширины в битах { 16, 8, 4, 2 } магическое значение, которое я показываю ниже, является только значение, которое при использовании, как я показываю здесь, удовлетворяет критериям идеального хеширования, изложенным выше.

Без лишних слов уникальная и математически оптимальная процедура для хэширования 32-битного кода в n = { 16, 8, 4, 2 } заключается в умножении на магическое значение, соответствующее n (без знака, отбрасывая переполнение), и затем возьмите n старшие биты результата. Чтобы выделить эти результирующие биты в качестве значения хеш-функции в диапазоне [0 ... (2ⁿ - 1)], просто сдвиньте вправо (без знака!) Результат умножения на 32 - n биты.

«Волшебные» значения и синтаксис C-like выражений:

Максимально сохраняющий энтропию хеш для сокращения с 32-битных до ...

Target Bits    Multiplier    Right Shift          Expression
-----------   ------------   -----------   -----------------------
    16         0x80008001        16        (i * 0x80008001) >> 16
     8         0x80808081        24        (i * 0x80808081) >> 24
     4         0x88888889        28        (i * 0x88888889) >> 28
     2         0xAAAAAAAB        30        (i * 0xAAAAAAAB) >> 30


< сильный > Примечание :

  1. Используйте 32-разрядное умножение без знака и отбрасывайте все переполнения (умножение на 64 бита не требуется).
  2. Если вы изолируете результат с помощью правого сдвига (как показано), обязательно используйте операцию unsigned shift.


[ edit: добавлена таблица для 64-битных входных значений]

Максимально сохраняющий энтропию хеш для уменьшения 64-битного значения до ...

Target Bits   Multiplier           Right Shift              Expression
-----------   ------------------   -----------   -------------------------------
    32        0x8000000080000001       32        (i * 0x8000000080000001) >> 32
    16        0x8000800080008001       48        (i * 0x8000800080008001) >> 48
     8        0x8080808080808081       56        (i * 0x8080808080808081) >> 56
     4        0x8888888888888889       60        (i * 0x8888888888888889) >> 60
     2        0xAAAAAAAAAAAAAAAB       62        (i * 0xAAAAAAAAAAAAAAAB) >> 62



Дальнейшее обсуждение

Я нашел все это довольно круто. С практической точки зрения ключевое информационное теоретическое требование - это гарантия того, что для любого входного значения m-bit и соответствующего ему результата хеш-значения n-bit, перестановка любого одного из m исходных битов всегда вызывает некоторые изменения в значении результата n-bit . Теперь, хотя в общей сложности существует 2ⁿ возможных значений результатов, одно из них уже «используется» (само по себе) с момента «переключения» на этот от любого другого результата был бы никаким изменением вообще. Это оставляет 2ⁿ - 1 результирующие значения, которые могут быть использованы для всего набора m входных значений, перевернутых одним битом.

Давайте рассмотрим пример; на самом деле, чтобы показать, как эта техника может граничить с жутким или совершенно волшебным, мы рассмотрим более экстремальный случай, когда m = 64 и n = 2. С 2 выходными битами есть четыре возможных значения результата, { 0, 1, 2, 3 }. Предполагая произвольное 64-битное входное значение 0x7521d9318fbdf523, мы получаем его 2-битное хеш-значение 1:

 (0x7521d9318fbdf523 * 0xAAAAAAAAAAAAAAAB) >> 62   // result -->  '1'

Таким образом, результатом является 1, и утверждается, что нет значения в наборе из 64 значений где переключается один бит 0x7521d9318fbdf523 может иметь то же значение результата . То есть ни один из этих 64 других результатов не может использовать значение 1, и все должны вместо этого использовать 0, 2 или 3 . Таким образом, в этом примере кажется, что каждое из 2⁶⁴ входных значений - исключая 64 других входных значения - будет эгоистично занимать одну четверть выходного пространства для себя. Когда вы рассматриваете абсолютную величину этих взаимодействующих ограничений, может ли вообще существовать одновременно удовлетворительное решение?

Конечно же, чтобы показать, что (точно?) Один делает , здесь приведены значения результата хеширования, перечисленные по порядку, для входов, которые переключают один бит 0x7521d9318fbdf523 (по одному за раз). ), от MSB (позиция 63) до LSB (0).

3 2 0 3 3 3 3 3 3 0 0 0 3 0 3 3 0 3 3 3 0 0 3 3 3 0 0 3 3 0 3 3  // continued…
0 0 3 0 0 3 0 3 0 0 0 3 0 3 3 3 0 3 0 3 3 3 3 3 3 0 0 0 3 0 0 3  // notice: no '1' values

Как видите, здесь нет 1 значений, что означает, что каждый бит в исходном «как есть» должен влиять на результат ( или, если вы предпочитаете, состояние de facto каждого бита в 0x7521d9318fbdf523 является существенным , чтобы весь общий результат не был "не-" 1 " ) . Поскольку независимо от того, какое однобитное изменение вы вносите в 64-битный вход, 2-битное значение результата больше не будет 1.

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

Таким образом, мы видим, что каждое из 2⁶⁴ возможных значений источника независимо накладывает на ровно 64 других значения источника ограничение исключения одного из возможных значений результата. Что бросает вызов моей интуиции по этому поводу, так это то, что существуют неописанные квадриллионы этих наборов из 64 членов, каждый из которых также принадлежит к 63 другим , по-видимому, не связанным наборам переворота битов. Тем не менее, несмотря на эту самую запутанную загадку переплетенных ограничений, тем не менее, тривиально использовать одно (я полагаю) разрешение, которое одновременно точно удовлетворяет их всем.

Кажется, все это связано с чем-то, что вы, возможно, заметили в таблицах выше: а именно, я не вижу очевидного способа распространить методику на случай сжатия до 1-битного результата. В этом случае есть только два возможных результирующих значения { 0, 1 }, поэтому, если любое / каждое заданное (например) 64-битное входное значение все же суммарно исключает свой собственный результат из результата для всех 64 его однобитовых переверните соседей, тогда это теперь по существу накладывает другие , оставаясь только значением для этих 64. Математическая разбивка, которую мы видим в таблице, кажется, сигнал о том, что одновременный результат в таких условиях является слишком большим мостом.

Другими словами, специальная «сохраняющая информацию» характеристика XOR (то есть его роскошная надежная гарантия того, что, в отличие от И , ИЛИ и т. д., это c̲a̲n̲ и w̲i̲l̲l̲ всегда немного меняется), что неудивительно, требует определенных затрат, а именно, жесткой необоротной потребности в определенном количестве свободного пространства - по крайней мере, 2 бита - для работы.

5
Glenn Slayden 10 Дек 2019 в 13:01

Что-то простое, как это ....

function hash_32b_to_16b(val32b) {    
    var h = hmac(secretKey, sha512);
    var v = val32b;
    for(var i = 0; i < 4096; ++i)
        v = h(v);
    return v % 0xffff;
}
0
yfeldblum 17 Июн 2010 в 00:40

Я бы сказал, просто примените стандартный хеш, такой как sha1 или md5, а затем возьмите последние 16 бит.

2
dreeves 17 Июн 2010 в 00:41