2015-07-28 4 views
10

Чтобы решить this question, я играл с настраиваемой структурой, которая реализует протокол Hashable. Я пытаюсь увидеть, сколько раз перегрузка оператора эквивалентности (==) вызывается в зависимости от наличия хеш-коллизии или нет при заполнении Dictionary.Как словарь использует протокол Equatable в Swift?

Update

@matt написал более чистый пример пользовательской структуры, которая реализует Hashable протокол и показывает, как часто hashValue и == дозвонились. Я копирую his code ниже. Чтобы увидеть мой оригинальный пример, посмотрите edit history.

struct S : Hashable { 
    static func ==(lhs:S,rhs:S) -> Bool { 
     print("called == for", lhs.id, rhs.id) 
     return lhs.id == rhs.id 
    } 
    let id : Int 
    var hashValue : Int { 
     print("called hashValue for", self.id) 
     return self.id 
    } 
    init(_ id:Int) {self.id = id} 
} 
var s = Set<S>() 
for i in 1...5 { 
    print("inserting", i) 
    s.insert(S(i)) 
} 

Это дает результаты:

/* 
inserting 1 
called hashValue for 1 
inserting 2 
called hashValue for 2 
called == for 1 2 
called hashValue for 1 
called hashValue for 2 
inserting 3 
called hashValue for 3 
inserting 4 
called hashValue for 4 
called == for 3 4 
called == for 1 4 
called hashValue for 2 
called hashValue for 3 
called hashValue for 1 
called hashValue for 4 
called == for 3 4 
called == for 1 4 
inserting 5 
called hashValue for 5 
*/ 

Поскольку Hashable использует Equatable дифференцировать хэш столкновений (я предполагаю, что в любом случае), я ожидал бы func ==() только называться, когда есть хэш столкновения. Тем не менее, никаких столкновений с хэшем вообще нет в примере с приведенным выше примером @ matt, и все же == все еще вызывается. В других моих экспериментах, вызывающих столкновения хэшей (см. Историю изменений этого вопроса), ==, казалось, называлось случайным числом раз.

Что здесь происходит?

+1

Ненавижу давать это как ответ или даже комментарий, но это краткая внутренняя деталь реализации. Они могут оптимизировать тип, хотя они хотят, если он соответствует документированным API-интерфейсам словаря. И в документах нет никаких гарантий того, как часто ключи будут проверяться на равенство - они просто требуют, чтобы вы обеспечивали этот интерфейс '=='. Думаю, мы узнаем об этом позже в этом году, когда Swift станет открытым исходным кодом. Также см. Мой аналогичный комментарий к вашему [другому вопросу] (http://stackoverflow.com/questions/31664159/how-to-handle-hash-collisions-for-dictionaries-in-swift). – justinpawela

+2

Вот более простой тест (я думаю): https://gist.github.com/mattneub/430fef70e3496f5ce6917aa35c98f419 Результат делает очень явным, сколько раз вызывается 'hashValue' и' == 'для каждой вставки. – matt

+0

@matt, да, это гораздо более простой и понятный тест. Но теперь я более смущен, чем я думал. В вашем примере нет хеш-коллизий, не так ли? И все же '==' все еще вызван. Я был в предположении, что '==' получил вызов только для случаев хеш-коллизий. – Suragch

ответ

7

Ну, есть свой ответ:

https://bugs.swift.org/browse/SR-3330?focusedCommentId=19980&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-19980

Что происходит на самом деле:

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

Так что вы видите, это:

  • одна хэш ключа поиска
  • некоторые == 's (поиск пространства)
  • хэши каждого элемента в коллекции (изменение размера)
  • один хэш ключа поиска (на самом деле полностью расточительный, но не большой расчет, учитывая, что это происходит только после перераспределения O)
  • some == (поиск пространства в e новый буфер)

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

+1

Итак, в чем смысл заставить нас реализовать протокол Hashable вообще, когда мы хотим создать пользовательскую структуру, используемую в словаре? Похоже, что Equatable protocol будет достаточно. – Suragch

+2

@Suragch В основном хэш-таблица используется для _fetching_.Но здесь мы _storing_, который представляет собой совершенно другой шар воска. – matt

+0

* Когда мы изменяем размер словаря, нам нужно все перефразировать, потому что мы не хранили хэши. * Можете ли вы объяснить это немного больше? @Suragch Это относится к примеру в вопросе – Honey

10

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

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

Причина, по которой вы все еще видите больше вызовов ==, чем вы ожидали бы, если количество ковшей равно или больше числа элементов имеет отношение к детализации реализации вычисления индекса ковша. Биты hashValue смешиваются или «перетасовываются» перед операцией modulo. Это должно сократить чрезмерные столкновения для типов с плохими алгоритмами хеширования.

+2

Спасибо. Итак, в таком случае, это моя обязанность расширять возможности коллекции, прежде чем добавлять к ней? – matt

+1

Если вы знаете количество элементов загодя, я всегда передавал бы это значение инициализатору. Если это может сэкономить много циклов. – robinkunde

+0

Хотя этот ответ по-прежнему очень полезен, я, по крайней мере, временно снимаю его как принятый ответ из-за новой информации, на которую ссылается @matt. – Suragch

Смежные вопросы