Kapan, jika pernah, apakah loop unrolling masih berguna?

93

Saya telah mencoba untuk mengoptimalkan beberapa kode yang sangat kritis terhadap kinerja (algoritma pengurutan cepat yang disebut jutaan dan jutaan kali di dalam simulasi monte carlo) dengan membuka gulungan berulang. Inilah loop dalam yang saya coba percepat:

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

Saya mencoba membuka gulungan ke sesuatu seperti:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

Ini sama sekali tidak membuat perbedaan jadi saya mengubahnya kembali ke bentuk yang lebih mudah dibaca. Saya memiliki pengalaman serupa di lain waktu saya mencoba membuka gulungan loop. Mengingat kualitas prediktor cabang pada perangkat keras modern, kapan, jika pernah, apakah pembukaan gulungan masih merupakan pengoptimalan yang berguna?

dsimcha
sumber
1
Bolehkah saya bertanya mengapa Anda tidak menggunakan rutinitas quicksort perpustakaan standar?
Peter Alexander
14
@Poita: Karena milik saya memiliki beberapa fitur tambahan yang saya perlukan untuk kalkulasi statistik yang saya lakukan dan sangat disesuaikan untuk kasus penggunaan saya dan oleh karena itu kurang umum tetapi terukur lebih cepat daripada lib standar. Saya menggunakan bahasa pemrograman D, yang memiliki pengoptimal jelek lama, dan untuk array besar mengambang acak, saya masih mengalahkan pengurutan C ++ STL GCC sebesar 10-20%.
dsimcha

Jawaban:

122

Loop unrolling masuk akal jika Anda dapat memutuskan rantai ketergantungan. Ini memberikan CPU yang rusak atau super skalar kemungkinan untuk menjadwalkan hal-hal dengan lebih baik dan dengan demikian berjalan lebih cepat.

Contoh sederhana:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

Di sini rantai ketergantungan argumen sangat pendek. Jika Anda mendapatkan stall karena Anda memiliki cache-miss pada data-array, cpu tidak dapat melakukan apapun selain menunggu.

Di sisi lain kode ini:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

bisa berlari lebih cepat. Jika Anda mendapatkan cache miss atau stall lain dalam satu kalkulasi, masih ada tiga rantai dependensi lain yang tidak bergantung pada stall. CPU yang rusak dapat menjalankan ini.

Nils Pipenbrinck
sumber
2
Terima kasih. Saya telah mencoba membuka gulungan berulang dalam gaya ini di beberapa tempat lain di perpustakaan tempat saya menghitung jumlah dan hal-hal, dan di tempat-tempat ini ia bekerja dengan sangat baik. Saya hampir yakin alasannya adalah bahwa itu meningkatkan paralelisme tingkat instruksi, seperti yang Anda sarankan.
dsimcha
2
Jawaban bagus dan contoh instruktif. Meskipun saya tidak melihat bagaimana penghentian cache-miss dapat memengaruhi kinerja untuk contoh khusus ini . Saya datang untuk menjelaskan kepada diri saya sendiri perbedaan kinerja antara dua bagian kode (di mesin saya potongan kode kedua 2-3 kali lebih cepat) dengan mencatat bahwa yang pertama menonaktifkan semua jenis paralelisme tingkat instruksi di jalur floating point. Yang kedua akan memungkinkan CPU skalar super untuk mengeksekusi hingga empat penambahan floating point pada saat yang bersamaan.
Toby Brull
2
Ingatlah bahwa hasilnya tidak akan identik secara numerik dengan loop asli saat menghitung jumlah dengan cara ini.
Barabas
Dependensi yang dibawa loop adalah satu siklus , penambahan. Inti OoO akan baik-baik saja. Di sini membuka gulungan mungkin membantu SIMD floating point, tetapi itu bukan tentang OoO.
Veedrac
2
@Nils: Tidak terlalu banyak; CPU OoO x86 mainstream masih cukup mirip dengan Core2 / Nehalem / K10. Mengejar setelah kehilangan cache masih cukup kecil, menyembunyikan latensi FP masih merupakan keuntungan utama. Pada tahun 2010, CPU yang dapat melakukan 2 beban per jam bahkan lebih langka (hanya AMD karena SnB belum dirilis), jadi banyak akumulator jelas kurang berharga untuk kode integer daripada sekarang (tentu saja ini adalah kode skalar yang harus melakukan vektorisasi otomatis , jadi siapa yang tahu apakah kompiler akan mengubah beberapa akumulator menjadi elemen vektor atau menjadi beberapa akumulator vektor ...)
Peter Cordes
25

Itu tidak akan membuat perbedaan karena Anda melakukan jumlah perbandingan yang sama. Inilah contoh yang lebih baik. Dari pada:

for (int i=0; i<200; i++) {
  doStuff();
}

menulis:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

Meskipun demikian, hampir pasti tidak akan menjadi masalah tetapi Anda sekarang melakukan 50 perbandingan, bukan 200 (bayangkan perbandingannya lebih kompleks).

Putaran manual membuka gulungan secara umum sebagian besar merupakan artefak sejarah. Ini adalah salah satu dari daftar hal-hal yang terus bertambah yang akan dilakukan kompiler yang baik untuk Anda ketika itu penting. Misalnya, kebanyakan orang tidak repot-repot menulis x <<= 1atau x += xsebaliknya x *= 2. Anda tinggal menulis x *= 2dan kompilator akan mengoptimalkannya untuk Anda untuk apa pun yang terbaik.

Pada dasarnya, kebutuhan untuk menebak-nebak kompiler Anda semakin berkurang.

cletus
sumber
1
@Mike Tentunya mematikan pengoptimalan jika ide bagus ketika bingung, tetapi ada baiknya membaca tautan yang diposting Poita_. Compiler semakin menyakitkan baik pada bisnis itu.
dmckee --- mantan moderator anak kucing
16
@ Mike "Saya sangat mampu memutuskan kapan atau kapan untuk tidak melakukan hal-hal itu" ... Saya meragukannya, kecuali Anda manusia super.
Tn. Boy
5
@ John: Saya tidak tahu mengapa Anda mengatakan itu; Orang-orang tampaknya berpikir bahwa pengoptimalan adalah semacam penyusun seni hitam saja dan penebak yang baik tahu bagaimana melakukannya. Semuanya bermuara pada instruksi dan siklus dan alasan mengapa mereka dihabiskan. Seperti yang telah saya jelaskan berkali-kali di SO, mudah untuk mengetahui bagaimana dan mengapa mereka dibelanjakan. Jika saya memiliki loop yang harus menggunakan persentase waktu yang signifikan, dan itu menghabiskan terlalu banyak siklus dalam loop overhead, dibandingkan dengan konten, saya dapat melihatnya dan membukanya. Sama untuk pengangkatan kode. Tidak perlu jenius.
Mike Dunlavey
3
Saya yakin ini tidak terlalu sulit, tetapi saya masih ragu Anda dapat melakukannya secepat kompilator. Apa masalahnya dengan kompiler yang melakukannya untuk Anda? Jika Anda tidak menyukainya, matikan saja pengoptimalan dan buang waktu Anda seperti tahun 1990!
Tn. Boy
2
Peningkatan kinerja karena pembukaan loop tidak ada hubungannya dengan perbandingan yang Anda simpan. Tidak ada sama sekali.
bobbogo
14

Terlepas dari prediksi cabang pada perangkat keras modern, sebagian besar kompiler tetap melakukan loop unrolling untuk Anda.

Akan bermanfaat untuk mengetahui seberapa banyak pengoptimalan yang dilakukan kompiler Anda untuk Anda.

Saya menemukan presentasi Felix von Leitner sangat mencerahkan tentang subjek ini. Saya sarankan Anda membacanya. Ringkasan: Kompiler modern SANGAT pintar, jadi pengoptimalan tangan hampir tidak pernah efektif.

Peter Alexander
sumber
7
Itu adalah bacaan yang bagus, tetapi satu-satunya bagian yang saya pikir sudah tepat adalah di mana dia berbicara tentang menjaga struktur data tetap sederhana. Sisa itu akurat tapi sisanya di asumsi tak tertulis raksasa - bahwa apa yang sedang dijalankan memiliki untuk menjadi. Dalam penyetelan yang saya lakukan, saya menemukan orang-orang yang mengkhawatirkan register & cache hilang ketika sejumlah besar waktu masuk ke pegunungan kode abstraksi yang tidak perlu.
Mike Dunlavey
4
"pengoptimalan tangan hampir tidak pernah efektif" → Mungkin benar jika Anda benar-benar baru dalam tugas tersebut. Tidak benar sebaliknya.
Veedrac
Pada tahun 2019 saya masih melakukan pembukaan gulungan manual dengan keuntungan substansial atas upaya otomatis kompiler .. jadi tidak terlalu dapat diandalkan untuk membiarkan kompilator melakukan semuanya. Tampaknya tidak terlalu sering membuka gulungan. Setidaknya untuk c # saya tidak bisa berbicara atas nama semua bahasa.
WDUK
2

Sejauh yang saya pahami, kompiler modern sudah membuka gulungan loop yang sesuai - contohnya adalah gcc, jika diteruskan, tanda pengoptimalan, manual mengatakan itu akan:

Unroll loop yang jumlah iterasinya dapat ditentukan pada waktu kompilasi atau saat masuk ke loop.

Jadi, dalam praktiknya, kemungkinan kompilator Anda akan melakukan kasus-kasus sepele untuk Anda. Oleh karena itu, terserah Anda untuk memastikan bahwa sebanyak mungkin loop Anda mudah bagi kompiler untuk menentukan berapa banyak iterasi yang diperlukan.

Bradshaw kaya
sumber
Just in time compiler biasanya tidak melakukan loop unrolling, heuristiknya terlalu mahal. Kompiler statis dapat menghabiskan lebih banyak waktu untuk itu, tetapi perbedaan antara dua cara dominan itu penting.
Abel
2

Loop unrolling, entah itu hand unrolling atau compiler unrolling, seringkali tidak produktif, terutama dengan CPU x86 yang lebih baru (Core 2, Core i7). Intinya: tolok ukur kode Anda dengan dan tanpa loop unrolling pada CPU apa pun yang Anda rencanakan untuk menerapkan kode ini.

Paul R
sumber
Mengapa khususnya pada CPU x86 recet?
JohnTortugo
7
@JohnTortugo: CPU x86 modern memiliki optimisasi tertentu untuk loop kecil - lihat misalnya Loop Stream Detector pada arsitektur Core dan Nehalem - membuka loop sehingga tidak lagi cukup kecil untuk muat di dalam cache LSD mengalahkan pengoptimalan ini. Lihat misalnya tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R
1

Mencoba tanpa mengetahui bukanlah cara untuk melakukannya.
Apakah jenis ini membutuhkan persentase waktu keseluruhan yang tinggi?

Semua loop unrolling yang dilakukan adalah mengurangi overhead loop dari incrementing / decrementing, membandingkan kondisi stop, dan jumping. Jika apa yang Anda lakukan dalam loop membutuhkan lebih banyak siklus instruksi daripada overhead loop itu sendiri, Anda tidak akan melihat banyak peningkatan dalam persentase.

Berikut contoh cara mendapatkan performa maksimal.

Mike Dunlavey
sumber
1

Loop unrolling dapat membantu dalam kasus tertentu. Keuntungan hanya tidak melewatkan beberapa tes!

Ini dapat misalnya memungkinkan penggantian skalar, penyisipan prapengambilan perangkat lunak yang efisien ... Anda akan terkejut betapa bermanfaatnya hal itu (Anda dapat dengan mudah mendapatkan kecepatan 10% pada sebagian besar loop bahkan dengan -O3) dengan membuka gulungan secara agresif.

Seperti yang dikatakan sebelumnya, ini sangat bergantung pada loop dan kompiler serta eksperimen diperlukan. Sulit untuk membuat aturan (atau heuristik kompiler untuk membuka gulungan akan sempurna)

Kamchatka
sumber
0

Pembukaan loop sepenuhnya tergantung pada ukuran masalah Anda. Itu sepenuhnya tergantung pada algoritma Anda untuk dapat mengurangi ukuran menjadi kelompok kerja yang lebih kecil. Apa yang Anda lakukan di atas tidak terlihat seperti itu. Saya tidak yakin apakah simulasi monte carlo bahkan dapat dibuka gulungannya.

Saya skenario yang baik untuk membuka gulungan loop akan memutar gambar. Karena Anda dapat merotasi kelompok kerja yang terpisah. Agar ini berfungsi, Anda harus mengurangi jumlah iterasi.

jwendl
sumber
Saya membuka gulungan semacam cepat yang dipanggil dari loop dalam simulasi saya, bukan loop utama simulasi.
dsimcha
0

Loop unrolling masih berguna jika ada banyak variabel lokal baik di dalam maupun dengan loop. Untuk menggunakan kembali register tersebut lebih banyak daripada menyimpannya untuk indeks loop.

Dalam contoh Anda, Anda menggunakan sejumlah kecil variabel lokal, tidak terlalu sering menggunakan register.

Perbandingan (ke ujung loop) juga merupakan kelemahan utama jika perbandingannya berat (yaitu non-test instruksi), terutama jika itu tergantung pada fungsi eksternal.

Loop unrolling membantu meningkatkan kesadaran CPU untuk prediksi cabang juga, tetapi itu tetap terjadi.

LiraNuna
sumber