В .NET GetHashCode
метод используется во многих местах в библиотеках базовых классов .NET. Для правильной его реализации особенно важно быстро находить элементы в коллекции или при определении равенства.
Существуют ли стандартные алгоритмы или рекомендации по реализации GetHashCode
пользовательских классов, чтобы я не снижал производительность?
.net
algorithm
hashcode
gethashcode
bitbonk
источник
источник
GetHashCode
. Я надеюсь, что это будет полезно для других. Руководство и правила для GetHashCode, написанные Эриком ЛиппертомGetHashCode()
используется в очень многих реализацияхEquals()
. Вот что я имел в виду под этим утверждением.GetHashCode()
insideEquals()
часто используется как ярлык для определения неравенства , потому что, если два объекта имеют разный хеш-код, они должны быть объектами, которые не равны, а остальная часть проверки на равенство не должна выполняться.GetHashCode()
иEquals()
нужно смотреть на все поля обоих объектов (Равный должен делать это, если хеш-коды равны или не проверены). Из-за этого вызовGetHashCode()
внутрьEquals()
часто избыточен и может снизить производительность.Equals()
может также быть в состоянии короткого замыкания, делая это намного быстрее - однако в некоторых случаях хеш-коды могут быть кэшированы, что делаетGetHashCode()
проверку быстрее и стоит. Смотрите этот вопрос для более.Ответы:
Я обычно использую что-то вроде реализации, описанной в сказочной « Эффективной Java» Джоша Блоха . Это быстро и создает довольно хороший хеш, который вряд ли вызовет столкновения. Выберите два разных простых числа, например, 17 и 23, и выполните:
Как отмечено в комментариях, вы можете найти, что вместо этого лучше выбрать большое простое число для умножения. Очевидно, что 486187739 - это хорошо ... и хотя большинство примеров, которые я видел с небольшими числами, имеют тенденцию использовать простые числа, существуют, по крайней мере, похожие алгоритмы, где часто используются не простые числа. Например, в примере с не совсем FNV позже я использовал числа, которые, по-видимому, работают хорошо, но начальное значение не является простым. (Постоянное умножение является простой , хотя. Я не знаю, как это важно.)
Это лучше, чем обычная практика использования
XOR
хэш-кодов по двум основным причинам. Предположим, у нас есть тип с двумяint
полями:Кстати, более ранний алгоритм в настоящее время используется компилятором C # для анонимных типов.
Эта страница дает довольно много вариантов. Я думаю, что в большинстве случаев вышеприведенное «достаточно хорошо», и его невероятно легко запомнить и понять правильно. ФПНА альтернатива является аналогично простой, но использует различные константы и
XOR
вместо того , чтобы вADD
качестве операции комбинирования. Он выглядит примерно так, как показано ниже, но обычный алгоритм FNV работает с отдельными байтами, поэтому для этого потребуется модификация для выполнения одной итерации на байт вместо 32-битного хеш-значения. FNV также предназначен для переменных длин данных, тогда как мы используем его здесь всегда для одного и того же числа значений полей. Комментарии к этому ответу предполагают, что код здесь на самом деле не работает так же (в тестируемом примере), как подход к добавлению выше.Обратите внимание, что следует помнить, что в идеале вы должны предотвращать изменение вашего чувствительного к равенству (и, следовательно, чувствительного к хеш-коду) состояния после добавления его в коллекцию, которая зависит от хеш-кода.
Согласно документации :
источник
Dictionary<TKey,TValue>
предполагается хорошее распределение по модулю определенных простых чисел. И 23 является одним из них. Так что, если у вас есть словарь с Capacity 23, только последний вкладGetHashCode
влияет на составной хеш-код. Поэтому я бы предпочел использовать 29 вместо 23.null
- что не то же самое, что игнорирование поля.Анонимный тип
Microsoft уже предоставляет хороший универсальный генератор HashCode: просто скопируйте значения вашего свойства / поля в анонимный тип и хешируйте его:
Это будет работать для любого количества свойств. Он не использует бокс. Он просто использует алгоритм, уже реализованный в рамках для анонимных типов.
ValueTuple - обновление для C # 7
Как упоминает @cactuaroid в комментариях, можно использовать кортеж значения. Это экономит несколько нажатий клавиш и, что более важно, выполняется исключительно в стеке (без мусора):
(Примечание: оригинальная техника, использующая анонимные типы, по-видимому, создает объект в куче, т.е. мусор, поскольку анонимные типы реализованы как классы, хотя это может быть оптимизировано компилятором. Было бы интересно сравнить эти параметры, но Вариант кортежа должен быть лучше.)
источник
GetHashCode
реализация очень эффективна (кстати, она такая же, как в ответе Джона Скита), но единственная проблема с этим решением состоит в том, что вы генерируете новый экземпляр при любомGetHashCode
вызове. Это может быть немного перегружено, особенно в случае интенсивного доступа к большимnew { PropA, PropB, PropC, PropD }.GetHashCode()
тожеNew With {Key PropA}.GetHashCode()
противном случае GetHashCode не будет возвращать один и тот же хеш-код для разных объектов с одинаковыми «идентифицирующими» свойствами.Вот мой помощник хеш-кода.
Преимущество состоит в том, что он использует аргументы универсального типа и поэтому не будет вызывать бокс:
Также он имеет метод расширения для обеспечения свободного интерфейса, так что вы можете использовать его следующим образом:
или вот так:
источник
T[]
отдельно, как это ужеIEnumerable<T>
У меня есть класс Hashing в библиотеке Helper, который я использую для этой цели.
Тогда просто вы можете использовать его как:
Я не оценивал его производительность, поэтому любые отзывы приветствуются.
источник
unchecked
, чтобы избежать исключений при переполнении, которое желательно приGetHashCode
. Так что это не правильно, если значение переполняется,int
и это совсем не больно.null
он полностью пропущен, может дать вам неожиданные результаты. Вместо того, чтобы пропускать их, вы должны просто использовать некоторое постоянное значение, а неinput[i].GetHashCode()
когда оноinput[i]
равно NULL.Вот мой вспомогательный класс, использующий реализацию Джона Скита .
Применение:
Если вы хотите избежать написания метода расширения для System.Int32:
Он по-прежнему избегает выделения кучи и используется точно так же:
Редактирование (май 2018 г.):
EqualityComparer<T>.Default
теперь метод get является внутренним свойством JIT - запрос на извлечение упомянут Стивеном Тубом в этом сообщении в блоге .источник
var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
obj != null
скомпилируетbox
инструкцию, которая выделит память, еслиT
это тип значения. Вместо этого вы можете использовать,obj.Equals(null)
который будет компилироваться для виртуального вызоваEquals
метода.this.hashCode != h
. Это не вернуло бы то же значение.Стандарт .NET 2.1 и выше
Если вы используете .NET Standard 2.1 или выше, вы можете использовать структуру System.HashCode . Есть два способа его использования:
HashCode.Combine
Combine
Метод может быть использован для создания хэш - код, данные до восьми объектов.HashCode.Add
Add
Метод поможет вам справиться с коллекциями:GetHashCode Made Easy
Вы можете прочитать полный пост в блоге « GetHashCode Made Easy » для более подробной информации и комментариев.
Пример использования
Реализация
Что делает хороший алгоритм?
скорость
Алгоритм, который вычисляет хеш-код, должен быть быстрым. Простой алгоритм обычно будет быстрее.
детерминистический
Алгоритм хеширования должен быть детерминированным, т. Е. При одинаковых входных данных он всегда должен давать одинаковые выходные данные.
Уменьшить коллизии
Алгоритм, который вычисляет хеш-код, должен поддерживать минимальные коллизии хеш-кодов . Столкновение хеша - это ситуация, которая возникает, когда два обращения к
GetHashCode
двум разным объектам производят идентичные хеш-коды. Обратите внимание, что коллизии разрешены (у некоторых есть неправильные представления о том, что это не так), но они должны быть сведены к минимуму.Хорошая хеш-функция должна отображать ожидаемые входные данные как можно более равномерно по всему выходному диапазону. Это должно иметь единообразие.
Предотвратить DoS
В .NET Core каждый раз при перезапуске приложения вы получаете разные хеш-коды. Это функция безопасности для предотвращения атак типа «отказ в обслуживании» (DoS). Для .NET Framework вы должны включить эту функцию, добавив следующий файл App.config:
Благодаря этой функции, хеш-коды никогда не должны использоваться за пределами области приложения, в которой они были созданы, они никогда не должны использоваться в качестве ключевых полей в коллекции, и их никогда не следует сохранять.
Подробнее об этом читайте здесь .
Криптографически безопасно?
Алгоритм не обязательно должен быть криптографической хеш-функцией . Это означает, что он не должен удовлетворять следующим условиям:
источник
В большинстве случаев, когда Equals () сравнивает несколько полей, на самом деле не имеет значения, хеширует ли ваш GetHash () одно или несколько полей. Вам просто нужно убедиться, что вычисление хеша действительно дешево ( без выделения ресурсов , пожалуйста) и быстро ( без сложных вычислений и, конечно, без соединений с базой данных) и обеспечивает хорошее распределение.
Поднятие тяжестей должно быть частью метода Equals (); хеш должен быть очень дешевой операцией, чтобы разрешить вызов Equals () для как можно меньшего числа элементов.
И последний совет: не надейтесь, что GetHashCode () будет стабильным в течение нескольких запусков приложений . Многие типы .Net не гарантируют, что их хэш-коды останутся прежними после перезапуска, поэтому вы должны использовать только значение GetHashCode () для структур данных памяти.
источник
GetHashCode
выполнением выделения памяти, при условии, что это происходит только при первом использовании (при последующих вызовах просто возвращается кэшированный результат). Важно не то, что нужно избегать коллизий, а что нужно избегать «системных» коллизий. Если тип имеет дваint
поляoldX
иnewX
часто различаются по одному, хеш-значениеoldX^newX
будет назначать 90% таких хеш-значений записей 1, 2, 4 или 8. ИспользованиеoldX+newX
[непроверенная арифметика] может вызвать больше коллизий ...До недавнего времени мой ответ был бы очень близок к ответу Джона Скита. Тем не менее, я недавно начал проект, в котором использовались хеш-таблицы степени двойки, то есть хеш-таблицы, где размер внутренней таблицы равен 8, 16, 32 и т. Д. Есть веская причина для предпочтения размеров простых чисел, но есть Есть некоторые преимущества для степени двух размеров.
И это в значительной степени отстой. Поэтому после небольшого количества экспериментов и исследований я начал перефразировать свои хэши следующим образом:
А потом мой хэш-стол с степенью двойки больше не сосал.
Это беспокоило меня, хотя, потому что выше не должно работать. Или, точнее, он не должен работать, если оригинал не
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, но учтите, что приведенный выше код является его упрощенной версией.
Однако, поскольку он уже написан, его можно использовать проще:
Он также принимает начальные значения, поэтому, если вам нужно иметь дело с ненадежным вводом и хотите защитить от атак Hash DoS, вы можете установить начальное время на основе времени безотказной работы или аналогичного, а также сделать результаты непредсказуемыми для злоумышленников:
* Большим сюрпризом в этом является то, что ручной метод ротации вернул
(x << n) | (x >> -n)
улучшенные вещи. Я был бы уверен, что дрожание указало бы на это для меня, но профилирование показало обратное.†
decimal
не является родным с точки зрения .NET, хотя это с C #. Проблема с этим состоит в том, что его собственнаяGetHashCode()
трактует точность как значимую, а собственнаяEquals()
- нет. Оба являются допустимыми, но не смешанными. При реализации своей собственной версии вам нужно выбрать одну или другую, но я не могу знать, что вы хотите.‡ Для сравнения. При использовании в строке SpookyHash на 64 битах значительно быстрее, чем
string.GetHashCode()
на 32 битах, что немного быстрее, чемstring.GetHashCode()
на 64 битах, что значительно быстрее, чем SpookyHash на 32 битах, хотя все еще достаточно быстро, чтобы быть разумным выбором.источник
long
значения для промежуточных результатов, а затем уменьшать конечный результат до значенияint
. Это кажется хорошей идеей? Меня беспокоит то, что кто-то использует, например, hash = (hash * 31) + nextField, тогда пары совпадающих значений будут влиять только на верхние 27 бит хеша. Разрешение вычисления распространяется на along
и завернутый материал минимизирует эту опасность..Update()
с несколькими значениями согласно ответу выше сделает свое дело.Это хороший:
А вот как это использовать:
источник
GetHashCode()
метод, поэтому вы всегда можете использовать метод сparams
параметром массива. Или я что-то здесь упускаю?h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);
Конечные шаги shift / xor ( имеют кодовую запятую: они не зависят ни от каких входных данных и выглядят для меня ужасно избыточными.Начиная с https://github.com/dotnet/coreclr/pull/14863 , существует новый способ генерации хеш-кодов, который очень прост! Просто пиши
Это сгенерирует качественный хеш-код без необходимости беспокоиться о деталях реализации.
источник
HashCode
изменения для corefx были объединены всего за пару часов до вашего комментария :) Тип планируется отправить в .NET Core 2.1.Вот еще одна свободная реализация алгоритма, опубликованная выше Джоном Скитом , но которая не включает в себя операции выделения или упаковки:
Применение:
Компилятор гарантирует, что
HashValue
он не вызывается с классом из-за ограничения общего типа. Но компилятор не поддерживается,HashObject
поскольку добавление универсального аргумента также добавляет операцию упаковки.источник
Вот мой упрощенный подход. Я использую классический шаблон для этого. Он безопасен для типов (без упаковки / распаковки), а также совместим с .NET 2.0 (без методов расширения и т. Д.).
Используется так:
А вот класс острых строителей:
источник
AddItems<T>(params T[] items)
метод чаще в классе помощника (чем вызовAddItem(T)
каждый раз).this.result * Prime2 * item.GetHashCode()
когда часто используетеthis.result * Prime2 + item.GetHashCode()
?AddItems<T>(params T[] items)
чаще, потому чтоtypeof(T1) != typeof(T2)
и т. Д.Пользователи ReSharper могут генерировать GetHashCode, Equals и другие с помощью
ReSharper -> Edit -> Generate Code -> Equality Members
.источник
Если у нас есть не более 8 свойств (надеюсь), здесь есть другая альтернатива.
ValueTuple
является структурой и, кажется, имеет твердое телоGetHashCode
реализацию.Это означает, что мы могли бы просто сделать это:
Давайте посмотрим на текущую реализацию .NET Core для
ValueTuple
sGetHashCode
.Это из
ValueTuple
:И это из
HashHelper
:На английском:
Было бы неплохо узнать больше о свойствах этого алгоритма хэш-кода ROL-5.
К сожалению, откладывать на
ValueTuple
себяGetHashCode
может не так быстро, как хотелось бы и ожидать. Этот комментарий в связанном обсуждении показывает, что прямой вызовHashHelpers.Combine
более производительный. С другой стороны, это внутреннее, поэтому нам пришлось бы копировать код, жертвуя большей частью того, что мы получили здесь. Кроме того, мы будем ответственны за то, что сначала запомнилиCombine
случайное семя. Я не знаю, каковы будут последствия, если мы пропустим этот шаг.источник
h1 >> 27
что 0 игнорирует его,h1 << 5
равно,h1 * 32
следовательно, он такой же, какh1 * 33 ^ h2
. Согласно этой странице , он называется «Модифицированный Бернштейн».Большая часть моей работы выполняется с подключением к базе данных, что означает, что все мои классы имеют уникальный идентификатор из базы данных. Я всегда использую идентификатор из базы данных для генерации хэш-кода.
источник
_id.GetHashCode
как цель ясна.Очень похоже на решение ночного кодера, за исключением того, что проще поднимать простые числа, если хотите.
PS: Это один из тех случаев, когда вы немного рвете, зная, что это может быть реорганизовано в один метод с 9 значениями по умолчанию, но это будет медленнее, поэтому вы просто закрываете глаза и пытаетесь забыть об этом.
источник
Я столкнулся с проблемой с плавающей запятой и десятичной дробью, используя реализацию, выбранную в качестве ответа выше.
Этот тест не пройден (с плавающей запятой; хэш-код такой же, хотя я переключил 2 значения, чтобы они были отрицательными):
Но этот тест проходит (с целыми числами):
Я изменил свою реализацию, чтобы не использовать GetHashCode для примитивных типов, и кажется, что она работает лучше
источник
unchecked
НЕ влияет наConvert.ToInt32
:uint
,long
,float
,double
иdecimal
все это может Переполнение здесь.Microsoft привела к нескольким способам хеширования ...
Я могу догадаться, что для нескольких больших int вы можете использовать это:
И то же самое для мультитипа : все преобразованные сначала в
int
использование, аGetHashCode()
затем значения int будут xor'ed, и результатом будет ваш хеш.Для тех, кто использует хэш в качестве идентификатора (я имею в виду уникальное значение), хэш естественно ограничен количеством цифр, я думаю, что это было 5 байтов для алгоритма хеширования, по крайней мере, MD5.
Вы можете превратить несколько значений в хэшированное значение, и некоторые из них будут одинаковыми, поэтому не используйте его в качестве идентификатора. (возможно, однажды я собираюсь использовать ваш компонент)
источник
Это статический вспомогательный класс, который реализует реализацию Джоша Блоха; и обеспечивает явные перегрузки для «предотвращения» бокса, а также для реализации хеша специально для длинных примитивов.
Вы можете передать сравнение строк, соответствующее вашей реализации equals.
Поскольку выход Hash всегда является int, вы можете просто связывать вызовы Hash.
источник
HashKeysAndValues
Метод был зафиксирован: он вызываетHashKeyAndValue
.Если вы хотите, чтобы polyfill
HashCode
отnetstandard2.1
Примечание: если используется с
struct
, он будет выделять память из-за боксаисточник