Почему ValueType.GetHashCode () реализован как есть? - PullRequest
22 голосов
/ 01 октября 2010

С ValueType.cs

**Action: Our algorithm for returning the hashcode is a little bit complex. We look 
**        for the first non-static field and get it's hashcode.  If the type has no 
**        non-static fields, we return the hashcode of the type. We can't take the
**        hashcode of a static member because if that member is of the same type as 
**        the original type, we'll end up in an infinite loop.

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

Пример (к / п от Linqpad):

void Main()
{
    var kvp1 = new KeyValuePair<string, string>("foo", "bar");
    var kvp2 = new KeyValuePair<string, string>("foo", "baz");

    // true
    (kvp1.GetHashCode() == kvp2.GetHashCode()).Dump();
}

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

Ответы [ 5 ]

43 голосов
/ 01 октября 2010

Реальная реализация ValueType.GetHashCode () не совсем соответствует комментарию.Он имеет две версии алгоритма, быстрый и медленный.Сначала проверяется, содержит ли структура какие-либо члены ссылочного типа и есть ли заполнение между полями.Заполнение - это пустое место в значении структуры, которое создается, когда компилятор JIT выравнивает поля.Есть заполнение в структуре, которая содержит bool и int (3 байта), но нет заполнения, когда оно содержит int и int, они плотно сочетаются друг с другом.

Без ссылки и без заполнения, она может делать быструю версию, так как каждыйбит в значении структуры - это бит, принадлежащий значению поля.Это просто xors 4 байта за раз.Вы получите «хороший» хеш-код, который учитывает всех участников.Многие простые типы структур в .NET Framework ведут себя таким образом, как Point и Size.

Не пройдя этот тест, он делает медленную версию, моральный эквивалент отражения.Это то, что вы получаете, ваш KeyValuePair <> содержит ссылки.И этот проверяет только первое поле кандидата, как говорится в комментарии.Это, безусловно, идеальная оптимизация, позволяющая избежать слишком длительного сжигания.

Да, неприятные детали и малоизвестные.Обычно это обнаруживается, когда кто-то замечает, что его коллекционный код сосет грязь.

Еще одна мучительная деталь: в быстрой версии есть ошибка, которая байт, когда структура содержит поле типа decimal.Значения 12m и 12.0m логически равны, но они не имеют одинаковую битовую комбинацию.GetHashCode () скажет, что они не равны.Уч.

33 голосов
/ 01 октября 2010

ОБНОВЛЕНИЕ: Этот ответ был (частично) основой статьи в блоге, которую я написал, в которой более подробно рассматриваются характеристики дизайна GetHashcode. Спасибо за интересный вопрос!


Я не реализовал это, и я не говорил с людьми, которые сделали. Но я могу указать на несколько вещей.

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

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

Так что же "приятно иметь" в дополнение к этому контракту? Хорошая реализация хеш-кода должна быть:

1) Быстро. Очень быстро! Помните, весь смысл хеш-кода в первую очередь заключается в том, чтобы быстро найти относительно пустой слот в хеш-таблице. Если вычисление O (1) хеш-кода на практике медленнее, чем время O (n), затрачиваемое на наивный поиск, то решение с использованием хеш-кода является чистым убытком.

2) Хорошо распределено по пространству 32-битных целых чисел для данного распределения входов. Чем хуже распределение по целым числам, тем больше похоже на наивный линейный поиск хэш-таблицы.

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

Распространенным предложением является "хэшировать все поля, а затем XOR объединить полученные хеш-коды". Но это напрашивается на вопрос; XORing двух 32-битных целых дает хорошее распределение, только если сами входы очень хорошо распределены и не связаны друг с другом, и это маловероятный сценарий:

// (Updated example based on good comment!)
struct Control
{
    string name;
    int x;
    int y;
}

Какова вероятность того, что x и y хорошо распределены по всему диапазону 32-битных целых чисел? Очень низкий. Шансы намного лучше, если они оба маленькие и близки друг к другу , и в этом случае кеширование их хэш-кодов вместе делает вещи хуже , а не лучше . xoring вместе целые числа, которые близки друг к другу, обнуляют большинство битов.

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

По сути, мы здесь находимся в том, что пользователь сам не предоставил реализацию хеш-кода; либо им все равно, либо они не ожидают, что этот тип когда-либо будет использоваться в качестве ключа в хэш-таблице. Учитывая, что у вас нет семантической информации о типе, что лучше всего делать? Лучшее, что можно сделать, это то, что быстро и дает хорошие результаты в большинстве случаев.

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

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

Сравните это с дизайном анонимных типов в C #.С анонимными типами мы do знаем, что весьма вероятно, что тип используется в качестве ключа к таблице.Мы делаем знаем, что весьма вероятно, что будет избыточность между экземплярами анонимных типов (потому что они являются результатом декартового произведения или другого объединения).И поэтому мы объединяем хеш-коды всех полей в один хеш-код.Если это приводит к плохой производительности из-за избыточного числа вычисляемых хеш-кодов, вы можете использовать собственный номинальный тип, а не анонимный.

7 голосов
/ 01 октября 2010

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

В частности:

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

Теперь я не говорю, что реализация ValueType - отличная идея - она ​​может привести к снижению производительности разными способами ... но я недумаю, что это на самом деле сломан .

3 голосов
/ 01 октября 2010

Ну, есть плюсы и минусы любой реализации GetHashCode().Это, конечно, вещи, которые мы взвешиваем при реализации наших собственных, но в случае ValueType.GetHashCode() есть особая трудность в том, что они не имеют большой информации о том, какими будут фактические детали конкретного типа.Конечно, это часто случается с нами, когда мы создаем абстрактный класс или класс, предназначенный для использования в качестве базы классов, которые добавят намного больше в плане состояния, но в этих случаях у нас есть очевидное решение - просто использовать реализацию по умолчанию.object.GetHashCode(), если производный класс не захочет переопределить его там.

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

Итак, для метода Equals() реализация очевидна: проверьте, что оба объекта имеют одинаковый тип, и, если это так, проверьте также, что все поляравны (на самом деле есть оптимизация, которая дв некоторых случаях выполняется побитовое сравнение, но это оптимизация той же базовой идеи).

Что делать для GetHashCode()?Идеального решения просто не существует.Одна вещь, которую они могли бы сделать, это что-то вроде mult-then-add или shift-then-xor для каждого поля.Это, вероятно, дало бы довольно хороший хэш-код, но могло бы быть дорогим, если бы было много полей (не берите в голову, что не рекомендуется иметь типы значений, у которых есть много полей, разработчик должен учитывать, что они все еще могут, и действительномогут даже быть случаи, когда это имеет смысл, хотя я, честно говоря, не могу представить время, когда оно имеет смысл и имеет смысл его хешировать).Если бы они знали, что некоторые поля редко различались между экземплярами, они могли бы игнорировать эти поля и при этом иметь довольно хороший хэш-код, при этом будучи достаточно быстрым.Наконец, они могут игнорировать большинство полей и надеяться, что те поля, которые они не игнорируют, в большинстве случаев будут меняться.Они выбрали самую крайнюю версию последней.

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

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

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

Так что, не очень, но ничего не было бы идеально.Это говорит о том, что всегда нужно учитывать обе стороны, что означает «равенство» при использовании объекта в качестве ключа.Это легко исправить в вашем случае с помощью:

public class KVPCmp<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>, IEqualityComparer
{
  bool IEqualityComparer.Equals(object x, object y)
  {
      if(x == null)
        return y == null;
      if(y == null)
        return false;
      if(!(x is KeyValuePair<TKey, TValue>) || !(y is KeyValuePair<TKey, TValue>))
        throw new ArgumentException("Comparison of KeyValuePairs only.");
      return Equals((KeyValuePair<TKey, TValue>) x, (KeyValuePair<TKey, TValue>) y);
  }
  public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
  {
      return x.Key.Equals(y.Key) && x.Value.Equals(y.Value);
  }
  public int GetHashCode(KeyValuePair<TKey, TValue> obj)
  {
      int keyHash = obj.GetHashCode();
      return ((keyHash << 16) | (keyHash >> 16)) ^ obj.Value.GetHashCode();
  }
  public int GetHashCode(object obj)
  {
      if(obj == null)
        return 0;
      if(!(obj is KeyValuePair<TKey, TValue>))
       throw new ArgumentException();
      return GetHashCode((KeyValuePair<TKey, TValue>)obj);
  }
}

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

0 голосов
/ 02 октября 2010

Спасибо всем за очень, очень информативные ответы.Я знал, что в этом решении должно быть какое-то обоснование, но хотелось бы, чтобы оно было лучше задокументировано.Я не могу использовать v4 фреймворка, поэтому нет Tuple<>, и это было основной причиной, по которой я решил использовать KeyValuePair struct.Но я предполагаю, что нет никаких сокращающихся углов, и я должен буду катиться самостоятельно.Еще раз спасибо всем.

...