Скажем, у меня есть объект, в котором хранится массив байтов, и я хочу иметь возможность эффективно сгенерировать для него хэш-код. Раньше я использовал для этого криптографические хеш-функции, потому что их легко реализовать, но они делают гораздо больше работы, чем должно быть криптографически в один конец, и меня это не волнует (я просто использую хэш-код как ключ к хеш-таблице).
Вот что у меня есть сегодня:
struct SomeData : IEquatable<SomeData>
{
private readonly byte[] data;
public SomeData(byte[] data)
{
if (null == data || data.Length <= 0)
{
throw new ArgumentException("data");
}
this.data = new byte[data.Length];
Array.Copy(data, this.data, data.Length);
}
public override bool Equals(object obj)
{
return obj is SomeData && Equals((SomeData)obj);
}
public bool Equals(SomeData other)
{
if (other.data.Length != data.Length)
{
return false;
}
for (int i = 0; i < data.Length; ++i)
{
if (data[i] != other.data[i])
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
return BitConverter.ToInt32(new MD5CryptoServiceProvider().ComputeHash(data), 0);
}
}
есть идеи?
dp: Вы правы, что в Equals я пропустил проверку, обновил. Использование существующего хэш-кода из массива байтов приведет к равенству ссылок (или, по крайней мере, к той же концепции, переведенной в хэш-коды). Например:
byte[] b1 = new byte[] { 1 };
byte[] b2 = new byte[] { 1 };
int h1 = b1.GetHashCode();
int h2 = b2.GetHashCode();
С этим кодом, несмотря на то, что два байтовых массива имеют одинаковые значения внутри, они относятся к разным частям памяти и приведут к (возможно) различным хэш-кодам. Мне нужно, чтобы хэш-коды для двух байтовых массивов с одинаковым содержимым были равны.





Если вам нужна производительность, я протестировал несколько хеш-ключей и Рекомендую Хеш-функция Боба Дженкина. Это безумно быстро вычислить и даст столько же коллизий, сколько криптографический хеш, который вы использовали до сих пор.
Я вообще не знаю C# и не знаю, может ли он связываться с C, но вот его реализация на C.
Хэш-код объекта не обязательно должен быть уникальным.
Правило проверки:
Equals.Все, что вам нужно, это алгоритм GetHashCode, который разбивает вашу коллекцию примерно на равные группы - он не должен формировать ключ, поскольку HashTable или Dictionary<> должны будут использовать хэш для оптимизации поиска.
Как долго, по вашему мнению, будут храниться данные? Насколько случайным? Если длина сильно различается (например, для файлов), просто верните длину. Если длины, вероятно, будут одинаковыми, посмотрите на подмножество байтов, которое меняется.
GetHashCode должен быть намного быстрее, чем Equals, но не обязательно должен быть уникальным.
Две одинаковые вещи никогда не должен имеют разные хэш-коды. Два разных объекта не следует имеют одинаковый хэш-код, но следует ожидать некоторых коллизий (в конце концов, существует больше перестановок, чем возможных 32-битных целых чисел).
+1 Это было одно из самых ясных объяснений, которые я когда-либо слышал, почему выгодно переопределить Equals и GetHashcode.
Недостаточно ли использовать существующий хэш-код из поля массива байтов? Также обратите внимание, что в методе Equals вы должны проверить, что массивы имеют одинаковый размер, прежде чем выполнять сравнение.
Легче сказать, чем сделать хороший хэш. Помните, что вы в основном представляете n байтов данных с m битами информации. Чем больше ваш набор данных и чем меньше m, тем больше вероятность столкновения ... два фрагмента данных разрешаются в один и тот же хэш.
Самый простой хэш, который я когда-либо узнал, - это просто XOR для всех байтов вместе. Это просто, быстрее, чем самые сложные алгоритмы хеширования и приличный алгоритм хеширования общего назначения для небольших наборов данных. На самом деле это пузырьковые алгоритмы хеширования. Поскольку простая реализация оставит вам 8 бит, это всего 256 хэшей ... не так уж и важно. Вы можете использовать фрагменты XOR вместо отдельных байтов, но тогда алгоритм становится намного сложнее.
Так что, конечно, криптографические алгоритмы, возможно, делают некоторые вещи, которые вам не нужны ... но они также являются огромным шагом в улучшении качества хеширования общего назначения. Используемый вами хеш MD5 имеет 128 бит с миллиардами и миллиардами возможных хешей. Единственный способ получить что-то лучшее - это взять несколько репрезентативных выборок данных, которые, как вы ожидаете, будут обрабатываться вашим приложением, и попробовать на них различные алгоритмы, чтобы увидеть, сколько у вас коллизий.
Так что, пока я не увижу причину не использовать стандартный алгоритм хеширования (возможно, производительность?), Я буду рекомендовать вам придерживаться того, что у вас есть.
Вы сравнивали с методом SHA1CryptoServiceProvider.ComputeHash? Он принимает байтовый массив и возвращает хеш SHA1, и я считаю, что он довольно хорошо оптимизирован. Я использовал его в Обработчик идентификаторов, который неплохо работал под нагрузкой.
SHA1 медленнее, чем MD5. Если вас не беспокоит безопасность, используйте MD5.
Спасибо, Джон .. Метод SHA1CryptoServiceProvider.ComputeHash сработал для меня .. !!
RuntimeHelpers.GetHashCode может помочь:
From Msdn:
Serves as a hash function for a particular type, suitable for use in hashing algorithms and data structures such as a hash table.
Независимо от того, хотите ли вы идеальную хеш-функцию (разные значения для каждого объекта, который оценивается как равный) или просто довольно хорошее, всегда зависит от производительности, обычно требуется время, чтобы вычислить хорошую хеш-функцию, и если ваш набор данных невелик, вам лучше быстрая функция. Самым важным (как указано в вашем втором сообщении) является правильность, и для этого все, что вам нужно, - это вернуть длину массива. В зависимости от вашего набора данных это может быть даже нормально. Если это не так (скажем, все ваши массивы одинаково длинные), вы можете пойти с чем-то дешевым, например, посмотреть на первое и последнее значение и выполнить XOR для их значений, а затем добавить больше сложности, если вы сочтете нужным для ваших данных.
Быстрый способ увидеть, как ваша хеш-функция работает с вашими данными, - это добавить все данные в хеш-таблицу и подсчитать количество вызовов функции Equals, если это слишком часто, у вас есть дополнительная работа над функцией. Если вы сделаете это, просто имейте в виду, что при запуске размер хеш-таблицы должен быть больше, чем ваш набор данных, иначе вы собираетесь повторно хешировать данные, которые вызовут повторные вставки и другие оценки Equals (хотя, возможно, более реалистично?)
Для некоторых объектов (не для этого) быстрый HashCode может быть сгенерирован с помощью ToString (). GetHashCode (), конечно, не оптимально, но полезно, поскольку люди склонны возвращать что-то близкое к идентичности объекта из ToString (), и это точно что ищет GetHashcode
Интересный факт: худшая производительность, которую я когда-либо видел, была, когда кто-то по ошибке вернул константу из GetHashCode, хотя ее легко обнаружить с помощью отладчика, особенно если вы выполняете много поисков в своей хеш-таблице.
Заимствуя код, сгенерированный программой JetBrains, я остановился на этой функции:
public override int GetHashCode()
{
unchecked
{
var result = 0;
foreach (byte b in _key)
result = (result*31) ^ b;
return result;
}
}
Проблема только с XOring байтов заключается в том, что 3/4 (3 байта) возвращаемого значения имеет только 2 возможных значения (все включено или все выключено). Это немного расширяет кругозор.
Установка точки останова в Equals была хорошим предложением. При добавлении около 200 000 записей моих данных в словарь обнаруживается около 10 вызовов Equals (или 1/20 000).
для IList<byte> определенно используйте цикл for на основе индексации, чем foreach. Возможно, это не большая разница для byte[], поскольку foreach будет преобразован в for внутренне.
Циклы foreach иногда компилируются в циклы for при переходе по списку, не уверен, может ли это также произойти при цикле по IList (который всегда должен быть немного медленнее, не имеет такого значения для больших массивов, но для маленьких => foreach имеет больше инициализаций, чем для).
Не используйте криптографические хэши для хеш-таблицы, это смешно / излишне.
Вот и все ... Модифицированный хеш FNV на C#
http://bretm.home.comcast.net/hash/6.html
public static int ComputeHash(params byte[] data)
{
unchecked
{
const int p = 16777619;
int hash = (int)2166136261;
for (int i = 0; i < data.Length; i++)
hash = (hash ^ data[i]) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return hash;
}
}
Это даст довольно уникальные хэши, но на самом деле не будет работать для GetHashCode. Идея состоит в том, что хэш позволяет коллекции иметь быстрый метод проверки совпадения двух byte[] перед использованием более медленного Equals. В этой реализации вы зацикливаете весь массив, поэтому для очень больших массивов проверка равенства может быть намного быстрее. Это хороший способ вычислить хэш общего назначения, но из-за того, как .Net на самом деле использует GetHashCode, это может действительно замедлить сборку.
@Keith: GetHashCode позволяет классам, использующим этот метод, получать целочисленное значение для объекта, чего Equals не предоставляет. С этим значением он может делать другие вещи, кроме простого сравнения (например, получать индекс корзины в хеш-таблице). Таким образом, зацикливание всего массива в GetHashCode может быть преимуществом, даже если то же самое сделано в Equals.
@tigrou - я не говорю, что это бесполезный механизм хеширования, но вы не должны использовать его для реализации GetHashCode, потому что все хешированные коллекции .Net предполагают, что GetHashCode будет на несколько порядков быстрее, чем Equals. Фактически, если проверка GetHashCode пройдена, они продолжат вызывать Equals, потому что ожидается некоторое количество конфликтов. Если оба метода зацикливают всю коллекцию, вы получите очень медленный HashTable или Dictionary.
@Keith - ты здесь не прав. Ключевым моментом является то, что GetHashCode () должен вызываться только один раз, а Equals () должен вызываться для каждого сравнения. Так что для вычисления хэша вполне нормально иметь более продолжительное время выполнения, чем равное. Фактически, встроенное хеширование строк .NET делает именно это.
@Keith: Каалус прав. Хороший хэш-код должен включать информацию обо всем хешируемом объекте, включая все значения свойств и полей. Невозможно избежать сканирования этой информации при каждом вызове, если только рассматриваемый объект не является неизменяемым и не кэширует хэш-код при создании.
Стоит отметить, что связанная страница (здесь кешированная версия - archive.is/MnmRY) на самом деле использует uint, поэтому будет создавать разные хеши.
@Keith в современном ООП мы рассматриваем объект как его внешнюю форму (или лицо, или контракт). Изменения во внутренней, не являющейся внешней спецификацией, не считаются "изменениями в объекте", т.е. изменение поля long cachedHash не считается изменением. Так что иногда мы можем кэшировать номер хеша в поле. Таким образом, хеш кэшируется в большинстве (неизменяемых) классов Java.
Распределение хеширования здесь очень хорошее, наша тема обсуждает «как использовать этот код». Лично я знаю, что создал несколько плохих алгоритмов хеширования;)
Я смотрел на алгоритм FNV .. Я не вижу такого же сдвига бит в коде c. Это украшение или я не смотрю правильный код FNV 1a?
Какова цель битового сдвига? Спасибо.
Я нашел интересные результаты:
У меня есть класс:
public class MyHash : IEquatable<MyHash>
{
public byte[] Val { get; private set; }
public MyHash(byte[] val)
{
Val = val;
}
/// <summary>
/// Test if this Class is equal to another class
/// </summary>
/// <param name = "other"></param>
/// <returns></returns>
public bool Equals(MyHash other)
{
if (other.Val.Length == this.Val.Length)
{
for (var i = 0; i < this.Val.Length; i++)
{
if (other.Val[i] != this.Val[i])
{
return false;
}
}
return true;
}
else
{
return false;
}
}
public override int GetHashCode()
{
var str = Convert.ToBase64String(Val);
return str.GetHashCode();
}
}
Затем я создал словарь с ключами типа MyHash, чтобы проверить, насколько быстро я могу вставлять, и я также могу знать, сколько существует коллизий. Я сделал следующее
// dictionary we use to check for collisions
Dictionary<MyHash, bool> checkForDuplicatesDic = new Dictionary<MyHash, bool>();
// used to generate random arrays
Random rand = new Random();
var now = DateTime.Now;
for (var j = 0; j < 100; j++)
{
for (var i = 0; i < 5000; i++)
{
// create new array and populate it with random bytes
byte[] randBytes = new byte[byte.MaxValue];
rand.NextBytes(randBytes);
MyHash h = new MyHash(randBytes);
if (checkForDuplicatesDic.ContainsKey(h))
{
Console.WriteLine("Duplicate");
}
else
{
checkForDuplicatesDic[h] = true;
}
}
Console.WriteLine(j);
checkForDuplicatesDic.Clear(); // clear dictionary every 5000 iterations
}
var elapsed = DateTime.Now - now;
Console.Read();
Каждый раз, когда я вставляю новый элемент в словарь, словарь будет вычислять хэш этого объекта. Таким образом, вы можете определить, какой метод наиболее эффективен, разместив несколько ответов, найденных здесь, в методе public override int GetHashCode(). Метод, который был безусловно самым быстрым и имел наименьшее количество столкновений, был:
public override int GetHashCode()
{
var str = Convert.ToBase64String(Val);
return str.GetHashCode();
}
это заняло 2 секунды. Метод
public override int GetHashCode()
{
// 7.1 seconds
unchecked
{
const int p = 16777619;
int hash = (int)2166136261;
for (int i = 0; i < Val.Length; i++)
hash = (hash ^ Val[i]) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return hash;
}
}
коллизий тоже не было, но выполнение заняло 7 секунд!
Не могли бы вы объяснить свой алгоритм хеширования
private int? hashCode;
public override int GetHashCode()
{
if (!hashCode.HasValue)
{
var hash = 0;
for (var i = 0; i < bytes.Length; i++)
{
hash = (hash << 4) + bytes[i];
}
hashCode = hash;
}
return hashCode.Value;
}
Вы можете вызывать функции c из C# с помощью pinvoke. Он имеет некоторое влияние на производительность (например, закрепление и маршалинг переданных параметров - как зависит от фактического используемого типа), но им можно пренебречь, если не вызывать их слишком часто (что означает, например,> тысячи раз в цикле). Даже некоторые фреймворки для графического рендеринга (а именно OpenTK, SkiaSharp) используют много вызовов pinvoke, и производительность по-прежнему приличная.