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:
u64 g_correctionFactor; // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;
static u64 const errormeasure = ~((u64)0);
#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // flush OOO instruction pipeline
return __rdtsc();
}
inline void WarmupRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // warmup cpuid.
__cpuid(a, 0x80000000);
__cpuid(a, 0x80000000);
// measure the measurer overhead with the measurer (crazy he..)
u64 minDiff = LLONG_MAX;
u64 maxDiff = 0; // this is going to help calculate our PRECISION ERROR MARGIN
for (int i = 0; i < 80; ++i)
{
u64 tick1 = GetRDTSC();
u64 tick2 = GetRDTSC();
minDiff = std::min(minDiff, tick2 - tick1); // make many takes, take the smallest that ever come.
maxDiff = std::max(maxDiff, tick2 - tick1);
}
g_correctionFactor = minDiff;
printf("Correction factor %llu clocks\n", g_correctionFactor);
g_accuracy = maxDiff - minDiff;
printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif
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:
- Alokator
- ukuran tipe yang terkandung
- biaya pelaksanaan operasi penyalinan, operasi penugasan, operasi pemindahan, operasi konstruksi, dari jenis yang terkandung.
- jumlah elemen dalam wadah (ukuran masalah)
- jenis memiliki operasi 3.-sepele
- jenisnya adalah POD
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, tetapi map
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 seperti google::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) dan std::vector
.
jenis informasi yang diuji:
typeid=__int64 . sizeof=8 . ispod=yes
typeid=struct MediumTypePod . sizeof=184 . ispod=yes
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 kedua flat_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 fileflat_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