2015-07-28 25 views
10

Per risolvere this question, ho giocato con una struttura personalizzata che implementa il protocollo Hashable. Sto provando a vedere quante volte viene chiamato il sovraccarico dell'equivalente operatore (==) a seconda che si verifichi una collisione hash o meno quando si popola un Dictionary.In che modo il dizionario utilizza il protocollo Equatable in Swift?

Aggiornamento

@matt scritto un esempio molto più pulito di una struttura personalizzata che implementa il protocollo hashable e mostra come spesso hashValue e == vieni chiamato. Sto copiando his code qui sotto. Per vedere il mio esempio originale, controlla lo 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)) 
} 

Questo produce i risultati:

/* 
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 
*/ 

Dal hashable utilizza equatable di differenziare collisioni hash (presumo comunque), mi aspetterei func ==() solo per essere chiamato in caso di collisioni hash. Tuttavia, non ci sono mai collisioni di hash nell'esempio di @ matt sopra, eppure == viene ancora chiamato. Nei miei altri esperimenti che hanno forzato le collisioni di hash (vedi la cronologia delle modifiche di questa domanda), == sembrava essere chiamato un numero casuale di volte.

Cosa sta succedendo qui?

+1

Detesto dare una risposta o un commento, ma questo è un dettaglio di implementazione interno di Swift. Possono ottimizzare il tipo come vogliono a patto che sia conforme alle API del dizionario documentate. E i documenti non forniscono alcuna garanzia sulla frequenza con cui le chiavi verranno controllate per l'uguaglianza - richiedono semplicemente che forniate quell'interfaccia '=='. Immagino che lo sapremo più avanti quest'anno quando Swift diventerà open source. Inoltre, vedi il mio commento simile alla tua [altra domanda] (http://stackoverflow.com/questions/31664159/how-to-handle-hash-collisions-for-dictionaries-in-swift). – justinpawela

+2

Ecco un test più semplice (credo): https://gist.github.com/mattneub/430fef70e3496f5ce6917aa35c98f419 L'output rende molto esplicito quante volte vengono chiamati 'hashValue' e' == 'per ogni inserimento. – matt

+0

@matt, sì, questo è un test molto più semplice e chiaro. Ma ora sono più confuso di quanto pensassi. Il tuo esempio non ha collisioni hash, giusto? E tuttavia '==' viene ancora chiamato. Prima ero convinto che '==' venisse chiamato solo per trattare i casi di collisione dell'hash. – Suragch

risposta

7

Beh, c'è la risposta:

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

quello che succede:

  • Abbiamo hash un valore una sola volta all'inserimento.
  • Non utilizziamo gli hash per il confronto di elementi, solo ==. Utilizzare gli hash per il confronto è ragionevole solo se si memorizzano gli hash, ma significa più utilizzo della memoria per ogni dizionario. Un compromesso che è necessario valutare .
  • Proviamo ad inserire l'elemento prima di valutare se il dizionario può adattarsi a quell'elemento. Questo perché l'elemento potrebbe già essere nel dizionario , nel qual caso non è necessaria altra capacità.
  • Quando ridimensioniamo il dizionario, dobbiamo riordinare tutto, perché non abbiamo archiviato gli hash.

Quindi quello che stai vedendo è:

  • un hash della chiave di ricerca
  • s 'alcuni == (alla ricerca di uno spazio)
  • hash di ogni elemento della collezione (ridimensiona)
  • un hash della chiave di ricerca (in realtà totalmente dispendioso, ma non un grosso problema considerando che succede solo dopo una riallocazione O)
  • alcuni == 's (ricerca di uno spazio in th e nuovo buffer)

Abbiamo sbagliato tutto. Non usano affatto gli hash - solo== - per decidere se si tratta di una chiave distinta. E poi c'è un secondo giro di chiamate nella situazione in cui la collezione è cresciuta.

+1

Quindi qual è il punto di farci implementare il protocollo Hashable quando vogliamo rendere una struttura personalizzata utilizzabile in un dizionario? Sembra che il protocollo Equitable sia sufficiente. – Suragch

+2

@Suragch Fondamentalmente la tabella hash viene utilizzata per _fetching_.Ma qui, siamo _storing_, che è una palla di cera completamente diversa. – matt

+0

* Quando ridimensioniamo il dizionario, dobbiamo riordinare tutto, perché non abbiamo archiviato gli hash. * Puoi spiegarlo un po 'di più? @Suragch Questo si applica all'esempio nella domanda – Honey

10

Sto copiando la mia risposta da bugs.swift.org qui. Parla di Sets ma i dettagli si applicano ai Dizionari allo stesso modo.

Nelle raccolte con hash, le collisioni possono verificarsi ogni volta che il numero di bucket è inferiore allo spazio delle chiavi. Quando crei un nuovo Set senza specificare una capacità minima, il set potrebbe avere un solo bucket, quindi quando inserisci il secondo elemento, si verifica una collisione. Il metodo di inserimento deciderà quindi se aumentare la memoria, usando qualcosa chiamato fattore di carico. Se la memoria è stata sviluppata, gli elementi esistenti devono essere migrati sul nuovo buffer di archiviazione. Ecco quando vedi tutte le chiamate extra a hashValue quando inserisci 4.

Il motivo per cui vedi ancora più chiamate a == di quanto ti aspetteresti se il numero di bucket è uguale o superiore al numero di elementi ha a che fare con un dettaglio di implementazione del calcolo dell'indice del bucket. I bit di hashValue vengono mixati o "shuffled" prima dell'operazione di modulo. Questo serve a ridurre le collisioni eccessive per i tipi con algoritmi di hash errati.

+2

Grazie. Quindi, in un caso come questo, è mia responsabilità aumentare la capacità della raccolta prima di aggiungerla? – matt

+1

Se si conosce il numero di elementi prima del tempo, passerei sempre quel valore all'inizializzatore. Se è possibile risparmiare un sacco di cicli. – robinkunde

+0

Anche se questa risposta è ancora molto utile, almeno temporaneamente la deseleziono come risposta accettata a causa delle nuove informazioni a cui si riferisce @matt. – Suragch