Ini adalah pertanyaan yang muncul di benak saya ketika membaca jawaban brilian oleh Mysticial untuk pertanyaan: mengapa lebih cepat memproses array yang diurutkan daripada array yang tidak diurutkan ?
Konteks untuk jenis yang terlibat:
const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;
Dalam jawabannya dia menjelaskan bahwa Intel Compiler (ICC) mengoptimalkan ini:
for (int i = 0; i < 100000; ++i)
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += data[c];
... menjadi sesuatu yang setara dengan ini:
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
for (int i = 0; i < 100000; ++i)
sum += data[c];
Pengoptimal mengakui bahwa ini adalah setara dan karenanya bertukar loop , memindahkan cabang di luar loop dalam. Sangat pintar!
Tetapi mengapa tidak melakukan ini?
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000 * data[c];
Semoga Mysticial (atau siapa pun) dapat memberikan jawaban yang sama-sama brilian. Saya belum pernah belajar tentang optimasi yang dibahas dalam pertanyaan lain sebelumnya, jadi saya sangat berterima kasih untuk ini.
c
performance
compiler-optimization
jhabbott
sumber
sumber
volatile
, maka pertukaran loop akan menjadi optimasi yang tidak valid juga.Jawaban:
Kompilator umumnya tidak dapat mentransformasikannya
ke
karena yang terakhir dapat menyebabkan overflow bilangan bulat yang ditandatangani di mana yang pertama tidak. Bahkan dengan perilaku wrap-around yang dijamin untuk overflow bilangan bulat pelengkap dua yang ditandatangani, itu akan mengubah hasilnya (jika
data[c]
30000, produk akan menjadi-1294967296
untuk 32-bit khasint
dengan lilitan sekitar, sementara 100000 kali menambahkan 30000sum
akan, jika itu tidak meluap, naiksum
3000000000). Perhatikan bahwa hal yang sama berlaku untuk jumlah yang tidak ditandatangani, dengan angka yang berbeda, luapan dari100000 * data[c]
biasanya akan memperkenalkan modulo reduksi2^32
yang tidak boleh muncul dalam hasil akhir.Itu bisa mengubahnya menjadi
meskipun, jika, seperti biasa,
long long
cukup besarint
.Mengapa tidak melakukan itu, saya tidak tahu, saya kira itulah yang dikatakan Mysticial , "tampaknya, ia tidak menjalankan loop-collapsing setelah loop-interchange".
Perhatikan bahwa loop-interchange itu sendiri umumnya tidak valid (untuk bilangan bulat yang ditandatangani), karena
dapat menyebabkan meluapnya tempat
tidak akan. Ini halal di sini, karena kondisi memastikan semua
data[c]
yang ditambahkan memiliki tanda yang sama, jadi jika satu meluap, keduanya melakukannya.Saya tidak akan terlalu yakin bahwa kompiler memperhitungkannya (@Mysticial, dapatkah Anda mencoba dengan kondisi seperti
data[c] & 0x80
atau sehingga bisa benar untuk nilai positif dan negatif?). Saya memiliki kompiler membuat optimisasi yang tidak valid (misalnya, beberapa tahun yang lalu, saya memiliki ICC (11.0, iirc) menggunakan konversi ditandatangani-32-bit-int-ke-ganda di1.0/n
manan
adalahunsigned int
. Sekitar dua kali lebih cepat dari gcc output. Tapi salah, banyak nilai lebih besar dari2^31
, oops.).sumber
ADD.W A6,$A000
, lupa bahwa operasi kata dengan register register sign-memperpanjang kata menjadi 32 bit sebelum menambahkan. Butuh beberapa saat untuk memecahkan masalah, karena satu-satunya hal yang dilakukan kode antara ituADD
dan waktu berikutnya muncul A6 dari tumpukan adalah untuk mengembalikan register pemanggil itu telah disimpan ke bingkai itu ...MyArray[0] = 4;
saya bisa memeriksa alamatMyArray
, dan melihat lokasi itu sebelum dan sesudah pernyataan dieksekusi; itu tidak akan berubah. Kode adalah sesuatu sepertimove.B @A3,#4
dan A3 seharusnya selalu menunjukMyArray
kapan saja instruksi dijalankan, tetapi tidak. Menyenangkan.Jawaban ini tidak berlaku untuk kasus spesifik yang ditautkan, tetapi berlaku untuk judul pertanyaan dan mungkin menarik bagi pembaca di masa mendatang:
Karena presisi yang terbatas, penambahan floating-point tidak setara dengan perkalian . Mempertimbangkan:
Demo
sumber
Kompiler berisi berbagai lintasan yang melakukan optimasi. Biasanya di setiap pass baik optimasi pada pernyataan atau optimasi loop dilakukan. Saat ini tidak ada model yang melakukan optimalisasi loop body berdasarkan pada header loop. Ini sulit dideteksi dan kurang umum.
Optimasi yang dilakukan adalah gerakan kode invarian loop. Ini dapat dilakukan dengan menggunakan serangkaian teknik.
sumber
Yah, saya kira beberapa kompiler mungkin melakukan optimasi semacam ini, dengan asumsi bahwa kita berbicara tentang Integer Arithmetics.
Pada saat yang sama, beberapa kompiler mungkin menolak untuk melakukannya karena mengganti penambahan berulang dengan multiplikasi dapat mengubah perilaku overflow kode. Untuk tipe integer yang tidak ditandatangani, seharusnya tidak membuat perbedaan karena perilaku overflow mereka ditentukan sepenuhnya oleh bahasa. Tapi untuk yang sudah masuk, mungkin (mungkin tidak pada platform komplemen 2's). Memang benar bahwa limpahan yang ditandatangani sebenarnya mengarah ke perilaku yang tidak terdefinisi dalam C, yang berarti bahwa itu boleh saja untuk mengabaikan semantik melimpah itu sama sekali, tetapi tidak semua kompiler cukup berani untuk melakukan itu. Seringkali menuai banyak kritik dari kerumunan "C hanya bahasa tingkat tinggi". (Ingat apa yang terjadi ketika GCC memperkenalkan optimisasi berdasarkan semantik aliasing yang ketat?)
Secara historis, GCC telah menunjukkan dirinya sebagai kompiler yang memiliki apa yang diperlukan untuk mengambil langkah drastis seperti itu, tetapi kompiler lain mungkin lebih memilih untuk tetap dengan perilaku "yang dimaksudkan pengguna" yang dirasakan bahkan jika itu tidak ditentukan oleh bahasa.
sumber
Itu sekarang - setidaknya, dentang tidak :
kompilasi dengan -O1 ke
Overflow integer tidak ada hubungannya dengan itu; jika ada integer overflow yang menyebabkan perilaku tidak terdefinisi, itu bisa terjadi dalam kedua kasus tersebut. Inilah jenis fungsi yang sama yang digunakan
int
alih-alihlong
:kompilasi dengan -O1 ke
sumber
Ada hambatan konseptual untuk optimasi semacam ini. Penulis kompiler menghabiskan banyak upaya untuk pengurangan kekuatan - misalnya, mengganti perkalian dengan penambahan dan pergeseran. Mereka terbiasa berpikir bahwa perkalian itu buruk. Jadi kasus di mana seseorang harus pergi ke arah yang lain mengejutkan dan berlawanan dengan intuisi. Jadi tidak ada yang berpikir untuk mengimplementasikannya.
sumber
Orang-orang yang mengembangkan dan memelihara kompiler memiliki jumlah waktu dan energi yang terbatas untuk dihabiskan pada pekerjaan mereka, sehingga mereka umumnya ingin fokus pada apa yang paling dipedulikan pengguna mereka: mengubah kode yang ditulis dengan baik menjadi kode cepat. Mereka tidak ingin menghabiskan waktu mencoba mencari cara untuk mengubah kode konyol menjadi kode cepat — itulah tujuan dari tinjauan kode. Dalam bahasa tingkat tinggi, mungkin ada kode "konyol" yang mengekspresikan ide penting, menjadikannya sepadan dengan waktu pengembang untuk membuatnya secepat itu — misalnya, penggundulan hutan jalan pintas dan penggabungan aliran memungkinkan program Haskell terstruktur di sekitar jenis malas tertentu menghasilkan struktur data untuk dikompilasi menjadi loop ketat yang tidak mengalokasikan memori. Tetapi insentif semacam itu tidak berlaku untuk mengubah penambahan yang berulang menjadi multiplikasi. Jika Anda ingin cepat,
sumber