Menjawab pertanyaan Stack Overflow lainnya (yang ini ) saya menemukan sub-masalah yang menarik. Apa cara tercepat untuk mengurutkan array 6 bilangan bulat?
Karena tingkat pertanyaannya sangat rendah:
- kami tidak dapat menganggap perpustakaan tersedia (dan panggilan itu sendiri biayanya), hanya C biasa
- untuk menghindari pengosongan pipa instruksi (yang memiliki biaya yang sangat tinggi) kita mungkin harus meminimalkan cabang, lompatan, dan setiap jenis aliran kontrol lainnya (seperti yang tersembunyi di belakang titik urutan
&&
atau||
). - ruangan dibatasi dan meminimalkan register dan penggunaan memori adalah masalah, idealnya di tempat semacam itu mungkin yang terbaik.
Sungguh pertanyaan ini adalah semacam Golf di mana tujuannya bukan untuk meminimalkan panjang sumber tetapi waktu eksekusi. Saya menyebutnya kode 'Zening' seperti yang digunakan dalam judul buku Zen of Code optimisasi oleh Michael Abrash dan sekuelnya .
Adapun mengapa itu menarik, ada beberapa lapisan:
- contohnya sederhana dan mudah dipahami dan diukur, tidak banyak keterampilan C yang terlibat
- itu menunjukkan efek pilihan dari algoritma yang baik untuk masalah, tetapi juga efek dari kompiler dan perangkat keras yang mendasarinya.
Inilah implementasi referensi saya (naif, tidak dioptimalkan) dan set pengujian saya.
#include <stdio.h>
static __inline__ int sort6(int * d){
char j, i, imin;
int tmp;
for (j = 0 ; j < 5 ; j++){
imin = j;
for (i = j + 1; i < 6 ; i++){
if (d[i] < d[imin]){
imin = i;
}
}
tmp = d[j];
d[j] = d[imin];
d[imin] = tmp;
}
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int main(int argc, char ** argv){
int i;
int d[6][5] = {
{1, 2, 3, 4, 5, 6},
{6, 5, 4, 3, 2, 1},
{100, 2, 300, 4, 500, 6},
{100, 2, 3, 4, 500, 6},
{1, 200, 3, 4, 5, 600},
{1, 1, 2, 1, 2, 1}
};
unsigned long long cycles = rdtsc();
for (i = 0; i < 6 ; i++){
sort6(d[i]);
/*
* printf("d%d : %d %d %d %d %d %d\n", i,
* d[i][0], d[i][6], d[i][7],
* d[i][8], d[i][9], d[i][10]);
*/
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
}
Hasil mentah
Karena jumlah varian menjadi besar, saya mengumpulkan semuanya di ruang uji yang dapat ditemukan di sini . Tes yang sebenarnya digunakan sedikit lebih naif dari yang ditunjukkan di atas, terima kasih kepada Kevin Stock. Anda dapat mengkompilasi dan menjalankannya di lingkungan Anda sendiri. Saya cukup tertarik dengan perilaku arsitektur target / kompiler yang berbeda. (OK teman, taruh di jawaban, saya akan memberi +1 setiap kontributor dari hasil baru).
Saya memberikan jawaban kepada Daniel Stutzbach (untuk bermain golf) satu tahun yang lalu ketika ia menjadi sumber solusi tercepat saat itu (menyortir jaringan).
Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O2
- Panggilan langsung ke fungsi perpustakaan qsort: 689.38
- Implementasi naif (jenis penyisipan): 285.70
- Jenis Penyisipan (Daniel Stutzbach): 142.12
- Penyisipan Sortir Belum Terdaftar: 125,47
- Peringkat Pesanan: 102.26
- Urutan Peringkat dengan register: 58.03
- Penyortiran Jaringan (Daniel Stutzbach): 111.68
- Sorting Networks (Paul R): 66.36
- Sorting Networks 12 dengan Fast Swap: 58.86
- Sorting Networks 12 Swap dipesan ulang: 53.74
- Sorting Networks 12 Simple Swap yang disusun ulang: 31.54
- Reortered Sorting Network dengan swap cepat: 31.54
- Jaringan Penyortiran Reordered dengan swap cepat V2: 33.63
- Sortir Bubble Bergaris (Paolo Bonzini): 48.85
- Sortir Penyisipan Tidak Dikontrol (Paolo Bonzini): 75,30
Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O1
- Panggilan langsung ke fungsi perpustakaan qsort: 705.93
- Implementasi naif (jenis penyisipan): 135.60
- Jenis Penyisipan (Daniel Stutzbach): 142.11
- Penyisipan Sortir Belum Terdaftar: 126,75
- Peringkat Pesanan: 46,42
- Urutan Peringkat dengan register: 43,58
- Sorting Networks (Daniel Stutzbach): 115.57
- Sorting Networks (Paul R): 64.44
- Sorting Networks 12 dengan Fast Swap: 61.98
- Sorting Networks 12 Swap yang disusun ulang: 54.67
- Sorting Networks 12 Simple Swap yang disusun ulang: 31.54
- Jaringan Penyortiran Reordered dengan swap cepat: 31.24
- Reortered Sorting Network dengan swap cepat V2: 33.07
- Sortir Bubble Bergaris (Paolo Bonzini): 45,79
- Sortir Penyisipan Tidak Dikontrol (Paolo Bonzini): 80.15
Saya menyertakan hasil -O1 dan -O2 karena secara mengejutkan untuk beberapa program O2 kurang efisien daripada O1. Saya ingin tahu pengoptimalan spesifik apa yang memiliki efek ini?
Komentar tentang solusi yang diusulkan
Jenis Penyisipan (Daniel Stutzbach)
Seperti yang diharapkan meminimalkan cabang memang ide yang bagus.
Sorting Networks (Daniel Stutzbach)
Lebih baik daripada jenis penyisipan. Saya bertanya-tanya apakah efek utama tidak didapat dari menghindari loop eksternal. Saya mencobanya dengan memasukkan penyisipan tanpa gulungan untuk memeriksa dan memang kita mendapatkan kira-kira angka yang sama (kode ada di sini ).
Sorting Networks (Paul R)
Yang terbaik sejauh ini. Kode aktual yang saya gunakan untuk menguji ada di sini . Belum tahu mengapa ini hampir dua kali lebih cepat dari implementasi jaringan penyortiran lainnya. Melewati parameter? Max cepat?
Sorting Networks 12 SWAP dengan Fast Swap
Seperti yang disarankan oleh Daniel Stutzbach, saya menggabungkan 12 jaringan sortir swap-nya dengan swap cepat tanpa cabang (kode ada di sini ). Ini memang lebih cepat, yang terbaik sejauh ini dengan margin kecil (sekitar 5%) seperti yang bisa diharapkan menggunakan 1 swap lebih sedikit.
Menarik juga untuk memperhatikan bahwa swap tanpa cabang tampaknya jauh (4 kali) kurang efisien daripada yang sederhana jika menggunakan arsitektur PPC.
Memanggil Perpustakaan qsort
Untuk memberikan titik referensi lain saya juga mencoba seperti yang disarankan untuk memanggil perpustakaan qsort (kode ada di sini ). Seperti yang diharapkan itu jauh lebih lambat: 10 hingga 30 kali lebih lambat ... seperti yang terlihat jelas dengan test suite baru, masalah utama tampaknya menjadi beban awal perpustakaan setelah panggilan pertama, dan membandingkan tidak begitu buruk dengan yang lain Versi: kapan. Itu hanya antara 3 dan 20 kali lebih lambat di Linux saya. Pada beberapa arsitektur yang digunakan untuk tes oleh orang lain tampaknya bahkan lebih cepat (saya benar-benar terkejut dengan yang itu, karena perpustakaan qsort menggunakan API yang lebih kompleks).
Urutan peringkat
Rex Kerr mengusulkan metode lain yang sama sekali berbeda: untuk setiap item array menghitung langsung posisi akhirnya. Ini efisien karena urutan peringkat komputasi tidak perlu cabang. Kelemahan dari metode ini adalah dibutuhkan tiga kali jumlah memori array (satu salinan array dan variabel untuk menyimpan urutan peringkat). Hasil kinerja sangat mengejutkan (dan menarik). Pada arsitektur referensi saya dengan OS 32 bit dan Intel Core2 Quad E8300, jumlah siklus sedikit di bawah 1000 (seperti jaringan sortir dengan branching swap). Tetapi ketika dikompilasi dan dieksekusi pada kotak 64 bit saya (Intel Core2 Duo) kinerjanya jauh lebih baik: itu menjadi yang tercepat sejauh ini. Saya akhirnya menemukan alasan sebenarnya. Kotak 32 bit saya menggunakan gcc 4.4.1 dan kotak 64bits saya gcc 4.4.
perbarui :
Seperti gambar yang diterbitkan di atas menunjukkan efek ini masih ditingkatkan oleh versi gcc dan Urutan Peringkat kemudian secara konsisten dua kali lebih cepat dari alternatif lain.
Sorting Networks 12 dengan Swap yang disusun ulang
Efisiensi luar biasa dari proposal Rex Kerr dengan gcc 4.4.3 membuat saya bertanya-tanya: bagaimana mungkin sebuah program dengan penggunaan memori 3 kali lebih cepat dari pada jaringan sortir tanpa cabang? Hipotesis saya adalah bahwa ia memiliki sedikit ketergantungan dari jenis baca setelah menulis, memungkinkan untuk penggunaan yang lebih baik dari penjadwal instruksi superscalar x86. Itu memberi saya ide: mengatur ulang swap untuk meminimalkan membaca setelah menulis dependensi. Lebih sederhana: ketika Anda melakukannya, SWAP(1, 2); SWAP(0, 2);
Anda harus menunggu swap pertama selesai sebelum melakukan yang kedua karena keduanya mengakses sel memori yang sama. Saat Anda melakukannya SWAP(1, 2); SWAP(4, 5);
, prosesor dapat menjalankan keduanya secara paralel. Saya mencobanya dan berfungsi seperti yang diharapkan, jaringan sortasi berjalan sekitar 10% lebih cepat.
Sortasi Jaringan 12 dengan Simple Swap
Satu tahun setelah posting asli Steinar H. Gunderson menyarankan, bahwa kita tidak boleh mencoba mengakali kompiler dan menjaga kode swap sederhana. Ini memang ide yang bagus karena kode yang dihasilkan sekitar 40% lebih cepat! Dia juga mengusulkan swap yang dioptimalkan dengan tangan menggunakan kode perakitan inline x86 yang masih dapat meluangkan lebih banyak siklus. Yang paling mengejutkan (katanya volume pada psikologi programmer) adalah bahwa satu tahun yang lalu tidak ada yang digunakan mencoba versi swap. Kode yang saya gunakan untuk menguji ada di sini . Lainnya menyarankan cara lain untuk menulis swap cepat C, tetapi menghasilkan kinerja yang sama seperti yang sederhana dengan kompiler yang layak.
Kode "terbaik" sekarang adalah sebagai berikut:
static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x)
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
const int b = max(d[x], d[y]); \
d[x] = a; d[y] = b; }
SWAP(1, 2);
SWAP(4, 5);
SWAP(0, 2);
SWAP(3, 5);
SWAP(0, 1);
SWAP(3, 4);
SWAP(1, 4);
SWAP(0, 3);
SWAP(2, 5);
SWAP(1, 3);
SWAP(2, 4);
SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}
Jika kami yakin set pengujian kami (dan, ya itu sangat buruk, itu hanya manfaatnya pendek, sederhana dan mudah untuk memahami apa yang kami ukur), jumlah rata-rata siklus kode yang dihasilkan untuk satu jenis adalah di bawah 40 siklus ( 6 tes dijalankan). Itu menempatkan setiap swap pada rata-rata 4 siklus. Saya menyebutnya sangat cepat. Adakah perbaikan lain yang mungkin?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
karena rdtsc menempatkan jawabannya di EDX: EAX sementara GCC mengharapkannya dalam register 64-bit tunggal. Anda dapat melihat bug dengan kompilasi di -O3. Juga lihat di bawah ini komentar saya kepada Paul R tentang SWAP yang lebih cepat.
CMP EAX, EBX; SBB EAX, EAX
akan menempatkan 0 atau 0xFFFFFFFF di EAX
tergantung pada apakah EAX
lebih besar atau lebih kecil dari EBX
, masing-masing. SBB
adalah "kurangi dengan meminjam", mitra dari ADC
("tambah dengan carry"); bit status yang Anda lihat adalah carry bit. Kemudian lagi, saya ingat itu ADC
dan SBB
memiliki latensi & throughput yang mengerikan pada Pentium 4 vs ADD
dan SUB
, dan masih dua kali lebih lambat pada Core CPU. Sejak 80386 ada juga instruksi SETcc
conditional-store dan CMOVcc
conditional-move, tetapi mereka juga lambat.
x-y
danx+y
tidak akan menyebabkan underflow atau overflow?