C'è qualche vantaggio nell'usare map over unordered_map in caso di chiavi banali?


371

Un recente discorso unordered_mapin C ++ mi ha fatto capire che dovrei usare unordered_mapper la maggior parte dei casi in cui ho usato mapprima, a causa dell'efficienza della ricerca ( O ammortizzato (1) vs. O (log n) ). La maggior parte delle volte che utilizzo una mappa, utilizzo intostd::string come chiave; quindi, non ho problemi con la definizione della funzione hash. Più ci pensavo, più mi rendevo conto che non riesco a trovare alcun motivo per usare un std::mapover a std::unordered_mapnel caso di chiavi con tipi semplici: ho dato un'occhiata alle interfacce e non ho trovato differenze significative che potrebbero influire sul mio codice.

Di qui la domanda: c'è una vera ragione per usare std::mapsopra std::unordered_mapin caso di tipi semplici come inte std::string?

Lo sto chiedendo da un punto di vista strettamente programmatico: so che non è completamente considerato standard e che potrebbe creare problemi con il porting.

Inoltre, mi aspetto che una delle risposte corrette potrebbe essere "è più efficiente per insiemi di dati più piccoli" a causa di un sovraccarico minore (è vero?) - quindi vorrei limitare la domanda ai casi in cui la quantità di keys non è banale (> 1 024).

Modifica: duh, ho dimenticato l'ovvio (grazie GMan!) - Sì, le mappe sono ordinate ovviamente - Lo so, e sto cercando altri motivi.


22
Mi piace porre questa domanda nelle interviste: "Quando è meglio l'ordinamento rapido rispetto all'ordinamento a bolle?" La risposta alla domanda fornisce informazioni sull'applicazione pratica della teoria della complessità e non solo semplici affermazioni in bianco e nero come O (1) sono migliori di O (n) o O (k) equivalgono a O (logn) ecc. ..

42
@Beh, penso che volevi dire "quando la bolla è meglio della rapida": P
Kornel Kisielewicz

2
Un puntatore intelligente sarebbe una chiave banale?
Thom

Ecco uno dei casi in cui mappa è quella vantaggiosa: stackoverflow.com/questions/51964419/...
anilbey

Risposte:


399

Non dimenticare che mapmantiene ordinati i suoi elementi. Se non puoi rinunciare, ovviamente non puoi usarlo unordered_map.

Qualcos'altro da tenere a mente è che unordered_mapgeneralmente usa più memoria. mapha solo alcuni puntatori per la pulizia della casa e memoria per ogni oggetto. Al contrario, unordered_mapha un grande array (questi possono diventare piuttosto grandi in alcune implementazioni) e quindi memoria aggiuntiva per ogni oggetto. Se è necessario essere consapevoli della memoria, mapdovrebbe risultare migliore, perché manca l'array di grandi dimensioni.

Quindi, se hai bisogno di un semplice recupero di ricerca, direi che unordered_mapè la strada da percorrere. Ma ci sono sempre dei compromessi, e se non puoi permetterli, non puoi usarli.

Proprio per esperienza personale, ho riscontrato un enorme miglioramento delle prestazioni (misurato, ovviamente) durante l'utilizzo unordered_mapanzichémap in una tabella di ricerca delle entità principali.

D'altra parte, ho scoperto che era molto più lento inserire e rimuovere ripetutamente elementi. È ottimo per una raccolta di elementi relativamente statica, ma se stai facendo tonnellate di inserzioni ed eliminazioni, sembra che l'hashing + il bucket aumentino. (Nota, questo è stato su molte iterazioni.)


3
Un'altra cosa sulla grande (r) proprietà del blocco di memoria di unordered_map vs. map (o vector vs list), l'heap di processo predefinito (che parla Windows qui) è serializzato. Allocare blocchi (piccoli) in grandi quantità in un'applicazione multithread è molto costoso.
RUGGITO

4
RA: Puoi in qualche modo controllarlo con il tuo tipo di allocatore combinato con qualsiasi contenitore, se pensi che sia importante per un particolare programma.

9
Se conosci la dimensione del unordered_mape lo riservi all'inizio - paghi ancora una penalità per molti inserimenti? Supponiamo che tu stia inserendo una sola volta quando hai creato la tabella di ricerca, e in seguito leggi solo da essa.
Thom

3
@thomthom Per quanto ne so, non ci dovrebbero essere penalità in termini di prestazioni. Il motivo per cui le prestazioni subiscono un colpo è dovuto al fatto che se l'array diventa troppo grande, farà una revisione di tutti gli elementi. Se chiami la riserva, potenzierai potenzialmente gli elementi esistenti ma se lo chiami all'inizio, allora non ci dovrebbe essere alcuna penalità, almeno secondo cplusplus.com/reference/unordered_map/unordered_map/reserve
Richard Fung

6
Sono abbastanza sicuro che per quanto riguarda la memoria è il contrario. Supponendo il fattore di carico predefinito 1,0 per un contenitore non ordinato: hai un puntatore per elemento per il bucket e un puntatore per elemento per l'elemento next-in-bucket, quindi finisci con due puntatori più i dati per ogni elemento. Per un contenitore ordinato, invece, un'implementazione tipica dell'albero RB avrà: tre puntatori (sinistra / destra / padre) più un bit di colore che a causa dell'allineamento richiede una quarta parola. Sono quattro puntatori più dati per ogni elemento.
Yakov Galka,

126

Se vuoi confrontare la velocità della tua std::mape delle tue std::unordered_mapimplementazioni, puoi usare il progetto sparsehash di Google che ha un programma time_hash_map per cronometrarle. Ad esempio, con gcc 4.4.2 su un sistema Linux x86_64

$ ./time_hash_map
TR1 UNORDERED_MAP (4 byte objects, 10000000 iterations):
map_grow              126.1 ns  (27427396 hashes, 40000000 copies)  290.9 MB
map_predict/grow       67.4 ns  (10000000 hashes, 40000000 copies)  232.8 MB
map_replace            22.3 ns  (37427396 hashes, 40000000 copies)
map_fetch              16.3 ns  (37427396 hashes, 40000000 copies)
map_fetch_empty         9.8 ns  (10000000 hashes,        0 copies)
map_remove             49.1 ns  (37427396 hashes, 40000000 copies)
map_toggle             86.1 ns  (20000000 hashes, 40000000 copies)

STANDARD MAP (4 byte objects, 10000000 iterations):
map_grow              225.3 ns  (       0 hashes, 20000000 copies)  462.4 MB
map_predict/grow      225.1 ns  (       0 hashes, 20000000 copies)  462.6 MB
map_replace           151.2 ns  (       0 hashes, 20000000 copies)
map_fetch             156.0 ns  (       0 hashes, 20000000 copies)
map_fetch_empty         1.4 ns  (       0 hashes,        0 copies)
map_remove            141.0 ns  (       0 hashes, 20000000 copies)
map_toggle             67.3 ns  (       0 hashes, 20000000 copies)

2
Sembra che una mappa non ordinata batte la mappa sulla maggior parte delle operazioni. Evento all'inserimento ...
Michele IV,

7
sparsehash non esiste più. è stato eliminato o rimosso.
Utente 9102d82

1
@ User9102d82 Ho modificato la domanda per fare riferimento a un link WaybackMachine .
andreee

Solo per assicurarsi che altri notino anche gli altri numeri oltre al tempo: quei test sono stati fatti con oggetti / strutture di dati a 4 byte aka int. Se memorizzi qualcosa che richiede un hash più pesante o è più grande (rendendo più pesanti le operazioni di copia), la mappa standard potrebbe rapidamente avere un vantaggio!
AlexGeorg

82

Rispetterei più o meno lo stesso punto sollevato da GMan: a seconda del tipo di utilizzo, std::mappuò essere (e spesso lo è) più veloce di std::tr1::unordered_map(usando l'implementazione inclusa in VS 2008 SP1).

Ci sono alcuni fattori complicanti da tenere a mente. Ad esempio, in std::map, stai confrontando le chiavi, il che significa che hai sempre guardato abbastanza all'inizio di una chiave per distinguere tra i rami secondari destro e sinistro dell'albero. Nella mia esperienza, quasi l'unica volta che guardi un'intera chiave è se stai usando qualcosa come int che puoi confrontare in una singola istruzione. Con un tipo di chiave più tipico come std :: string, spesso si confrontano solo pochi caratteri.

Una funzione hash decente, al contrario, guarda sempre l' intero tasto. IOW, anche se la ricerca della tabella è una complessità costante, l'hash stesso ha una complessità approssimativamente lineare (sebbene sulla lunghezza della chiave, non sul numero di elementi). Con stringhe lunghe come chiavi, si std::mappotrebbe finire una ricerca prima unordered_mapancora di iniziare la ricerca.

In secondo luogo, mentre esistono diversi metodi per ridimensionare le tabelle hash, la maggior parte sono piuttosto lente - al punto che, a meno che le ricerche non siano considerevolmente più frequenti degli inserimenti e delle eliminazioni, std :: map sarà spesso più veloce di std::unordered_map.

Ovviamente, come ho già detto nel commento alla tua domanda precedente, puoi anche usare una tavola di alberi. Questo ha sia vantaggi che svantaggi. Da un lato, limita il caso peggiore a quello di un albero. Inoltre, consente l'inserimento e l'eliminazione rapidi, perché (almeno quando l'ho fatto) ho usato una tabella di dimensioni fisse. L'eliminazione di tutti i ridimensionamenti delle tabelle ti consente di rendere la tua tabella hash molto più semplice e in genere più veloce.

Un altro punto: i requisiti per hashing e mappe basate su alberi sono diversi. L'hashing ovviamente richiede una funzione hash e un confronto di uguaglianza, in cui le mappe ordinate richiedono un confronto minore. Naturalmente l'ibrido che ho citato richiede entrambi. Naturalmente, per il caso comune di usare una stringa come chiave, questo non è davvero un problema, ma alcuni tipi di chiavi sono adatti all'ordinamento meglio dell'hashing (o viceversa).


2
Il ridimensionamento dell'hash può essere attenuato dalla dynamic hashingtecnica, che consiste nell'avere un periodo di transizione in cui ogni volta che si inserisce un elemento, è necessario ripassare anche kaltri elementi. Naturalmente, significa che durante la transizione devi cercare 2 tabelle diverse ...
Matthieu M.

2
"Con stringhe lunghe come chiavi, una std :: map potrebbe terminare una ricerca prima che un unordered_map possa iniziare la sua ricerca." - se la chiave non è presente nella raccolta. Se è presente, ovviamente è necessario confrontare l'intera lunghezza per confermare l'incontro. Allo stesso modo, unordered_mapdeve confermare una corrispondenza hash con un confronto completo, quindi tutto dipende da quali parti del processo di ricerca stai contrastando.
Steve Jessop,

2
di solito è possibile sostituire la funzione hash in base alla conoscenza dei dati. per esempio se le tue stringhe lunghe variano più negli ultimi 20 byte che nei primi 100,
esegui

56

Sono stato incuriosito dalla risposta di @Jerry Coffin, che ha suggerito che la mappa ordinata avrebbe mostrato aumenti delle prestazioni su lunghe stringhe, dopo un po 'di sperimentazione (che può essere scaricata da pastebin ), ho scoperto che questo sembra valere solo per le raccolte di stringhe casuali, quando la mappa viene inizializzata con un dizionario ordinato (che contiene parole con notevoli quantità di prefisso-sovrapposizione), questa regola si interrompe, presumibilmente a causa della maggiore profondità dell'albero necessaria per recuperare il valore. I risultati sono mostrati di seguito, la prima colonna numerica è il tempo di inserimento, il secondo è il tempo di recupero.

g++ -g -O3 --std=c++0x   -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
gmurphy@interloper:HashTests$ ./stdtests
# 1st number column is insert time, 2nd is fetch time
 ** Integer Keys ** 
 unordered:      137      15
   ordered:      168      81
 ** Random String Keys ** 
 unordered:       55      50
   ordered:       33      31
 ** Real Words Keys ** 
 unordered:      278      76
   ordered:      516     298

2
Grazie per il test. Per essere sicuro che non stiamo misurando il rumore, l'ho cambiato per fare ogni operazione molte volte (e ho inserito il contatore invece di 1 nella mappa). L'ho eseguito su un numero diverso di chiavi (da 2 a 1000) e fino a ~ 100 chiavi nella mappa, in std::mapgenere supera le prestazioni std::unordered_map, in particolare per le chiavi intere ma ~ 100 chiavi sembra perdere il bordo e std::unordered_mapiniziare a vincere. Inserendo una sequenza già ordinata in un std::mapè molto male, otterrai lo scenario peggiore (O (N)).
Andreas Magnusson,

30

Vorrei solo sottolineare che ... ci sono molti tipi di unordered_maps.

Cerca l' articolo di Wikipedia sulla mappa hash. A seconda dell'implementazione utilizzata, le caratteristiche in termini di ricerca, inserimento ed eliminazione potrebbero variare in modo significativo.

E questo è ciò che mi preoccupa di più con l'aggiunta di unordered_mapSTL: dovranno scegliere un'implementazione particolare poiché dubito che andranno giù per la Policystrada, e quindi saremo bloccati con un'implementazione per l'uso medio e niente per gli altri casi ...

Ad esempio, alcune mappe di hash hanno un rehash lineare, dove invece di rimodellare l'intera mappa di hash in una volta, una porzione viene ridisegnata ad ogni inserimento, il che aiuta ad ammortizzare il costo.

Un altro esempio: alcune mappe hash usano un semplice elenco di nodi per un bucket, altri usano una mappa, altri non usano nodi ma trovano lo slot più vicino e infine alcuni useranno un elenco di nodi ma lo riordinano in modo che l'ultimo elemento accessibile è nella parte anteriore (come una cosa da cache).

Quindi al momento tendo a preferire il std::mapo forse a loki::AssocVector(per set di dati congelati).

Non fraintendetemi, mi piacerebbe usare il std::unordered_mape potrei in futuro, ma è difficile "fidarsi" della portabilità di un tale contenitore quando si pensa a tutti i modi di implementarlo e alle varie prestazioni che ne risultano di questo.


17
+1: punto valido - la vita era più facile quando stavo usando la mia implementazione - almeno sapevo dove faceva schifo:>
Kornel Kisielewicz

25

Differenze significative che in realtà non sono state adeguatamente menzionate qui:

  • mapmantiene stabili gli iteratori su tutti gli elementi, in C ++ 17 puoi persino spostare gli elementi dall'uno mapall'altro senza invalidare gli iteratori (e se correttamente implementati senza alcuna potenziale allocazione).
  • map i tempi per le singole operazioni sono in genere più coerenti poiché non hanno mai bisogno di grandi allocazioni.
  • unordered_mapl'uso std::hashcome implementato in libstdc ++ è vulnerabile a DoS se alimentato con input non attendibile (usa MurmurHash2 con un seme costante - non che il seeding sarebbe davvero utile, vedi https://emboss.github.io/blog/2012/12/14/ break-mormorio-hash-flooding-dos-reloaded / ).
  • L'ordinamento consente ricerche di intervallo efficienti, ad esempio iterare su tutti gli elementi con chiave ≥ 42.

14

Le tabelle hash hanno costanti più elevate rispetto alle comuni implementazioni delle mappe, che diventano significative per i piccoli contenitori. La dimensione massima è 10, 100 o forse anche 1.000 o più? Le costanti sono le stesse di sempre, ma O (log n) è vicino a O (k). (Ricorda che la complessità logaritmica è ancora molto buona.)

Ciò che rende una buona funzione hash dipende dalle caratteristiche dei tuoi dati; quindi se non ho intenzione di guardare una funzione hash personalizzata (ma posso sicuramente cambiare idea in seguito, e facilmente dato che ho digitato dannatamente vicino a tutto) e anche se i valori predefiniti sono scelti per funzionare decentemente per molte fonti di dati, trovo l'ordine ordinato la natura della mappa è abbastanza per aiutarmi inizialmente che per impostazione predefinita continuo a mappare piuttosto che una tabella hash in quel caso.

Inoltre, non devi nemmeno pensare a scrivere una funzione hash per altri tipi (di solito UDT) e scrivere solo op <(che vuoi comunque).


@Roger, conosci la quantità approssimativa di elementi su cui unordered_map è migliore? Probabilmente scriverò un test per questo, comunque ... (+1)
Kornel Kisielewicz

1
@Kornel: non ci vogliono molti; i miei test erano con circa 10.000 elementi. Se vogliamo un grafico davvero accurato, è possibile esaminare un'implementazione di mape una unordered_map, con determinate piattaforme e determinate dimensioni della cache, e fare un'analisi complessa. : P
GManNickG

Dipende dai dettagli di implementazione, dai parametri di messa a punto in fase di compilazione (facile da supportare se si sta scrivendo la propria implementazione) e persino dalla macchina specifica utilizzata per i test. Proprio come per gli altri container, il comitato stabilisce solo i requisiti generali.

13

Le ragioni sono state fornite in altre risposte; eccone un altro.

Le operazioni std :: map (albero binario bilanciato) sono ammortizzate O (log n) e nel caso peggiore O (log n). Le operazioni std :: unordered_map (tabella hash) sono ammortizzate O (1) e nel caso peggiore O (n).

In pratica, ciò accade che la tabella hash "singhiozza" di tanto in tanto con un'operazione O (n), che può o meno essere qualcosa che l'applicazione può tollerare. Se non può tollerarlo, preferiresti std :: map rispetto a std :: unordered_map.


12

Sommario

Supponendo che l'ordinamento non sia importante:

  • Se hai intenzione di creare una tabella di grandi dimensioni una volta e fare molte query, usa std::unordered_map
  • Se hai intenzione di costruire una piccola tabella (potrebbe avere meno di 100 elementi) e fare molte query, usa std::map. Questo perché si legge su di esso O(log n).
  • Se hai intenzione di cambiare molto tavolo allora potrebbe essere std::map una buona opzione.
  • In caso di dubbi, basta usare std::unordered_map.

Contesto storico

Nella maggior parte delle lingue, la mappa non ordinata (ovvero dizionari basati su hash) è la mappa predefinita, tuttavia in C ++ si ottiene la mappa ordinata come mappa predefinita. Come è successo? Alcune persone presumono erroneamente che il comitato C ++ abbia preso questa decisione nella loro saggezza unica, ma la verità è purtroppo più brutta di così.

E 'opinione diffusa ritiene che il C ++ è conclusa con mappa ordinata di default perché non ci sono troppi parametri su come possono essere attuate. D'altra parte, le implementazioni basate sull'hash hanno un sacco di cose di cui parlare. Quindi, per evitare blocchi nella standardizzazione, andavano d'accordo con la mappa ordinata. Intorno al 2005, molte lingue avevano già buone implementazioni dell'implementazione basata sull'hash e quindi era più facile per il comitato accettarne di nuove std::unordered_map. In un mondo perfetto, non std::mapsarebbe stato ordinato e avremmo avuto std::ordered_mapun tipo separato.

Prestazione

Di seguito due grafici dovrebbero parlare da soli ( fonte ):

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine


Dati interessanti; quante piattaforme hai incluso nei tuoi test?
Toby Speight,

1
perché dovrei usare std :: map per una piccola tabella quando faccio molte query poiché std :: unordered_map ha sempre prestazioni migliori di std :: map secondo le 2 immagini che hai pubblicato qui?
ricky il

Il grafico mostra le prestazioni per 0,13 M o più elementi. Se hai elementi piccoli (possono essere <100), allora O (log n) potrebbe diventare più piccolo della mappa non ordinata.
Shital Shah,

10

Di recente ho fatto un test che rende 50000 unisci e ordina. Ciò significa che se le chiavi di stringa sono uguali, unire la stringa di byte. E l'output finale dovrebbe essere ordinato. Quindi questo include una ricerca per ogni inserimento.

Per l' mapimplementazione, sono necessari 200 ms per terminare il lavoro. Per unordered_map+ map, sono necessari 70 ms per l' unordered_mapinserimento e 80 ms per l' mapinserimento. Quindi l'implementazione ibrida è più veloce di 50 ms.

Dovremmo pensarci due volte prima di usare il map. Se hai solo bisogno di ordinare i dati nel risultato finale del tuo programma, una soluzione ibrida potrebbe essere migliore.


0

Piccola aggiunta a tutto quanto sopra:

Uso migliore map, quando è necessario ottenere elementi per intervallo, poiché sono ordinati e si può semplicemente scorrere su di essi da un confine all'altro.


-1

Da: http://www.cplusplus.com/reference/map/map/

"Internamente, gli elementi in una mappa sono sempre ordinati in base alla sua chiave seguendo uno specifico criterio di ordinamento debole indicato dal suo oggetto di confronto interno (di tipo Confronta).

i contenitori delle mappe sono generalmente più lenti dei contenitori delle mappe non ordinate per accedere ai singoli elementi con la loro chiave, ma consentono l'iterazione diretta su sottoinsiemi in base al loro ordine. "

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.