Saya memiliki 2 kolom bilangan bulat terbatas tab, yang pertama adalah bilangan bulat acak, yang kedua adalah bilangan bulat yang mengidentifikasi grup, yang dapat dihasilkan oleh program ini. ( generate_groups.cc
)
#include <cstdlib>
#include <iostream>
#include <ctime>
int main(int argc, char* argv[]) {
int num_values = atoi(argv[1]);
int num_groups = atoi(argv[2]);
int group_size = num_values / num_groups;
int group = -1;
std::srand(42);
for (int i = 0; i < num_values; ++i) {
if (i % group_size == 0) {
++group;
}
std::cout << std::rand() << '\t' << group << '\n';
}
return 0;
}
Saya kemudian menggunakan program kedua ( sum_groups.cc
) untuk menghitung jumlah per grup.
#include <iostream>
#include <chrono>
#include <vector>
// This is the function whose performance I am interested in
void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
for (size_t i = 0; i < n; ++i) {
p_out[p_g[i]] += p_x[i];
}
}
int main() {
std::vector<int> values;
std::vector<int> groups;
std::vector<int> sums;
int n_groups = 0;
// Read in the values and calculate the max number of groups
while(std::cin) {
int value, group;
std::cin >> value >> group;
values.push_back(value);
groups.push_back(group);
if (group > n_groups) {
n_groups = group;
}
}
sums.resize(n_groups);
// Time grouped sums
std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
for (int i = 0; i < 10; ++i) {
grouped_sum(values.data(), groups.data(), values.size(), sums.data());
}
std::chrono::system_clock::time_point end = std::chrono::system_clock::now();
std::cout << (end - start).count() << std::endl;
return 0;
}
Jika saya kemudian menjalankan program-program ini pada dataset dengan ukuran tertentu, dan kemudian mengacak urutan baris dari dataset yang sama, data yang dikocok menghitung jumlah ~ 2x atau lebih cepat dari data yang dipesan.
g++ -O3 generate_groups.cc -o generate_groups
g++ -O3 sum_groups.cc -o sum_groups
generate_groups 1000000 100 > groups
shuf groups > groups2
sum_groups < groups
sum_groups < groups2
sum_groups < groups2
sum_groups < groups
20784
8854
8220
21006
Saya berharap data asli yang diurutkan berdasarkan kelompok memiliki lokalitas data yang lebih baik dan lebih cepat, tetapi saya mengamati perilaku yang berlawanan. Saya bertanya-tanya apakah ada yang bisa membuat hipotesis alasannya?
sumber
.at()
atau mode debugoperator[]
yang tidak dibatasi memeriksa Anda akan melihat.sum
. Alih-alihsums.reserve(n_groups);
Anda harus meneleponsums.resize(n_groups);
- itulah yang mengisyaratkan @Shawn.p_out[p_g[i]] += p_x[i];
. Mungkin dalam urutan acak asli, grup sebenarnya menunjukkan pengelompokan yang baik sehubungan dengan akses kep_out
array. Mengurutkan nilai-nilai mungkin menyebabkan pola akses indeks-kelompok yang burukp_out
.Jawaban:
Pengaturan / membuatnya lambat
Pertama-tama, program berjalan dalam waktu yang hampir bersamaan terlepas dari:
Sebagian besar waktu dihabiskan di loop input. Tapi karena kita tertarik pada
grouped_sum()
, mari abaikan itu.Mengubah loop tolok ukur dari 10 menjadi 1000 iterasi,
grouped_sum()
mulai mendominasi waktu berjalan:perf diff
Sekarang kita dapat menggunakan
perf
untuk menemukan tempat terpanas di program kami.Dan perbedaan di antara mereka:
Lebih banyak waktu
main()
, yang mungkin telahgrouped_sum()
disimpulkan. Bagus, terima kasih banyak, perf.perf annotate
Apakah ada perbedaan di mana waktu dihabiskan di dalam
main()
?Dikocok:
Diurutkan:
Tidak, itu dua instruksi yang sama mendominasi. Jadi mereka membutuhkan waktu lama dalam kedua kasus, tetapi bahkan lebih buruk ketika data diurutkan.
stat perf
Baik. Tetapi kita harus menjalankannya dalam jumlah yang sama, jadi setiap instruksi harus menjadi lebih lambat karena alasan tertentu Mari kita lihat apa yang
perf stat
dikatakan.Hanya satu hal yang menonjol: mandek-siklus-frontend .
Oke, pipa instruksi mandek. Di frontend. Tepat apa artinya mungkin bervariasi antara microarchictectures.
Tapi saya punya dugaan. Jika Anda murah hati, Anda mungkin menyebutnya hipotesa.
Hipotesa
Dengan mengurutkan input, Anda meningkatkan lokalitas penulisan. Bahkan, mereka akan sangat lokal; hampir semua penambahan yang Anda lakukan akan menulis ke lokasi yang sama dengan yang sebelumnya.
Itu bagus untuk cache, tetapi tidak bagus untuk pipa. Anda memperkenalkan dependensi data, mencegah agar instruksi tambahan berikutnya tidak dilanjutkan sampai penambahan sebelumnya selesai (atau sebaliknya membuat hasilnya tersedia untuk instruksi selanjutnya )
Itu masalahmu.
Kupikir.
Memperbaikinya
Beberapa vektor penjumlahan
Sebenarnya, mari kita coba sesuatu. Bagaimana jika kita menggunakan beberapa vektor penjumlahan, beralih di antara mereka untuk setiap penambahan, dan kemudian menyimpulkannya di bagian akhir? Harganya sedikit lokalitas, tetapi harus menghapus dependensi data.
(kodenya tidak cantik; jangan menilai saya, internet !!)
(oh, dan saya juga memperbaiki perhitungan n_groups; tidak aktif satu per satu.)
Hasil
Setelah mengkonfigurasi makefile saya untuk memberikan
-DNSUMS=...
argumen kepada kompiler, saya bisa melakukan ini:Jumlah vektor penjumlahan yang optimal mungkin akan tergantung pada kedalaman pipa CPU Anda. CPU ultrabook berusia 7 tahun saya mungkin dapat memaksimalkan pipa dengan vektor yang lebih sedikit daripada yang dibutuhkan CPU desktop mewah.
Jelas, lebih banyak belum tentu lebih baik; ketika saya menjadi gila dengan 128 vektor penjumlahan, kami mulai menderita lebih banyak dari kesalahan cache - sebagaimana dibuktikan oleh input yang diacak menjadi lebih lambat dari yang diurutkan, seperti yang Anda harapkan sebelumnya. Kami telah datang lingkaran penuh! :)
Jumlah per grup dalam daftar
(ini ditambahkan dalam edit)
Agh, kutu buku dikecam ! Jika Anda tahu input Anda akan diurutkan dan mencari kinerja yang lebih banyak lagi, penulisan ulang fungsi berikut (tanpa tambahan jumlah array) bahkan lebih cepat, setidaknya di komputer saya.
Trik yang satu ini adalah memungkinkan kompiler menyimpan
gsum
variabel, jumlah grup, dalam register. Saya menduga (tetapi mungkin sangat salah) bahwa ini lebih cepat karena loop umpan balik dalam pipa bisa lebih pendek di sini, dan / atau lebih sedikit memori yang diakses. Prediktor cabang yang baik akan membuat pemeriksaan ekstra untuk kesetaraan grup menjadi murah.Hasil
Mengerikan untuk input yang diacak ...
... tetapi sekitar 40% lebih cepat daripada solusi "banyak jumlah" saya untuk input yang diurutkan.
Banyak grup kecil akan lebih lambat daripada yang besar, jadi apakah ini implementasi yang lebih cepat atau tidak akan sangat tergantung pada data Anda di sini. Dan, seperti biasa, pada model CPU Anda.
Beberapa vektor penjumlahan, dengan offset alih-alih bit masking
Sopel menyarankan empat tambahan yang tidak gulungan sebagai alternatif dari pendekatan masking bit saya. Saya telah mengimplementasikan versi umum dari saran mereka, yang dapat menangani hal yang berbeda
NSUMS
. Saya mengandalkan kompiler membuka gulungan lingkaran dalam untuk kita (yang memang, setidaknya untukNSUMS=4
).Hasil
Waktu untuk mengukur. Perhatikan bahwa karena saya bekerja di / tmp kemarin, saya tidak memiliki data input yang sama persis. Oleh karena itu, hasil ini tidak dapat dibandingkan secara langsung dengan yang sebelumnya (tetapi mungkin cukup dekat).
Yup, loop dalam
NSUMS=8
adalah yang tercepat di komputer saya. Dibandingkan dengan pendekatan "gsum lokal" saya, itu juga memiliki manfaat tambahan untuk tidak menjadi buruk bagi input yang dikocok.Menarik untuk dicatat:
NSUMS=16
menjadi lebih buruk daripadaNSUMS=8
. Ini bisa jadi karena kita mulai melihat lebih banyak cache yang hilang, atau karena kita tidak memiliki cukup register untuk membuka gulungan lingkaran dalam dengan benar.sumber
perf
.Inilah sebabnya mengapa kelompok yang disortir lebih lambat daripada kelompok yang tidak disentuh;
Pertama di sini adalah kode rakitan untuk menjumlahkan loop:
Mari kita lihat instruksi add yang merupakan alasan utama untuk masalah ini;
Saat prosesor terlebih dahulu menjalankan instruksi ini, prosesor akan mengeluarkan memori read (load) request ke alamat di edx kemudian menambahkan nilai ecx kemudian mengeluarkan write (store) request untuk alamat yang sama.
ada fitur dalam memori pemanggil prosesor menyusun ulang
dan ada aturannya
Jadi jika iterasi berikutnya mencapai instruksi tambah sebelum permintaan tulis selesai, ia tidak akan menunggu jika alamat edx berbeda dari nilai sebelumnya dan mengeluarkan permintaan baca dan memesan ulang atas permintaan menulis yang lebih lama dan instruksi tambahkan berlanjut. tetapi jika alamatnya sama, instruksi add akan menunggu sampai penulisan lama selesai.
Perhatikan bahwa loop pendek dan prosesor dapat menjalankannya lebih cepat daripada pengontrol memori menyelesaikan permintaan tulis ke memori.
jadi untuk grup yang diurutkan Anda akan membaca dan menulis dari alamat yang sama berkali-kali secara berurutan sehingga akan kehilangan peningkatan kinerja menggunakan memori yang dipesan ulang; sementara itu jika grup acak digunakan maka setiap iterasi akan memiliki alamat yang mungkin berbeda sehingga pembacaan tidak akan menunggu lebih lama tulis dan disusun ulang sebelum itu; tambah instruksi tidak akan menunggu yang sebelumnya pergi.
sumber