Apakah ada keuntungan menggunakan peta dibandingkan unordered_map jika ada kunci sepele?

371

Pembicaraan baru-baru ini unordered_mapdi C ++ membuat saya sadar bahwa saya harus menggunakan unordered_mapuntuk sebagian besar kasus di mana saya gunakan mapsebelumnya, karena efisiensi pencarian ( diamortisasi O (1) vs O (log n) ). Sering kali saya menggunakan peta, saya menggunakan salah satu intatau std::stringsebagai jenis kunci; karenanya, saya tidak punya masalah dengan definisi fungsi hash. Semakin saya memikirkannya, semakin saya sadari bahwa saya tidak dapat menemukan alasan untuk menggunakan std::maplebih std::unordered_mapdalam kasus kunci dengan tipe sederhana - Saya melihat antarmuka, dan tidak menemukan apa pun. perbedaan signifikan yang akan memengaruhi kode saya.

Oleh karena itu pertanyaan: apakah ada alasan untuk menggunakan std::maplebih std::unordered_mapdalam kasus jenis sederhana seperti intdan std::string?

Saya meminta dari sudut pandang pemrograman ketat - Saya tahu itu tidak sepenuhnya dianggap standar, dan itu dapat menimbulkan masalah dengan porting.

Juga, saya berharap bahwa salah satu jawaban yang benar mungkin "itu lebih efisien untuk set data yang lebih kecil" karena overhead yang lebih kecil (apakah itu benar?) - maka saya ingin membatasi pertanyaan pada kasus di mana jumlah kunci tidak mudah (> 1 024).

Sunting: ya, saya lupa yang sudah jelas (terima kasih GMan!) - ya, peta dipesan tentu saja - Saya tahu itu, dan saya mencari alasan lain.

Kornel Kisielewicz
sumber
22
Saya suka mengajukan pertanyaan ini dalam wawancara: "Kapan quick-sort lebih baik daripada bubble-sort?" Jawaban atas pertanyaan tersebut memberikan wawasan tentang penerapan praktis teori kompleksitas dan bukan hanya pernyataan hitam putih sederhana seperti O (1) yang lebih baik daripada O (n) atau O (k) setara dengan O (logn) dll. ..
42
@Beh, saya pikir Anda maksudkan "kapan gelembung-sort lebih baik daripada quick-sort": P
Kornel Kisielewicz
2
Apakah penunjuk pintar menjadi kunci yang sepele?
thomthom
Berikut adalah salah satu kasus di mana peta adalah yang menguntungkan: stackoverflow.com/questions/51964419/…
anilbey

Jawaban:

399

Jangan lupa bahwa mapelemen-elemennya tetap teratur. Jika Anda tidak bisa melepaskannya, jelas Anda tidak bisa menggunakannya unordered_map.

Hal lain yang perlu diingat adalah yang unordered_mapumumnya menggunakan lebih banyak memori. maphanya memiliki beberapa petunjuk rumah tangga, dan memori untuk setiap objek. Sebaliknya, unordered_mapmemiliki array besar (ini bisa menjadi sangat besar dalam beberapa implementasi), dan kemudian memori tambahan untuk setiap objek. Jika Anda perlu sadar memori, mapharus membuktikan lebih baik, karena tidak memiliki array yang besar.

Jadi, jika Anda perlu pencarian-pencarian murni, saya akan mengatakan unordered_mapadalah cara untuk pergi. Tetapi selalu ada trade-off, dan jika Anda tidak mampu membelinya, maka Anda tidak dapat menggunakannya.

Hanya dari pengalaman pribadi, saya menemukan peningkatan besar dalam kinerja (diukur, tentu saja) ketika menggunakan unordered_mapalih-alih mapdalam tabel pencarian entitas utama.

Di sisi lain, saya merasa jauh lebih lambat dalam memasukkan dan menghapus elemen berulang kali. Ini bagus untuk koleksi elemen yang relatif statis, tetapi jika Anda melakukan banyak penyisipan dan penghapusan, hashing + bucket sepertinya bertambah. (Catatan, ini lebih dari beberapa iterasi.)

GManNickG
sumber
3
Satu hal lagi tentang properti blok memori besar (r) dari unordered_map vs map (atau vektor vs daftar), tumpukan proses default (berbicara Windows di sini) adalah serial. Mengalokasikan (kecil) blok dalam jumlah besar dalam aplikasi multithread sangat mahal.
ROAR
4
RA: Anda bisa mengendalikannya dengan jenis pengalokasi Anda sendiri yang dikombinasikan dengan wadah apa pun, jika menurut Anda itu penting untuk program tertentu.
9
Jika Anda tahu ukuran unordered_mapdan memesannya di awal - apakah Anda masih membayar penalti dengan banyak penyisipan? Katakanlah, Anda hanya menyisipkan satu kali ketika Anda membangun tabel pencarian - dan kemudian hanya membaca dari itu.
thomthom
3
@thomthom Sejauh yang saya tahu, seharusnya tidak ada penalti dalam hal kinerja. Alasan mengapa kinerja terpukul adalah karena fakta bahwa jika array tumbuh terlalu besar, itu akan melakukan pengulangan semua elemen. Jika Anda memanggil cadangan, itu berpotensi akan mengulangi elemen yang ada tetapi jika Anda menyebutnya cadangan di awal, maka seharusnya tidak ada penalti, setidaknya menurut cplusplus.com/reference/unordered_map/unordered_map/unordered_map/reserve
Richard Fung
6
Saya cukup yakin bahwa dalam ingatannya adalah kebalikannya. Dengan asumsi faktor muatan default 1.0 untuk wadah yang tidak berurutan: Anda memiliki satu penunjuk per elemen untuk bucket dan satu penunjuk per elemen untuk bucket elemen-berikutnya, oleh karena itu Anda berakhir dengan dua petunjuk plus data per setiap elemen. Untuk wadah yang dipesan, di sisi lain, implementasi pohon RB khas akan memiliki: tiga pointer (kiri / kanan / induk) ditambah sedikit warna yang karena perataan membutuhkan kata maju. Itu adalah empat petunjuk plus data per setiap elemen.
Yakov Galka
126

Jika Anda ingin membandingkan kecepatan std::mapdan std::unordered_mapimplementasi Anda, Anda dapat menggunakan proyek sparsehash Google yang memiliki program time_hash_map untuk menentukan waktu mereka. Misalnya, dengan gcc 4.4.2 pada sistem 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)
Blair Zajac
sumber
2
Sepertinya peta yang tidak berurutan mengalahkan peta pada sebagian besar operasi. Bahkan dengan penyisipan ...
Michael IV
7
sparsehash tidak ada lagi. itu telah dihapus atau diturunkan.
User9102d82
1
@ User9102d82 Saya telah mengedit pertanyaan untuk merujuk ke tautan waybackmachine .
andreee
Hanya untuk memastikan bahwa orang lain memperhatikan angka-angka lain selain waktu juga: Tes-tes itu dilakukan dengan objek 4-byte / struktur data alias int. Jika Anda menyimpan sesuatu yang membutuhkan hashing lebih besar atau lebih besar (membuat operasi penyalinan lebih berat), peta standar mungkin dengan cepat memiliki keuntungan!
AlexGeorg
82

Saya kira-kira sama dengan poin yang dibuat GM: tergantung pada jenis penggunaannya, std::mapbisa (dan sering) lebih cepat daripada std::tr1::unordered_map(menggunakan implementasi yang termasuk dalam VS 2008 SP1).

Ada beberapa faktor rumit yang perlu diingat. Misalnya, dalam std::map, Anda membandingkan kunci, yang berarti Anda hanya pernah melihat cukup awal kunci untuk membedakan antara cabang-cabang pohon kanan dan kiri. Dalam pengalaman saya, hampir satu-satunya waktu Anda melihat seluruh kunci adalah jika Anda menggunakan sesuatu seperti int yang dapat Anda bandingkan dalam satu instruksi. Dengan jenis kunci yang lebih khas seperti std :: string, Anda sering membandingkan hanya beberapa karakter atau lebih.

Fungsi hash yang layak, sebaliknya, selalu melihat seluruh kunci. TKI, bahkan jika tabel lookup adalah kompleksitas konstan, hash itu sendiri memiliki kompleksitas linear (meskipun pada panjang kunci, bukan jumlah item). Dengan string panjang sebagai kunci, sebuah std::mapmungkin menyelesaikan pencarian sebelum unordered_mapbahkan akan memulai pencariannya.

Kedua, sementara ada beberapa metode mengubah ukuran tabel hash, kebanyakan dari mereka adalah cukup lambat - ke titik bahwa kecuali pencarian yang jauh lebih sering daripada sisipan dan penghapusan, std :: peta sering akan lebih cepat dari std::unordered_map.

Tentu saja, seperti yang saya sebutkan di komentar pada pertanyaan Anda sebelumnya, Anda juga dapat menggunakan tabel pohon. Ini memiliki kelebihan dan kekurangan. Di satu sisi, itu membatasi kasus terburuk ke pohon. Ini juga memungkinkan penyisipan dan penghapusan yang cepat, karena (setidaknya ketika saya sudah melakukannya) saya telah menggunakan tabel ukuran tetap. Menghapus semua ukuran tabel memungkinkan Anda untuk menjaga tabel hash Anda lebih sederhana dan biasanya lebih cepat.

Satu hal lain: persyaratan untuk hashing dan peta berbasis pohon berbeda. Hashing jelas membutuhkan fungsi hash, dan perbandingan kesetaraan, di mana peta yang dipesan membutuhkan perbandingan yang kurang. Tentu saja hibrida yang saya sebutkan membutuhkan keduanya. Tentu saja, untuk kasus umum menggunakan string sebagai kunci, ini sebenarnya bukan masalah, tetapi beberapa jenis kunci lebih cocok dipesan daripada hashing (atau sebaliknya).

Jerry Coffin
sumber
2
Mengubah ukuran hash dapat dikurangi dengan dynamic hashingteknik, yang terdiri dari memiliki periode transisi di mana setiap kali Anda memasukkan item, Anda juga mengulangi kitem lainnya. Tentu saja, ini berarti bahwa selama masa transisi Anda harus mencari 2 tabel yang berbeda ...
Matthieu M.
2
"Dengan string panjang sebagai kunci, std :: map mungkin menyelesaikan pencarian sebelum unordered_map bahkan memulai pencariannya." - jika kunci tidak ada dalam koleksi. Jika ada maka tentu saja panjang penuh perlu dibandingkan untuk mengkonfirmasi pertandingan. Tetapi juga unordered_mapperlu mengkonfirmasi kecocokan hash dengan perbandingan penuh, jadi itu semua tergantung pada bagian proses pencarian yang Anda kontras.
Steve Jessop
2
Anda biasanya dapat mengganti fungsi hash berdasarkan pengetahuan data. misalnya jika string panjang Anda lebih bervariasi dalam 20 byte terakhir daripada di 100 pertama, hanya hash 20 terakhir.
Erik Aronesty
56

Saya tertarik dengan jawaban dari @Jerry Coffin, yang menyarankan bahwa peta yang dipesan akan menunjukkan peningkatan kinerja pada string yang panjang, setelah beberapa percobaan (yang dapat diunduh dari pastebin ), saya telah menemukan bahwa ini hanya berlaku untuk koleksi string acak, ketika peta diinisialisasi dengan kamus diurutkan (yang berisi kata-kata dengan banyak awalan-tumpang tindih), aturan ini rusak, mungkin karena peningkatan kedalaman pohon yang diperlukan untuk mengambil nilai. Hasilnya ditunjukkan di bawah ini, kolom nomor 1 adalah waktu memasukkan, 2 adalah waktu pengambilan.

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
Murphy Gearoid
sumber
2
Terima kasih untuk tesnya. Untuk memastikan kami tidak mengukur kebisingan, saya mengubahnya untuk melakukan setiap operasi berkali-kali (dan memasukkan penghitung, bukannya 1 ke peta). Saya menjalankannya pada jumlah kunci yang berbeda (dari 2 hingga 1000) dan hingga ~ 100 kunci di peta, std::mapbiasanya mengungguli std::unordered_map, terutama untuk kunci integer tetapi ~ 100 kunci tampaknya kehilangan tepi dan std::unordered_mapmulai menang. Memasukkan urutan yang sudah dipesan ke dalam std::mapsangat buruk, Anda akan mendapatkan skenario terburuknya (O (N)).
Andreas Magnusson
30

Saya hanya akan menunjukkan bahwa ... ada banyak jenis unordered_maps.

Lihat Artikel Wikipedia di peta hash. Bergantung pada implementasi yang digunakan, karakteristik dalam hal pencarian, penyisipan, dan penghapusan mungkin sangat bervariasi.

Dan itulah yang paling membuat saya khawatir dengan penambahan unordered_mapSTL: mereka harus memilih implementasi tertentu karena saya ragu mereka akan turun Policy, dan jadi kita akan terjebak dengan implementasi untuk penggunaan rata-rata dan tidak ada untuk kasus lainnya ...

Misalnya beberapa peta hash memiliki pengulangan linear, di mana alih-alih mengulangi seluruh peta hash sekaligus, sebagian diulangi di setiap penyisipan, yang membantu mengamortisasi biaya.

Contoh lain: beberapa peta hash menggunakan daftar node untuk sebuah ember, yang lain menggunakan peta, yang lain tidak menggunakan node tetapi menemukan slot terdekat dan terakhir beberapa akan menggunakan daftar node tetapi menyusun ulang sehingga elemen yang terakhir diakses ada di depan (seperti caching).

Jadi pada saat ini saya cenderung memilih std::mapatau mungkin a loki::AssocVector(untuk set data beku).

Jangan salah paham, saya ingin menggunakan std::unordered_mapdan saya mungkin di masa depan, tetapi sulit untuk "mempercayai" portabilitas wadah seperti itu ketika Anda memikirkan semua cara menerapkannya dan berbagai pertunjukan yang dihasilkan ini.

Matthieu M.
sumber
17
+1: titik valid - hidup lebih mudah ketika saya menggunakan implementasi saya sendiri - setidaknya saya tahu di mana itu mengisap:>
Kornel Kisielewicz
25

Perbedaan signifikan yang belum benar-benar disebutkan di sini:

  • mapmenjaga iterator agar semua elemen stabil, di C ++ 17 Anda bahkan dapat memindahkan elemen dari satu mapke yang lain tanpa membatalkan iterator ke elemen (dan jika diimplementasikan dengan benar tanpa potensi alokasi).
  • map pengaturan waktu untuk operasi tunggal biasanya lebih konsisten karena mereka tidak pernah membutuhkan alokasi besar.
  • unordered_mapmenggunakan std::hashseperti yang diterapkan di libstdc ++ rentan terhadap DoS jika diumpankan dengan input yang tidak terpercaya (menggunakan MurmurHash2 dengan seed konstan - bukan bahwa seeding akan sangat membantu, lihat https://emboss.github.io/blog/2012/12/14/ breaking-murmur-hash-flooding-dos-reloaded / ).
  • Sedang dipesan memungkinkan pencarian rentang efisien, misalnya iterate atas semua elemen dengan kunci ≥ 42.
pengguna1531083
sumber
14

Tabel hash memiliki konstanta yang lebih tinggi daripada implementasi peta umum, yang menjadi signifikan untuk wadah kecil. Ukuran maks adalah 10, 100, atau mungkin bahkan 1.000 atau lebih? Konstanta sama seperti sebelumnya, tetapi O (log n) dekat dengan O (k). (Ingat kompleksitas logaritmik masih sangat bagus.)

Apa yang membuat fungsi hash yang baik tergantung pada karakteristik data Anda; jadi jika saya tidak berencana melihat fungsi hash kustom (tapi tentu saja dapat berubah pikiran nanti, dan dengan mudah karena saya mengetik hampir semua) dan meskipun default dipilih untuk tampil baik untuk banyak sumber data, saya menemukan yang dipesan sifat peta cukup membantu awalnya bahwa saya masih default untuk memetakan daripada tabel hash dalam kasus itu.

Ditambah lagi, Anda tidak perlu berpikir untuk menulis fungsi hash untuk tipe lain (biasanya UDT), dan cukup menulis op <(yang Anda inginkan juga).


sumber
@Roger, apakah Anda tahu perkiraan jumlah elemen di mana unordered_map bests map? Saya mungkin akan menulis tes untuk itu, bagaimanapun ... (+1)
Kornel Kisielewicz
1
@Kornel: Tidak butuh banyak; tes saya dengan sekitar 10.000 elemen. Jika kami menginginkan grafik yang benar - benar akurat, Anda bisa melihat implementasi mapdan salah satunya unordered_map, dengan platform tertentu dan ukuran cache tertentu, dan melakukan analisis yang kompleks. : P
GManNickG
Tergantung pada detail implementasi, parameter pengaturan waktu kompilasi (mudah didukung jika Anda menulis implementasi Anda sendiri), dan bahkan mesin khusus yang digunakan untuk pengujian. Sama seperti wadah lainnya, panitia hanya menetapkan persyaratan luas.
13

Alasan telah diberikan dalam jawaban lain; ini yang lain.

std :: map (pohon biner seimbang) operasi diamortisasi O (log n) dan kasus terburuk O (log n). operasi std :: unordered_map (tabel hash) diamortisasi O (1) dan kasus terburuk O (n).

Bagaimana ini dimainkan dalam praktek adalah bahwa tabel hash "cegukan" sesekali dengan operasi O (n), yang mungkin atau mungkin bukan sesuatu yang bisa ditoleransi aplikasi Anda. Jika tidak bisa menerimanya, Anda lebih suka std :: map over std :: unordered_map.

Don Hatch
sumber
12

Ringkasan

Dengan asumsi pemesanan tidak penting:

  • Jika Anda akan membangun tabel besar sekali dan melakukan banyak pertanyaan, gunakan std::unordered_map
  • Jika Anda akan membangun tabel kecil (mungkin di bawah 100 elemen) dan melakukan banyak pertanyaan, gunakan std::map. Ini karena bacaan tentang itu O(log n).
  • Jika Anda akan banyak mengubah tabel maka mungkin itu std::map adalah pilihan yang baik.
  • Jika Anda ragu, gunakan saja std::unordered_map.

Konteks Sejarah

Dalam sebagian besar bahasa, peta tidak berurutan (alias kamus berbasis hash) adalah peta default namun di C ++ Anda mendapatkan peta yang dipesan sebagai peta default. Bagaimana itu bisa terjadi? Beberapa orang keliru berasumsi bahwa komite C ++ membuat keputusan ini dalam kearifan unik mereka tetapi sayangnya sayangnya lebih buruk dari itu.

Dipercaya secara luas bahwa C ++ berakhir dengan peta yang dipesan sebagai default karena tidak ada terlalu banyak parameter tentang bagaimana mereka dapat diimplementasikan. Di sisi lain, implementasi berbasis hash memiliki banyak hal untuk dibicarakan. Jadi untuk menghindari kemacetan dalam standardisasi, mereka hanya cocok dengan peta yang dipesan. Sekitar tahun 2005, banyak bahasa sudah memiliki implementasi implementasi berbasis hash yang baik sehingga lebih mudah bagi komite untuk menerima yang baru std::unordered_map. Di dunia yang sempurna, std::mapakan tidak teratur dan kita akan memiliki std::ordered_maptipe yang terpisah.

Performa

Di bawah dua grafik harus berbicara sendiri ( sumber ):

masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

Shital Shah
sumber
Data yang menarik; berapa banyak platform yang Anda sertakan dalam pengujian Anda?
Toby Speight
1
mengapa saya harus menggunakan std :: map untuk tabel kecil ketika melakukan banyak pertanyaan karena std :: unordered_map selalu berkinerja lebih baik daripada std :: map sesuai dengan 2 gambar yang Anda posting di sini?
ricky
Grafik menunjukkan kinerja untuk elemen 0,13M atau lebih. Jika Anda memiliki elemen kecil (mungkin <100) maka O (log n) mungkin menjadi lebih kecil dari peta yang tidak berurutan.
Shital Shah
10

Saya telah membuat tes baru-baru ini yang membuat 50000 bergabung & urutkan. Itu berarti jika kunci string sama, gabungkan string byte. Dan hasil akhirnya harus disortir. Jadi ini termasuk mencari setiap penyisipan.

Untuk mapimplementasi, dibutuhkan 200 ms untuk menyelesaikan pekerjaan. Untuk unordered_map+ map, dibutuhkan 70 ms untuk unordered_mappenyisipan dan 80 ms untuk mappenyisipan. Jadi implementasi hybrid 50 ms lebih cepat.

Kita harus berpikir dua kali sebelum menggunakan map. Jika Anda hanya perlu data yang akan diurutkan dalam hasil akhir program Anda, solusi hybrid mungkin lebih baik.

Wendong
sumber
0

Tambahan kecil untuk semua hal di atas:

Penggunaan yang lebih baik map, ketika Anda perlu mendapatkan elemen berdasarkan rentang, karena mereka diurutkan dan Anda hanya bisa beralih dari satu batas ke batas lainnya.

Denis Sablukov
sumber
-1

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

"Secara internal, elemen-elemen dalam peta selalu diurutkan berdasarkan kuncinya mengikuti kriteria urutan lemah spesifik yang ditunjukkan oleh objek perbandingan internal (dari tipe Bandingkan).

kontainer peta pada umumnya lebih lambat daripada wadah unordered_map untuk mengakses elemen individu dengan kunci mereka, tetapi mereka memungkinkan iterasi langsung pada subset berdasarkan pesanan mereka. "

Kunal Bansal
sumber