Saya membuat beberapa pembandingan multiplikasi matriks, seperti yang disebutkan sebelumnya dalam Mengapa MATLAB begitu cepat dalam penggandaan matriks?
Sekarang saya punya masalah lain, ketika mengalikan dua matriks 2048x2048, ada perbedaan besar antara C # dan lainnya. Ketika saya coba kalikan hanya 2047x2047 matriks, sepertinya normal. Menambahkan beberapa yang lain untuk perbandingan juga.
1024x1024 - 10 detik.
1027x1027 - 10 detik.
2047x2047 - 90 detik.
2048x2048 - 300 detik.
2049x2049 - 91 detik. (memperbarui)
2500x2500 - 166 detik
Itu adalah perbedaan tiga setengah menit untuk kasus 2k dengan 2k.
menggunakan array 2dim
//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];
//Main multiply code
for(int j = 0; j < rozmer; j++)
{
for (int k = 0; k < rozmer; k++)
{
float temp = 0;
for (int m = 0; m < rozmer; m++)
{
temp = temp + matice1[j,m] * matice2[m,k];
}
matice3[j, k] = temp;
}
}
c#
arrays
matrix-multiplication
Serigala
sumber
sumber
Jawaban:
Ini mungkin ada hubungannya dengan konflik di cache L2 Anda.
Cache missses pada matice1 bukan masalah karena diakses secara berurutan. Namun untuk matice2 jika kolom lengkap cocok dengan L2 (yaitu ketika Anda mengakses matice2 [0, 0], matice2 [1, 0], matice2 [2, 0] ... dll, tidak ada yang diusir) daripada tidak ada masalah dengan cache merindukan dengan matice2 baik.
Sekarang untuk lebih dalam bagaimana cache bekerja, jika alamat byte variabel Anda adalah X, daripada baris cache untuk itu adalah (X >> 6) & (L - 1). Di mana L adalah jumlah total garis cache di cache Anda. L selalu berkekuatan 2. Enam berasal dari fakta bahwa 2 ^ 6 == 64 byte adalah ukuran standar garis cache.
Sekarang apa artinya ini? Yah itu berarti bahwa jika saya memiliki alamat X dan alamat Y dan (X >> 6) - (Y >> 6) dapat dibagi oleh L (yaitu beberapa kekuatan besar 2), mereka akan disimpan dalam cacheline yang sama.
Sekarang untuk kembali ke masalah Anda apa perbedaan antara 2048 dan 2049,
ketika 2048 adalah ukuran Anda:
jika Anda mengambil & matice2 [x, k] dan & matice2 [y, k] perbedaannya (& matice2 [x, k] >> 6) - (& matice2 [y, k] >> 6) akan habis dibagi 2048 * 4 (ukuran dari float). Jadi kekuatan besar 2.
Jadi tergantung pada ukuran L2 Anda, Anda akan memiliki banyak konflik garis cache, dan hanya menggunakan sebagian kecil dari L2 Anda untuk menyimpan kolom, sehingga Anda tidak akan benar-benar dapat menyimpan kolom penuh dalam cache Anda, sehingga Anda akan mendapatkan kinerja yang buruk .
Ketika ukuran 2049, maka perbedaannya adalah 2049 * 4 yang bukan kekuatan 2 sehingga Anda akan memiliki lebih sedikit konflik dan kolom Anda akan masuk ke cache dengan aman.
Sekarang untuk menguji teori ini ada beberapa hal yang dapat Anda lakukan:
Alokasikan array matice2 array Anda seperti ini matice2 [razmor, 4096], dan jalankan dengan razmor = 1024, 1025 atau ukuran apa pun, dan Anda akan melihat kinerja yang sangat buruk dibandingkan dengan yang Anda miliki sebelumnya. Ini karena Anda secara paksa menyelaraskan semua kolom untuk saling bertentangan.
Kemudian coba matice2 [razmor, 4097] dan jalankan dengan ukuran berapa pun dan Anda akan melihat kinerja yang jauh lebih baik.
sumber
Mungkin efek caching. Dengan dimensi matriks yang merupakan kekuatan besar dua, dan ukuran cache yang juga merupakan kekuatan dua, Anda hanya dapat menggunakan sebagian kecil dari cache L1 Anda, banyak memperlambat segalanya. Perkalian matriks naif biasanya dibatasi oleh kebutuhan untuk mengambil data ke dalam cache. Algoritma yang dioptimalkan menggunakan ubin (atau algoritma cache-oblivious) fokus pada membuat lebih baik menggunakan cache L1.
Jika Anda menghitung waktu pasangan lain (2 ^ n-1,2 ^ n) Saya berharap Anda akan melihat efek yang sama.
Untuk menjelaskan lebih lengkap, di loop dalam, di mana Anda mengakses matice2 [m, k], ada kemungkinan bahwa matice2 [m, k] dan matice2 [m + 1, k] diimbangi satu sama lain oleh 2048 * sizeof (float) dan dengan demikian memetakan ke indeks yang sama di cache L1. Dengan cache asosiatif N-way, Anda biasanya memiliki 1-8 lokasi cache untuk semua ini. Dengan demikian hampir semua akses tersebut akan memicu penggusuran cache L1, dan mengambil data dari cache yang lebih lambat atau memori utama.
sumber
Ini mungkin ada hubungannya dengan ukuran cache cpu Anda. Jika 2 baris matriks matriks tidak cocok, maka Anda akan kehilangan waktu menukar elemen dari RAM. Elemen 4095 tambahan mungkin cukup untuk mencegah agar baris tidak pas.
Dalam kasus Anda, 2 baris untuk 2047 matriks 2d berada dalam 16KB memori (dengan asumsi jenis 32 bit). Misalnya, jika Anda memiliki cache L1 (paling dekat dengan cpu di bus) dari 64KB, maka Anda dapat memasukkan setidaknya 4 baris (dari 2047 * 32) ke dalam cache sekaligus. Dengan baris yang lebih panjang jika ada padding yang diperlukan yang mendorong pasangan baris melebihi 16KB, maka segala sesuatunya mulai menjadi berantakan. Juga, setiap kali Anda 'ketinggalan' cache, bertukar data dari cache lain atau memori utama menunda banyak hal.
Dugaan saya adalah bahwa varians dalam menjalankan kali Anda melihat dengan matriks ukuran yang berbeda dipengaruhi oleh seberapa efektif sistem operasi dapat menggunakan cache yang tersedia (dan beberapa kombinasi hanya bermasalah). Tentu saja ini semua merupakan penyederhanaan besar di pihak saya.
sumber
Louis Brandy menulis dua posting blog yang menganalisis persis masalah ini:
Lebih Banyak Kegilaan Cache dan Kinerja Komputasi - Sebuah studi kasus pemula dengan beberapa statistik menarik dan upaya untuk menjelaskan perilaku secara lebih terperinci, hal ini memang mengarah pada batasan ukuran cache.
sumber
Mengingat bahwa waktu menurun pada ukuran yang lebih besar bukankah itu lebih cenderung menjadi konflik cache, terutama dengan kekuatan 2 untuk ukuran matriks yang bermasalah? Saya bukan ahli tentang masalah caching, tetapi info yang bagus tentang masalah kinerja yang berhubungan dengan cache di sini .
sumber
Ketika Anda mengakses
matice2
array secara vertikal, itu akan lebih banyak bertukar dan keluar dari cache. Jika Anda mirror array secara diagonal, sehingga Anda dapat mengaksesnya menggunakan[k,m]
bukan[m,k]
, kode akan berjalan jauh lebih cepat.Saya menguji ini untuk matriks 1024x1024, dan ini sekitar dua kali lebih cepat. Untuk 2048x2048 matriks sekitar sepuluh kali lebih cepat.
sumber
Cache Aliasing
Atau meronta-ronta cache , jika saya dapat koin istilah.
Tembolok bekerja dengan mengindeks dengan bit pesanan rendah dan penandaan dengan bit pesanan tinggi.
Gambar bahwa cache Anda memiliki 4 kata dan matriks Anda adalah 4 x 4. Ketika sebuah kolom diakses dan baris memiliki kekuatan dua panjangnya, maka setiap elemen kolom dalam memori akan dipetakan ke elemen cache yang sama.
Kekuatan dua-plus-satu sebenarnya sekitar optimal untuk masalah ini. Setiap elemen kolom baru akan dipetakan ke slot cache berikutnya persis seolah-olah mengakses dengan baris.
Dalam kehidupan nyata, tag mencakup beberapa alamat yang meningkat secara berurutan yang akan men-cache beberapa elemen yang berdekatan dalam satu baris. Dengan mengimbangi bucket yang dipetakan oleh setiap baris baru, melintasi kolom tidak menggantikan entri sebelumnya. Ketika kolom berikutnya dilalui, seluruh cache akan diisi dengan baris yang berbeda dan setiap bagian baris yang masuk ke cache akan menekan beberapa kolom.
Karena cache jauh lebih cepat daripada DRAM (kebanyakan karena on-chip) hit rate adalah segalanya.
sumber
Anda tampaknya telah mencapai batas ukuran cache, atau mungkin memiliki beberapa masalah pengulangan dalam timing Anda.
Apa pun masalahnya, Anda tidak perlu menulis sendiri perkalian matriks dalam C # dan alih-alih menggunakan versi BLAS yang dioptimalkan. Ukuran matriks itu harus dikalikan dalam waktu satu detik pada mesin modern mana pun.
sumber
Memanfaatkan hierarki cache secara efektif sangat penting. Anda perlu memastikan bahwa array multidimensi memiliki data dalam pengaturan yang bagus, yang dapat dilakukan dengan memasang ubin . Untuk melakukan ini, Anda harus menyimpan array 2D sebagai array 1D bersama dengan mekanisme pengindeksan. Masalah dengan metode tradisional adalah bahwa meskipun dua elemen array yang berdekatan yang berada di baris yang sama bersebelahan dalam memori, dua elemen yang berdekatan dalam kolom yang sama akan dipisahkan oleh elemen W dalam memori, di mana W adalah jumlah kolom . Ubin dapat membuat sebanyak faktor perbedaan kinerja.
sumber
Saya menduga itu adalah hasil dari sesuatu yang disebut " Sequential Flooding ". Apa ini adalah bahwa Anda mencoba untuk mengulang daftar objek yang sedikit lebih besar dari ukuran cache, sehingga setiap permintaan tunggal ke daftar (array) harus dilakukan dari ram, dan Anda tidak akan mendapatkan cache tunggal memukul.
Dalam kasus Anda, Anda mengulang-ulang array 2048 indeks 2048 kali, tetapi Anda hanya memiliki ruang untuk 2047 (mungkin karena beberapa overhead dari struktur array), jadi setiap kali Anda mengakses pos array, perlu mendapatkan pos array ini dari ram. Itu kemudian disimpan dalam cache, tetapi tepat sebelum digunakan lagi, itu dibuang. Jadi cache pada dasarnya tidak berguna, mengarah ke waktu eksekusi yang lebih lama.
sumber