Karena penasaran saya memutuskan untuk mengukur fungsi perkalian matriks saya sendiri versus implementasi BLAS ... Saya harus mengatakan yang paling tidak terkejut dengan hasilnya:
Implementasi Kustom, 10 uji coba perkalian matriks 1000x1000:
Took: 15.76542 seconds.
Implementasi BLAS, 10 percobaan perkalian matriks 1000x1000:
Took: 1.32432 seconds.
Ini menggunakan angka floating point presisi tunggal.
Implementasi Saya:
template<class ValT>
void mmult(const ValT* A, int ADim1, int ADim2, const ValT* B, int BDim1, int BDim2, ValT* C)
{
if ( ADim2!=BDim1 )
throw std::runtime_error("Error sizes off");
memset((void*)C,0,sizeof(ValT)*ADim1*BDim2);
int cc2,cc1,cr1;
for ( cc2=0 ; cc2<BDim2 ; ++cc2 )
for ( cc1=0 ; cc1<ADim2 ; ++cc1 )
for ( cr1=0 ; cr1<ADim1 ; ++cr1 )
C[cc2*ADim2+cr1] += A[cc1*ADim1+cr1]*B[cc2*BDim1+cc1];
}
Saya punya dua pertanyaan:
- Diketahui bahwa perkalian matriks-matriks mengatakan: nxm * mxn memerlukan perkalian n * n * m, jadi dalam kasus operasi di atas 1000 ^ 3 atau 1e9. Bagaimana mungkin pada prosesor 2.6Ghz saya untuk BLAS melakukan operasi 10 * 1e9 dalam 1,32 detik? Bahkan jika perkalian adalah satu operasi dan tidak ada lagi yang dilakukan, itu akan memakan waktu ~ 4 detik.
- Mengapa penerapan saya jauh lebih lambat?
Jawaban:
Titik awal yang baik adalah buku hebat The Science of Programming Matrix Computations oleh Robert A. van de Geijn dan Enrique S. Quintana-Ortí. Mereka menyediakan versi unduhan gratis.
BLAS dibagi menjadi tiga tingkatan:
Level 1 mendefinisikan sekumpulan fungsi aljabar linier yang beroperasi hanya pada vektor. Fungsi-fungsi ini mendapatkan keuntungan dari vektorisasi (misalnya dari penggunaan SSE).
Fungsi level 2 adalah operasi matriks-vektor, misalnya beberapa hasil kali matriks-vektor. Fungsi-fungsi ini dapat diimplementasikan dalam kaitannya dengan fungsi Level1. Namun, Anda dapat meningkatkan kinerja fungsi ini jika Anda dapat menyediakan implementasi khusus yang menggunakan beberapa arsitektur multiprosesor dengan memori bersama.
Fungsi level 3 adalah operasi seperti hasil kali matriks-matriks. Sekali lagi Anda bisa mengimplementasikannya dalam kaitannya dengan fungsi Level2. Tetapi fungsi Level3 melakukan operasi O (N ^ 3) pada data O (N ^ 2). Jadi, jika platform Anda memiliki hierarki cache, Anda dapat meningkatkan kinerja jika Anda menyediakan implementasi khusus yang dioptimalkan untuk cache / ramah cache . Ini dijelaskan dengan baik di buku. Peningkatan utama fungsi Level3 berasal dari pengoptimalan cache. Peningkatan ini secara signifikan melebihi dorongan kedua dari paralelisme dan pengoptimalan perangkat keras lainnya.
Ngomong-ngomong, sebagian besar (atau bahkan semua) implementasi BLAS berkinerja tinggi TIDAK diterapkan di Fortran. ATLAS diimplementasikan di C. GotoBLAS / OpenBLAS diimplementasikan di C dan kinerja bagian penting di Assembler. Hanya implementasi referensi BLAS yang dilaksanakan di Fortran. Namun, semua implementasi BLAS ini menyediakan antarmuka Fortran sehingga dapat ditautkan ke LAPACK (LAPACK mendapatkan semua kinerjanya dari BLAS).
Kompiler yang dioptimalkan memainkan peran kecil dalam hal ini (dan untuk GotoBLAS / OpenBLAS, kompiler tidak menjadi masalah sama sekali).
Implementasi IMHO no BLAS menggunakan algoritma seperti algoritma Coppersmith – Winograd atau algoritma Strassen. Saya tidak begitu yakin tentang alasannya, tapi ini tebakan saya:
Edit / Perbarui:
Makalah baru dan terobosan untuk topik ini adalah makalah BLIS . Mereka ditulis dengan sangat baik. Untuk kuliah saya "Dasar-dasar Perangkat Lunak untuk Komputasi Kinerja Tinggi", saya mengimplementasikan produk matriks-matriks setelah makalah mereka. Sebenarnya saya menerapkan beberapa varian dari produk matriks-matriks. Varian paling sederhana seluruhnya ditulis dalam C biasa dan memiliki kurang dari 450 baris kode. Semua varian lainnya hanya mengoptimalkan loop
Performa keseluruhan dari produk matriks-matriks hanya bergantung pada loop ini. Sekitar 99,9% waktu dihabiskan di sini. Dalam varian lain saya menggunakan kode intrinsik dan assembler untuk meningkatkan kinerja. Anda dapat melihat tutorial melalui semua varian di sini:
ulmBLAS: Tutorial tentang GEMM (Matrix-Matrix Product)
Bersama dengan makalah BLIS, menjadi cukup mudah untuk memahami bagaimana perpustakaan seperti Intel MKL dapat memperoleh kinerja seperti itu. Dan mengapa tidak masalah apakah Anda menggunakan penyimpanan utama baris atau kolom!
Tolok ukur terakhir ada di sini (kami menyebut proyek kami ulmBLAS):
Tolok ukur untuk ulmBLAS, BLIS, MKL, openBLAS dan Eigen
Edit / Pembaruan Lain:
Saya juga menulis beberapa tutorial tentang bagaimana BLAS digunakan untuk masalah aljabar linier numerik seperti memecahkan sistem persamaan linier:
Faktorisasi LU Kinerja Tinggi
(Faktorisasi LU ini misalnya digunakan oleh Matlab untuk menyelesaikan sistem persamaan linier.)
Saya berharap dapat menemukan waktuuntuk memperpanjang tutorial untuk menjelaskan dan mendemonstrasikan bagaimana mewujudkan implementasi paralel yang sangat skalabel dari faktorisasi LU seperti di PLASMA .Oke, ini dia: Coding a Cache Optimized Parallel LU Factorization
PS: Saya juga melakukan beberapa percobaan untuk meningkatkan kinerja uBLAS. Sebenarnya cukup mudah untuk meningkatkan (ya, mainkan kata-kata :)) kinerja uBLAS:
Eksperimen di uBLAS .
Berikut proyek serupa dengan BLAZE :
Eksperimen di BLAZE .
sumber
Jadi pertama-tama BLAS hanyalah antarmuka dari sekitar 50 fungsi. Ada banyak implementasi antarmuka yang bersaing.
Pertama saya akan menyebutkan hal-hal yang sebagian besar tidak terkait:
Sebagian besar implementasi memecah setiap operasi menjadi matriks berdimensi kecil atau operasi vektor dengan cara yang lebih atau kurang jelas. Misalnya perkalian matriks besar 1000x1000 dapat dipecah menjadi urutan perkalian matriks 50x50.
Operasi dimensi kecil ukuran tetap ini (disebut kernel) di-hardcode dalam kode assembly khusus CPU menggunakan beberapa fitur CPU dari targetnya:
Selanjutnya kernel ini dapat dieksekusi secara paralel satu sama lain menggunakan beberapa utas (inti CPU), dalam pola desain pengurangan peta yang khas.
Lihatlah ATLAS yang merupakan implementasi BLAS open source yang paling umum digunakan. Ini memiliki banyak kernel yang bersaing, dan selama proses pembuatan pustaka ATLAS, ia menjalankan persaingan di antara mereka (beberapa bahkan berparameter, jadi kernel yang sama dapat memiliki pengaturan yang berbeda). Ia mencoba konfigurasi yang berbeda dan kemudian memilih yang terbaik untuk sistem target tertentu.
(Tip: Itulah sebabnya jika Anda menggunakan ATLAS, lebih baik Anda membuat dan menyetel library secara manual untuk mesin tertentu Anda, kemudian menggunakan yang sudah dibuat sebelumnya.)
sumber
Pertama, ada algoritme yang lebih efisien untuk perkalian matriks daripada yang Anda gunakan.
Kedua, CPU Anda dapat melakukan lebih dari satu instruksi dalam satu waktu.
CPU Anda menjalankan 3-4 instruksi per siklus, dan jika unit SIMD digunakan, setiap proses instruksi 4 float atau 2 double. (tentu saja angka ini juga tidak akurat, karena CPU biasanya hanya dapat memproses satu instruksi SIMD per siklus)
Ketiga, kode Anda jauh dari optimal:
sumber
restrict
(tanpa aliasing) adalah default, tidak seperti di C / C ++. (Dan sayangnya ISO C ++ tidak memilikirestrict
kata kunci, jadi Anda harus menggunakannya__restrict__
pada kompiler yang menyediakannya sebagai ekstensi).Saya tidak tahu secara spesifik tentang implementasi BLAS tetapi ada alogoritma yang lebih efisien untuk Perkalian Matriks yang memiliki kompleksitas yang lebih baik daripada O (n3). Salah satu yang paling dikenal adalah Strassen Algorithm
sumber
Sebagian besar argumen untuk pertanyaan kedua - assembler, pemisahan menjadi blok, dll. (Tetapi tidak kurang dari algoritma N ^ 3, mereka benar-benar dikembangkan secara berlebihan) - berperan. Tetapi kecepatan rendah algoritme Anda pada dasarnya disebabkan oleh ukuran matriks dan pengaturan yang tidak menguntungkan dari tiga loop bersarang. Matriks Anda sangat besar sehingga tidak dapat dimasukkan sekaligus dalam memori cache. Anda dapat mengatur ulang loop sedemikian rupa sehingga sebanyak mungkin akan dilakukan pada satu baris dalam cache, dengan cara ini secara dramatis mengurangi penyegaran cache (Pembagian BTW menjadi blok-blok kecil memiliki efek analog, paling baik jika loop di atas blok diatur serupa). Implementasi model untuk matriks persegi berikut. Di komputer saya, konsumsi waktunya sekitar 1:10 dibandingkan dengan penerapan standar (seperti milik Anda). Dengan kata lain: jangan pernah memprogram perkalian matriks sepanjang "
Satu komentar lagi: Implementasi ini bahkan lebih baik di komputer saya daripada mengganti semua dengan BLAS rutin cblas_dgemm (coba di komputer Anda!). Tetapi jauh lebih cepat (1: 4) memanggil dgemm_ dari pustaka Fortran secara langsung. Saya pikir rutinitas ini sebenarnya bukan Fortran tetapi kode assembler (saya tidak tahu apa yang ada di perpustakaan, saya tidak punya sumbernya). Sama sekali tidak jelas bagi saya mengapa cblas_dgemm tidak secepat karena sepengetahuan saya itu hanyalah pembungkus untuk dgemm_.
sumber
Ini adalah percepatan yang realistis. Untuk contoh tentang apa yang dapat dilakukan dengan assembler SIMD melalui kode C ++, lihat beberapa contoh fungsi matriks iPhone - ini lebih dari 8x lebih cepat daripada versi C, dan bahkan tidak perakitan "dioptimalkan" - belum ada pipa-lining dan di sana adalah operasi tumpukan yang tidak perlu.
Juga kode Anda tidak " batasi yang benar " - bagaimana kompilator tahu bahwa ketika ia memodifikasi C, ia tidak memodifikasi A dan B?
sumber
-fstrict-aliasing
. Ada juga penjelasan yang lebih baik tentang "batasi" di sini: cellperformance.beyond3d.com/articles/2006/05/…Sehubungan dengan kode asli dalam perkalian MM, referensi memori untuk sebagian besar operasi adalah penyebab utama kinerja buruk. Memori berjalan 100-1000 kali lebih lambat dari cache.
Sebagian besar percepatan berasal dari penggunaan teknik pengoptimalan loop untuk fungsi triple loop ini dalam perkalian MM. Dua teknik optimasi loop utama digunakan; membuka gulungan dan memblokir. Sehubungan dengan unrolling, kami membuka gulungan dua loop terluar dan memblokirnya agar data digunakan kembali di cache. Pembukaan loop luar membantu mengoptimalkan akses data secara temporer dengan mengurangi jumlah referensi memori ke data yang sama pada waktu yang berbeda selama keseluruhan operasi. Memblokir indeks loop pada nomor tertentu, membantu mempertahankan data dalam cache. Anda dapat memilih untuk mengoptimalkan L2 cache atau L3 cache.
https://en.wikipedia.org/wiki/Loop_nest_optimization
sumber
Untuk banyak alasan.
Pertama, kompiler Fortran sangat dioptimalkan, dan bahasanya memungkinkan mereka untuk menjadi seperti itu. C dan C ++ sangat longgar dalam hal penanganan larik (misalnya kasus pointer yang merujuk ke area memori yang sama). Ini berarti kompilator tidak dapat mengetahui lebih dulu apa yang harus dilakukan, dan dipaksa untuk membuat kode generik. Di Fortran, kasus Anda lebih efisien, dan kompilator memiliki kendali yang lebih baik atas apa yang terjadi, memungkinkannya untuk lebih mengoptimalkan (misalnya menggunakan register).
Hal lain adalah Fortran menyimpan barang secara kolom, sementara C menyimpan data berdasarkan baris. Saya belum 'memeriksa kode Anda, tetapi hati-hati dengan cara Anda menjalankan produk. Di C Anda harus memindai baris dengan bijak: dengan cara ini Anda memindai array Anda di sepanjang memori yang berdekatan, mengurangi cache yang meleset. Cache miss adalah sumber inefisiensi pertama.
Ketiga, tergantung dari implementasi blas yang Anda gunakan. Beberapa implementasi mungkin ditulis dalam assembler, dan dioptimalkan untuk prosesor tertentu yang Anda gunakan. Versi netlib ditulis di fortran 77.
Juga, Anda melakukan banyak operasi, kebanyakan diulang dan mubazir. Semua perkalian untuk mendapatkan indeks tersebut merugikan kinerja. Saya tidak begitu tahu bagaimana hal ini dilakukan di BLAS, tetapi ada banyak trik untuk mencegah operasi yang mahal.
Misalnya, Anda dapat mengerjakan ulang kode Anda dengan cara ini
Cobalah, saya yakin Anda akan menyelamatkan sesuatu.
Pada pertanyaan # 1 Anda, alasannya adalah bahwa perkalian matriks berskala sebagai O (n ^ 3) jika Anda menggunakan algoritme yang sepele. Ada algoritme yang berskala jauh lebih baik .
sumber