Sudah menjadi pengetahuan umum dalam pemrograman bahwa lokalitas memori meningkatkan kinerja banyak karena cache ditemukan. Baru-baru ini saya menemukan tentang boost::flat_map
implementasi peta berbasis vektor. Tampaknya tidak sepopuler biasanya map
/ unordered_map
jadi saya belum bisa menemukan perbandingan kinerja apa pun. Bagaimana cara membandingkannya dan apa kasus penggunaan terbaik untuk itu?
Terima kasih!
Jawaban:
Saya baru-baru ini menjalankan tolok ukur pada struktur data yang berbeda di perusahaan saya, jadi saya merasa perlu berhenti bicara. Sangat rumit untuk mengukur sesuatu dengan benar.
Pembandingan
Di web kita jarang menemukan (jika pernah) patokan yang direkayasa dengan baik. Sampai saat ini saya hanya menemukan tolok ukur yang dilakukan dengan cara jurnalis (cukup cepat dan menyapu puluhan variabel di bawah karpet).
1) Anda perlu mempertimbangkan tentang pemanasan cache
Kebanyakan orang yang menjalankan benchmark takut akan ketidaksesuaian pengatur waktu, oleh karena itu mereka menjalankan barang mereka ribuan kali dan menghabiskan seluruh waktu, mereka berhati-hati untuk mengambil ribuan kali yang sama untuk setiap operasi, dan kemudian menganggapnya sebanding.
Sebenarnya, di dunia nyata ini tidak masuk akal, karena cache Anda tidak akan hangat, dan operasi Anda kemungkinan besar hanya akan dipanggil sekali. Oleh karena itu Anda perlu melakukan benchmark menggunakan RDTSC, dan mengatur waktu panggilan mereka hanya sekali. Intel telah membuat makalah yang menjelaskan cara menggunakan RDTSC (menggunakan instruksi cpuid untuk membersihkan pipeline, dan memanggilnya setidaknya 3 kali di awal program untuk menstabilkannya).
2) Pengukuran akurasi RDTSC
Saya juga merekomendasikan melakukan ini:
Ini adalah pengukur perbedaan, dan ini akan membutuhkan nilai minimum dari semua nilai terukur, untuk menghindari mendapatkan -10 ** 18 (64 bit nilai negatif pertama) dari waktu ke waktu.
Perhatikan penggunaan intrinsik dan bukan perakitan inline. Perakitan inline pertama jarang didukung oleh kompiler saat ini, tetapi yang lebih buruk dari semuanya, compiler membuat penghalang pemesanan penuh di sekitar perakitan inline karena tidak dapat menganalisis bagian dalam secara statis, jadi ini adalah masalah untuk membandingkan hal-hal dunia nyata, terutama saat memanggil barang-barang hanya sekali. Jadi intrinsik cocok di sini, karena tidak merusak pengurutan ulang instruksi compiler.
3) parameter
Masalah terakhir adalah orang biasanya menguji terlalu sedikit variasi skenario. Kinerja penampung dipengaruhi oleh:
Poin 1 penting karena container memang mengalokasikan dari waktu ke waktu, dan sangat penting jika mereka mengalokasikan menggunakan CRT "baru" atau operasi yang ditentukan pengguna, seperti alokasi kumpulan atau freelist atau lainnya ...
( untuk orang yang tertarik tentang pt 1, bergabunglah dengan utas misteri di gamedev tentang dampak kinerja pengalokasi sistem )
Poin 2 adalah karena beberapa kontainer (katakanlah A) akan kehilangan waktu untuk menyalin barang-barang, dan semakin besar jenisnya semakin besar overhead. Masalahnya adalah ketika membandingkan dengan kontainer B lain, A mungkin menang atas B untuk tipe kecil, dan kalah untuk tipe yang lebih besar.
Poin 3 sama dengan poin 2, hanya saja biaya dikalikan dengan beberapa faktor pembobotan.
Poin 4 adalah pertanyaan tentang O besar yang dicampur dengan masalah cache. Beberapa container dengan kompleksitas buruk sebagian besar dapat mengungguli container dengan kompleksitas rendah untuk sejumlah kecil jenis (seperti
map
vs.vector
, karena lokalitas cache-nya bagus, tetapimap
memecah memori). Dan kemudian di beberapa titik persimpangan, mereka akan kehilangan, karena ukuran keseluruhan yang terkandung mulai "bocor" ke memori utama dan menyebabkan cache hilang, ditambah fakta bahwa kompleksitas asimtotik dapat mulai dirasakan.Poin 5 adalah tentang kompiler yang dapat menghilangkan hal-hal yang kosong atau sepele pada waktu kompilasi. Ini dapat sangat mengoptimalkan beberapa operasi, karena penampung memiliki kerangka, oleh karena itu setiap jenis akan memiliki profil kinerjanya sendiri.
Poin 6 sama dengan poin 5, POD dapat mengambil keuntungan dari fakta bahwa konstruksi salinan hanyalah sebuah memcpy, dan beberapa container dapat memiliki implementasi khusus untuk kasus ini, menggunakan spesialisasi template parsial, atau SFINAE untuk memilih algoritme sesuai dengan ciri-ciri T.
Tentang peta datar
Rupanya peta datar adalah pembungkus vektor yang diurutkan, seperti Loki AssocVector, tetapi dengan beberapa modernisasi tambahan yang hadir dengan C ++ 11, memanfaatkan semantik gerakan untuk mempercepat penyisipan dan penghapusan elemen tunggal.
Ini masih merupakan wadah yang dipesan. Kebanyakan orang biasanya tidak membutuhkan bagian pemesanan, oleh karena itu keberadaan
unordered..
.Pernahkah Anda mempertimbangkan bahwa mungkin Anda membutuhkan
flat_unorderedmap
? yang akan menjadi sesuatu sepertigoogle::sparse_map
atau sesuatu seperti itu — peta hash alamat terbuka.Masalah dari peta hash alamat terbuka adalah bahwa pada saat
rehash
mereka harus menyalin semua yang ada di sekitar ke tanah datar baru yang diperluas, sedangkan peta standar yang tidak berurutan hanya harus membuat ulang indeks hash, sementara data yang dialokasikan tetap di tempatnya. Kerugiannya tentu saja adalah bahwa memori terfragmentasi seperti neraka.Kriteria pengulangan dalam peta hash alamat terbuka adalah jika kapasitas melebihi ukuran vektor keranjang dikalikan dengan faktor beban.
Faktor beban tipikal adalah
0.8
; oleh karena itu, Anda perlu memperhatikan hal itu, jika Anda dapat membuat ukuran awal peta hash Anda sebelum mengisinya, selalu lakukan pra-ukuran ke:intended_filling * (1/0.8) + epsilon
ini akan memberi Anda jaminan bahwa Anda tidak perlu mengulang dan menyalin ulang semuanya secara palsu selama pengisian.Keuntungan dari peta alamat tertutup (
std::unordered..
) adalah Anda tidak perlu peduli dengan parameter tersebut.Tapi itu
boost::flat_map
adalah vektor yang dipesan; Oleh karena itu, ia akan selalu memiliki kompleksitas asimtotik log (N), yang kurang baik daripada peta hash alamat terbuka (waktu konstan diamortisasi). Anda harus mempertimbangkan itu juga.Hasil benchmark
Ini adalah tes yang melibatkan peta yang berbeda (dengan
int
kunci dan__int64
/somestruct
sebagai nilai) danstd::vector
.jenis informasi yang diuji:
Insersi
EDIT:
Hasil saya sebelumnya menyertakan bug: mereka benar-benar menguji penyisipan yang dipesan, yang menunjukkan perilaku yang sangat cepat untuk peta datar.
Saya meninggalkan hasil itu nanti di halaman ini karena mereka menarik.
Ini tes yang benar:
Saya telah memeriksa implementasinya, tidak ada yang namanya ditangguhkan yang diterapkan di peta datar di sini. Setiap penyisipan diurutkan dengan cepat, oleh karena itu tolok ukur ini menunjukkan kecenderungan asimtotik:
peta: O (N * log (N))
hashmaps: O (N)
vektor dan flatmaps: O (N * N)
Peringatan : akhirat 2 tes untuk
std::map
dan keduaflat_map
s kereta uji dan benar-benar memerintahkan penyisipan (vs penyisipan acak untuk wadah lainnya ya itu membingungkan maaf.):Kita dapat melihat bahwa penyisipan berurutan, menghasilkan dorongan ke belakang, dan sangat cepat. Namun, dari hasil benchmark saya yang tidak dipetakan, saya juga dapat mengatakan bahwa ini tidak mendekati optimalitas absolut untuk penyisipan belakang. Pada elemen 10k, optimalitas penyisipan balik yang sempurna diperoleh pada vektor yang telah dipesan sebelumnya. Yang memberi kita 3 juta siklus; kami mengamati 4,8 juta di sini untuk penyisipan yang dipesan ke file
flat_map
(karena itu 160% dari optimal).Analisis: ingat ini adalah 'sisipan acak' untuk vektor, jadi 1 miliar siklus besar berasal dari keharusan menggeser setengah (rata-rata) data ke atas (satu elemen demi satu elemen) pada setiap penyisipan.
Pencarian acak dari 3 elemen (jam dinormalisasi menjadi 1)
dalam ukuran = 100
dalam ukuran = 10000
Pengulangan
lebih dari ukuran 100 (hanya tipe MediumPod)
lebih dari ukuran 10000 (hanya tipe MediumPod)
Butir garam terakhir
Pada akhirnya saya ingin kembali ke "Benchmarking §3 Pt1" (pengalokasi sistem). Dalam percobaan baru-baru ini yang saya lakukan seputar kinerja peta hash alamat terbuka yang saya kembangkan , saya mengukur kesenjangan kinerja lebih dari 3000% antara Windows 7 dan Windows 8 pada beberapa
std::unordered_map
kasus penggunaan ( dibahas di sini ).Yang membuat saya ingin memperingatkan pembaca tentang hasil di atas (dibuat di Win7): jarak tempuh Anda mungkin berbeda.
salam Hormat
sumber
flat_map
dibandingkan denganstd::map
- adakah yang bisa menjelaskan hasil ini?flat_map
sebagai wadah. KarenaAska::
versinya lebih cepat dari padastd::map
lookup. Membuktikan bahwa masih ada ruang untuk pengoptimalan. Performa yang diharapkan secara asimtotik sama, tetapi mungkin sedikit lebih baik berkat lokalitas cache. Dengan set ukuran tinggi, mereka harus bertemu.Dari dokumen, sepertinya ini analogi
Loki::AssocVector
yang saya pengguna cukup berat. Karena didasarkan pada vektor, ia memiliki karakteristik vektor, yaitu:size
berkembangcapacity
.capacity
itu perlu dialokasikan kembali dan memindahkan objek ke atas, yaitu penyisipan tidak dijamin waktu konstan kecuali untuk kasus khusus memasukkan padaend
saatcapacity > size
std::map
karena lokalitas cache, pencarian biner yang memiliki karakteristik kinerja yang sama seperti yangstd::map
lainnyaPenggunaan terbaik adalah saat Anda mengetahui jumlah elemen sebelumnya (sehingga Anda bisa
reserve
dimuka), atau saat penyisipan / penghapusan jarang terjadi tetapi pencarian sering dilakukan. Pembatalan Iterator membuatnya sedikit rumit dalam beberapa kasus penggunaan sehingga tidak dapat dipertukarkan dalam hal ketepatan program.sumber