Каков наилучший алгоритм для переопределенного System.Object.GetHashCode? - PullRequest
1332 голосов
/ 04 ноября 2008

В .NET System.Object.GetHashCode метод используется во многих местах, в библиотеках базовых классов .NET. Особенно когда быстро находишь предметы в коллекции или определяешь равенство. Существует ли стандартный алгоритм / лучшие рекомендации по реализации переопределения GetHashCode для моих пользовательских классов, чтобы я не снижал производительность?

Ответы [ 18 ]

1487 голосов
/ 04 ноября 2008

Обычно я использую что-то вроде реализации, приведенной в сказочном Effective Java Джоша Блоха. Это быстро и создает довольно хороший хеш, который вряд ли вызовет столкновения. Выберите два разных простых числа, например, 17 и 23 и сделайте:

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

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

Это лучше, чем обычная практика использования XOR хеш-кодов по двум основным причинам. Предположим, у нас есть тип с двумя int полями:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

Кстати, более ранний алгоритм в настоящее время используется компилятором C # для анонимных типов.

Эта страница предоставляет довольно много опций. Я думаю, что в большинстве случаев вышеприведенное «достаточно хорошо», и его невероятно легко запомнить и понять правильно. Альтернатива FNV также проста, но использует разные константы и XOR вместо ADD в качестве операции объединения. Он выглядит что-то как код ниже, но обычный алгоритм FNV работает с отдельными байтами, поэтому для этого потребуется модификация для выполнения одной итерации на байт вместо 32-битного хеш-значения. FNV также предназначен для переменных длин данных, тогда как мы используем его здесь всегда для одного и того же числа значений полей. Комментарии к этому ответу предполагают, что код здесь на самом деле не работает так же (в тестируемом примере), как описанный выше метод сложения.

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

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

Согласно документации :

Вы можете переопределить GetHashCode для неизменяемых ссылочных типов. В общем, для изменяемых ссылочных типов вы должны переопределить GetHashCode, только если:

  • Вы можете вычислить хеш-код из полей, которые не являются изменяемыми; или
  • Вы можете гарантировать, что хеш-код изменяемого объекта не изменится, пока объект содержится в коллекции, которая опирается на свой хеш-код.
362 голосов
/ 08 января 2011

Анонимный тип

Microsoft уже предоставляет хороший универсальный генератор HashCode: просто скопируйте значения вашего свойства / поля в анонимный тип и хешируйте его:

new { PropA, PropB, PropC, PropD }.GetHashCode();

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

ValueTuple - обновление для C # 7

Как упоминает @cactuaroid в комментариях, можно использовать кортеж значения. Это экономит несколько нажатий клавиш и, что более важно, выполняется исключительно в стеке (без мусора):

(PropA, PropB, PropC, PropD).GetHashCode();

(Примечание. Исходная техника с использованием анонимных типов, по-видимому, создает объект в куче, т.е. мусор, поскольку анонимные типы реализованы в виде классов, хотя это может быть оптимизировано компилятором. Было бы интересно сравнить эти параметры , но опция кортежа должна быть лучше.)

101 голосов
/ 04 апреля 2010

Вот мой помощник хеш-кода.
Преимущество состоит в том, что он использует аргументы универсального типа и, следовательно, не вызывает бокс:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

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

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

или как это:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}
60 голосов
/ 23 февраля 2009

У меня есть класс Hashing в библиотеке Helper, который я использую для этой цели.

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

Тогда просто вы можете использовать его как:

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

Я не оценивал его производительность, поэтому любые отзывы приветствуются.

54 голосов
/ 04 сентября 2013

Вот мой вспомогательный класс, использующий реализацию Джона Скита .

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

Использование:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

Если вы хотите избежать написания метода расширения для System.Int32:

public struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

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

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

Обновление после комментария Мартина:

obj != null вызвал бокс, поэтому я переключился на компаратор по умолчанию.

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

Редактировать (май 2018 г.):

EqualityComparer<T>.Default getter теперь встроен в JIT - запрос на получение упоминается Стивеном Таубом в этом посте .

29 голосов
/ 23 февраля 2009

В большинстве случаев, когда Equals () сравнивает несколько полей, на самом деле не имеет значения, хеширует ли ваш GetHash () одно или несколько полей. Вам просто нужно убедиться, что вычисление хэша действительно дешево ( Без выделения , пожалуйста) и быстро ( Без сложных вычислений и, конечно, без соединений с базой данных) и обеспечивает хорошее распределение. 1005 *

Поднятие тяжестей должно быть частью метода Equals (); хеш должен быть очень дешевой операцией, чтобы вызывать Equals () для как можно меньшего числа элементов.

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

20 голосов
/ 14 января 2014

До недавнего времени мой ответ был очень близок к ответу Джона Скита. Тем не менее, я недавно начал проект, в котором использовались хеш-таблицы степени двойки, то есть хеш-таблицы, где размер внутренней таблицы равен 8, 16, 32 и т. Д. Есть веская причина для предпочтения размеров простых чисел, но есть Есть некоторые преимущества для размеров степени два.

И это в значительной степени отстой. Поэтому после небольшого количества экспериментов и исследований я начал перефразировать свои хэши следующим текстом:

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

А потом мой хэш-стол с степенью двойки больше не сосал.

Это беспокоило меня, хотя, потому что выше не должно работать. Или, точнее, он не должен работать, если оригинал GetHashCode() не был очень плохим.

Повторное смешивание хеш-кода не может улучшить отличный хеш-код, потому что единственный возможный эффект - это введение нескольких коллизий.

Повторное смешивание хеш-кода не может улучшить ужасный хеш-код, потому что единственный возможный эффект - это изменение, например. большое количество столкновений по значению 53 с большим числом по значению 18,3487,291.

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

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

И, конечно, было тревожно, насколько можно улучшить реализацию string.GetHashCode() в .NET (или изучить здесь ) таким образом (порядка тестов, выполняющих около 20 -30 раз быстрее из-за меньшего количества коллизий) и больше беспокоит то, насколько мои хэш-коды могут быть улучшены (гораздо больше).

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

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

В итоге я остановился на портировании SpookyHash на .NET. Действительно, приведенный выше код является версией быстрого использования SpookyHash для получения 32-разрядного вывода из 32-разрядного ввода.

Теперь SpookyHash - это не просто быстрый фрагмент кода для запоминания. Мой порт этого еще меньше, потому что я много раз вписал его вручную для лучшей скорости *. Но для этого и используется повторное использование кода.

Затем я поместил этот проект в одну сторону, потому что так же, как в первоначальном проекте возник вопрос о том, как создать лучший хэш-код, так и в проекте возник вопрос о том, как создать лучший. NET memcpy.

Затем я вернулся и произвел много перегрузок, чтобы легко передать почти все нативные типы (кроме decimal †) в хэш-код.

Это быстро, за что Боб Дженкинс заслуживает большую часть кредита, потому что его оригинальный код, с которого я портировал, еще быстрее, особенно на 64-битных машинах, алгоритм которых оптимизирован для ‡.

Полный код можно увидеть на https://bitbucket.org/JonHanna/spookilysharp/src, но учтите, что приведенный выше код является его упрощенной версией.

Однако, поскольку он уже написан, его можно использовать проще:

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

Он также принимает начальные значения, поэтому, если вам нужно иметь дело с ненадежным вводом и хотите защитить от атак Hash DoS, вы можете установить начальное время на основе времени безотказной работы или аналогичного, а также сделать результаты непредсказуемыми для злоумышленников:

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

* Большим сюрпризом является то, что метод вращения вручную, который возвращает (x << n) | (x >> -n) улучшенных вещей. Я был бы уверен, что дрожание указало бы на это, но профилирование показало обратное.

decimal не является родным с точки зрения .NET, хотя это с C #. Проблема в том, что его собственный GetHashCode() рассматривает точность как значимую, а его собственный Equals() - нет. Оба являются допустимыми, но не смешанными. При реализации своей собственной версии вам нужно выбрать одну или другую, но я не знаю, какую именно вы хотите.

Для сравнения. При использовании в строке SpookyHash на 64 битах значительно быстрее, чем string.GetHashCode() на 32 битах, что немного быстрее, чем string.GetHashCode() на 64 битах, что значительно быстрее, чем SpookyHash на 32 битах, хотя все еще достаточно быстро, чтобы быть разумный выбор.

13 голосов
/ 07 октября 2010

Это хорошо:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

А вот как это использовать:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}
9 голосов
/ 21 января 2014

Вот еще одна свободная реализация алгоритма, опубликованного выше Джоном Скитом , но который не включает в себя операции выделения или упаковки:

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

Использование:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

Компилятор гарантирует, что HashValue не вызывается с классом из-за ограничения общего типа. Но компилятор HashObject не поддерживается, поскольку добавление универсального аргумента также добавляет операцию бокса.

8 голосов
/ 22 марта 2011

Вот мой упрощенный подход. Я использую классический шаблон строителя для этого. Он безопасен для типов (без упаковки / распаковки) и совместим с .NET 2.0 (без методов расширения и т.

Используется так:

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

А вот класс острых строителей:

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}
...