Переносимая реализация хэш-кода для двоичных данных

Я ищу переносимый алгоритм для создания хэш-кода для двоичных данных. Ни один из двоичных данных не является очень длинным - я Avro-кодирую ключи для использования в kafka.KeyedMessages - мы, вероятно, говорим о длине от 2 до 100 байтов, но большинство ключей находятся в диапазоне от 4 до 8 байтов.

На данный момент мое лучшее решение - преобразовать данные в шестнадцатеричную строку, а затем выполнить hashCode этого. Я могу заставить это работать как в Scala, так и в JavaScript. Предполагая, что я определил b: Array[Byte], Scala выглядит так:

b.map("%02X" format _).mkString.hashCode

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

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

Вместо того, чтобы заполнять данные в виде шестнадцатеричного значения, я предполагаю, что вы можете просто преобразовать двоичные данные в строку, чтобы строка имела то же количество байтов, что и двоичные данные. Это будет все искаженное, больше управляющих символов, чем печатных символов, но тем не менее это будет строка. Но есть ли у вас проблемы с переносимостью? Порядок байтов, Unicode и т. Д.

Между прочим, если вы так далеко прочитали и еще этого не знаете - вы не можете просто сделать:

val b: Array[Byte] = ...
b.hashCode

К счастью, я знал это еще до того, как начал, потому что натолкнулся на это на ранней стадии.

Обновить

Основываясь на первом полученном ответе, на первый взгляд кажется, что java.util.Arrays.hashCode(Array[Byte]) подойдет. Однако, если вы проследите путь javadoc, вы увидите, что это алгоритм, стоящий за ним, который основан на объединенном алгоритме для List и алгоритме для byte.

int hashCode = 1;
for (byte e : list)  hashCode = 31*hashCode + (e==null ? 0 : e.intValue());

Как видите, все, что он делает, это создает Long, представляющий значение. В какой-то момент число становится слишком большим, и оно замыкается. Это не очень удобно. Я могу заставить его работать для JavaScript, но вам нужно импортировать npm модуль long. Если да, то это будет выглядеть так:

function bufferHashCode(buffer) {
  const Long = require('long');
  var hashCode = new Long(1);
  for (var value of buff.values()) { hashCode = hashCode.multiply(31).add(value) }
  return hashCode
}

bufferHashCode(new Buffer([1,2,3]));
// hashCode = Long { low: 30817, high: 0, unsigned: false }

И вы действительно получаете те же результаты, когда данные оборачиваются, вроде как, хотя я не знаю почему. В Scala:

java.util.Arrays.hashCode(Array[Byte](1,2,3,4,5,6,7,8,9,10))
// res30: Int = -975991962

Обратите внимание, что результатом является Int. В JavaScript:

bufferHashCode(new Buffer([1,2,3,4,5,6,7,8,9,10]);
// hashCode = Long { low: -975991962, high: 197407, unsigned: false }

Поэтому я должен взять low байт и игнорировать high, но в остальном я получаю те же результаты.


person David Griffin    schedule 03.05.2016    source источник
comment
Если он должен быть портативным, разве вы не должны использовать что-то вроде md5? Должны быть API / реализации md5 на всех упомянутых вами языках, верно?   -  person DavidS    schedule 05.05.2016
comment
Или, в более широком смысле, какой-нибудь быстрый некриптографический хеш.   -  person DavidS    schedule 05.05.2016


Ответы (2)


Эта функция уже доступна в стандартной библиотеке Java, посмотрите метод Arrays.hashCode().

Поскольку ваши двоичные данные Array[Byte], вот как вы можете проверить их работу:

println(java.util.Arrays.hashCode(Array[Byte](1,2,3))) // prints 30817
println(java.util.Arrays.hashCode(Array[Byte](1,2,3))) // prints 30817
println(java.util.Arrays.hashCode(Array[Byte](2,2,3))) // prints 31778

Обновление: неверно, что реализация Java блокирует байты. Конечно, есть преобразование в int, но этого нет. Это реализация Java:

public static int hashCode(byte a[]) {
    if (a == null) return 0;
    int result = 1;
    for (byte element : a) result = 31 * result + element;
    return result;
}

Обновление 2 Если вам нужна реализация JavaScript, которая дает те же результаты, что и реализация Scala / Java, вы можете расширить алгоритм, например, взяв только крайний правый 31 бит:

def hashCode(a: Array[Byte]): Int = {
  if (a == null) {
    0
  } else {
    var hash = 1
    var i: Int = 0
    while (i < a.length) {
      hash = 31 * hash + a(i)
      hash = hash & Int.MaxValue // taking only the rightmost 31 bits
      i += 1
    }
    hash
  }
}

и JavaScript:

var hashCode = function(arr) {
    if (arr == null) return 0; 
    var hash = 1;
    for (var i = 0; i < arr.length; i++) {
        hash = hash * 31 + arr[i]
        hash = hash % 0x80000000 // taking only the rightmost 31 bits in integer representation
    }
    return hash;
}

Почему две реализации дают одинаковые результаты? В Java целочисленное переполнение ведет себя так, как если бы сложение было выполнено без потери точности, а затем биты выше 32 были отброшены, а & Int.MaxValue отбрасывает 32 бита nd. В JavaScript нет потери точности для целых чисел до 2 53, что является пределом, который выражение 31 * hash + a(i) никогда не превышает. % 0x80000000 тогда ведет себя так, как будто принимает крайний правый 31 бит. Случай без переполнений очевиден.

person Mifeet    schedule 04.05.2016
comment
Есть идеи, что это за алгоритм - вопрос тоже в переносимости. Как это работает, смогу ли я воссоздать его на языке, отличном от Java? - person David Griffin; 04.05.2016
comment
Смотрите мое обновление в вопросе, этот алгоритм не очень переносимый. - person David Griffin; 04.05.2016
comment
@DavidGriffin Итак, вам нужен не алгоритм хеширования в Scala / Java и Javascript, а алгоритм, который дает одинаковый результат в обоих? Если да, см. Мой обновленный ответ. - person Mifeet; 07.05.2016
comment
Я полагаю, что с точки зрения коллизий не хуже, так как результат в любом случае Int? - person David Griffin; 07.05.2016
comment
Частота столкновений зависит от модели использования. Хеш-таблицы часто имеют размер, равный степени 2. Если вы используете простой размер таблицы по модулю, точность будет такой же, как и в версии Arrays. Если вы используете тот же алгоритм, что и Java HashMap, вы можете потерять некоторую точность для таблиц, размер которых превышает 2 ^ 17. Как правило, если вам действительно нужно как можно меньше коллизий, лучше использовать какую-нибудь другую хэш-функцию, например, Murmur хеш или то, что DavidS предложил в комментариях. В противном случае описанной выше функции будет достаточно. - person Mifeet; 07.05.2016

Это основа алгоритма, используемого в библиотеке Java:

  int result 1;
  for (byte element : a) result = 31 * result + element;

Вы комментируете:

этот алгоритм не очень переносимый

Неправильно. Если мы говорим о Java, то при условии, что мы все согласны с типом result, алгоритм будет на 100% переносимым.

Да, при вычислении происходит переполнение, но оно происходит точно так же во всех допустимых реализациях языка Java. Java int задан как 32-битное дополнение до двух со знаком, и поведение операторов при переполнении четко определено ... и одинаково для всех реализаций. (То же самое и с long ... хотя размер, очевидно, другой.)

Я не эксперт, но, насколько я понимаю, числовые типы Scala имеют те же свойства, что и Java. Javascript отличается, поскольку основан на стандарте IEE 754 с плавающей запятой двойной точности. Однако в случае, если вы должны иметь возможность переносимо кодировать алгоритм Java на Javascript. (Я думаю, версия @Mifeet неверна ...)

person Stephen C    schedule 04.05.2016
comment
@DavidGriffin - Что не так? Версия Javascript Mifeet или мой ответ? - person Stephen C; 05.05.2016
comment
Извините, был на телефоне, и он нажал "Return", прежде чем я имел в виду - версия JavaScript Mifeet неверна. Вы должны использовать библиотеку для реализации правильной Long обработки. По крайней мере, в библиотеке я использовал обертки примерно так же, как и в Java. - person David Griffin; 05.05.2016
comment
В качестве альтернативы он мог использовать битовую маскировку после каждой итерации. По-видимому, Javascript преобразует число в 32-битное int перед выполнением побитовой операции. - person Stephen C; 05.05.2016