Pertimbangkan kode berikut ( p
adalah tipe unsigned char*
dan bitmap->width
dari beberapa tipe integer, persis yang tidak diketahui dan bergantung pada versi mana dari beberapa perpustakaan eksternal yang kita gunakan):
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Apakah layak untuk dioptimalkan [..]
Mungkinkah ada kasus di mana ini dapat menghasilkan hasil yang lebih efisien dengan menulis:
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
... atau apakah hal ini sepele untuk dioptimasi oleh compiler?
Apa yang Anda anggap sebagai kode yang "lebih baik"?
Catatan dari editor (Ike): bagi mereka yang bertanya-tanya tentang teks coretan, pertanyaan asli, seperti yang diutarakan, sangat dekat dengan wilayah di luar topik dan hampir ditutup meskipun ada tanggapan positif. Ini telah dilumpuhkan. Namun tolong jangan menghukum penjawab yang menjawab bagian pertanyaan yang terserang ini.
sumber
*p
memiliki tipe yang samawidth
maka tidak mudah untuk dioptimalkan, karenap
bisa menunjukwidth
dan memodifikasinya di dalam loop.p
menunjuk ke memori yang sama denganbitmap->width
. Oleh karena itu saya tidak dapat secara legal mengoptimalkan contoh pertama ke yang kedua.Jawaban:
Pada pandangan pertama, saya pikir compiler dapat menghasilkan assembly yang setara untuk kedua versi dengan flag optimasi yang diaktifkan. Ketika saya memeriksanya, saya terkejut melihat hasilnya:
Sumber
unoptimized.cpp
catatan: kode ini tidak dimaksudkan untuk dieksekusi.
Sumber
optimized.cpp
catatan: kode ini tidak dimaksudkan untuk dieksekusi.
Kompilasi
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
Perakitan (tidak dioptimalkan)
Perakitan (dioptimalkan.)
beda
Rakitan yang dihasilkan untuk versi yang dioptimalkan sebenarnya memuat (
lea
)width
konstanta tidak seperti versi yang tidak dioptimalkan yang menghitungwidth
offset pada setiap iterasi (movq
).Ketika saya punya waktu, saya akhirnya memposting beberapa patokan untuk itu. Pertanyaan bagus.
sumber
const unsigned
bukan hanyaunsigned
dalam kasus yang tidak dioptimalkan.main
untuk menguji pengoptimalan. Gcc sengaja menandainya sebagai dingin dan dengan demikian menonaktifkan beberapa pengoptimalan untuknya. Saya tidak tahu apakah itu masalahnya di sini, tetapi itu adalah kebiasaan penting untuk dilakukan.bitmap
adalah global. Versi non-CSEd menggunakan operan memori kecmp
, yang tidak menjadi masalah kinerja dalam kasus ini. Jika itu lokal, kompilator bisa berasumsi pointer lain tidak bisa "tahu tentang" itu dan menunjuk ke dalamnya. Bukan ide yang buruk untuk menyimpan ekspresi yang melibatkan global dalam variabel temp, selama itu meningkatkan (atau tidak mengganggu) keterbacaan, atau jika performa sangat penting. Kecuali ada banyak hal yang terjadi, penduduk setempat seperti itu biasanya hanya tinggal di register, dan tidak akan pernah tumpah.Sebenarnya ada informasi yang tidak cukup dari cuplikan kode Anda untuk dapat diceritakan, dan satu hal yang dapat saya pikirkan adalah aliasing. Dari sudut pandang kami, cukup jelas bahwa Anda tidak ingin
p
danbitmap
menunjuk ke lokasi yang sama di memori, tetapi kompilator tidak tahu itu dan (karenap
tipechar*
) kompilator harus membuat kode ini berfungsi bahkan jikap
danbitmap
tumpang tindih.Ini berarti dalam kasus ini bahwa jika loop berubah
bitmap->width
melalui pointerp
maka itu harus dilihat saat membaca ulangbitmap->width
nanti, yang pada gilirannya berarti menyimpannya dalam variabel lokal akan ilegal.Karena itu, saya yakin beberapa kompiler terkadang benar-benar menghasilkan dua versi dari kode yang sama (saya telah melihat bukti tidak langsung dari ini, tetapi tidak pernah secara langsung mencari informasi tentang apa yang dilakukan kompiler dalam kasus ini), dan dengan cepat memeriksa apakah petunjuknya alias dan jalankan kode yang lebih cepat jika dianggap tidak apa-apa.
Yang sedang berkata, saya mendukung komentar saya tentang hanya mengukur kinerja dua versi, uang saya tidak melihat perbedaan kinerja yang konsisten antara dua versi kode.
Menurut pendapat saya, pertanyaan seperti ini boleh-boleh saja jika tujuan Anda adalah mempelajari teori dan teknik pengoptimalan compiler, tetapi hanya membuang-buang waktu (pengoptimalan mikro yang tidak berguna) jika tujuan akhir Anda di sini adalah membuat program berjalan lebih cepat.
sumber
restrict
kualifikasi menjadi jawaban untuk masalah aliasing dalam kasus ini?restrict
sebagian besar untung-untungan. MSVC adalah satu-satunya kompiler yang pernah saya lihat yang tampaknya melakukannya dengan benar. ICC kehilangan info aliasing melalui pemanggilan fungsi meskipun mereka sebaris. Dan GCC biasanya gagal mendapatkan manfaat apa pun kecuali Anda mendeklarasikan setiap parameter input sebagairestrict
(termasukthis
untuk fungsi anggota).char
alias semua jenis, jadi jika Anda memiliki char * maka Anda harus menggunakanrestrict
semuanya. Atau jika Anda telah memaksa aturan aliasing ketat GCC off dengan-fno-strict-aliasing
semua itu dianggap alias yang mungkin.restrict
-seperti semantik di C ++ adalah N4150 .Ok teman-teman, jadi saya sudah mengukur, dengan
GCC -O3
(menggunakan GCC 4.9 di Linux x64).Ternyata, versi kedua bekerja 54% lebih cepat!
Jadi, saya kira aliasing adalah masalahnya, saya belum memikirkannya.
[Sunting]
Saya sudah mencoba lagi versi pertama dengan semua petunjuk yang ditentukan dengan
__restrict__
, dan hasilnya sama. Aneh .. Entah aliasing bukanlah masalah, atau, untuk beberapa alasan, kompilator tidak mengoptimalkannya dengan baik bahkan dengan__restrict__
.[Sunting 2]
Oke, saya rasa saya cukup bisa membuktikan bahwa aliasing adalah masalahnya. Saya mengulangi pengujian asli saya, kali ini menggunakan array daripada pointer:
Dan diukur (harus menggunakan "-mcmodel = large" untuk menghubungkannya). Kemudian saya mencoba:
Hasil pengukurannya sama - Sepertinya kompilator dapat mengoptimalkannya sendiri.
Kemudian saya mencoba kode asli (dengan penunjuk
p
), kali ini ketikap
bertipestd::uint16_t*
. Sekali lagi, hasilnya sama - karena aliasing yang ketat. Kemudian saya mencoba membangun dengan "-fno-strict-aliasing", dan sekali lagi melihat perbedaan waktu.sumber
Jawaban lain telah menunjukkan bahwa mengangkat operasi pointer keluar dari loop dapat mengubah perilaku yang ditentukan karena aturan aliasing yang memungkinkan char menjadi alias apa pun dan karenanya bukan pengoptimalan yang diizinkan untuk kompiler meskipun dalam banyak kasus itu jelas benar untuk manusia. programmer.
Mereka juga telah menunjukkan bahwa mengangkat operasi keluar dari loop biasanya tetapi tidak selalu merupakan perbaikan dari sudut pandang kinerja dan seringkali negatif dari sudut pandang keterbacaan.
Saya ingin menunjukkan bahwa sering kali ada "cara ketiga". Daripada menghitung hingga jumlah iterasi yang Anda inginkan, Anda dapat menghitung mundur hingga nol. Artinya, jumlah iterasi hanya diperlukan satu kali di awal loop, tidak harus disimpan setelah itu. Lebih baik lagi di tingkat assembler, ia sering menghilangkan kebutuhan akan perbandingan eksplisit karena operasi pengurangan biasanya akan menetapkan tanda yang menunjukkan apakah penghitungnya nol baik sebelum (membawa bendera) dan setelah (bendera nol) penurunan.
Perhatikan bahwa versi pengulangan ini memberikan nilai x dalam kisaran 1..lebar daripada kisaran 0 .. (lebar-1). Itu tidak masalah dalam kasus Anda karena Anda sebenarnya tidak menggunakan x untuk apa pun tetapi itu adalah sesuatu yang harus diperhatikan. Jika Anda menginginkan loop hitung mundur dengan nilai x dalam kisaran 0 .. (lebar-1) bisa Anda lakukan.
Anda juga dapat menyingkirkan cast pada contoh di atas jika Anda mau tanpa mengkhawatirkan pengaruhnya terhadap aturan perbandingan karena semua yang Anda lakukan dengan bitmap-> width adalah menugaskannya langsung ke variabel.
sumber
x --> 0
, menghasilkan operator "downto". Cukup lucu. PS Saya tidak menganggap membuat variabel untuk kondisi akhir menjadi negatif untuk keterbacaan, sebenarnya bisa sebaliknya.static_cast<unsigned>(bitmap->width)
dan menggunakanwidth
sebagai gantinya dalam loop sebenarnya merupakan peningkatan untuk keterbacaan karena sekarang ada lebih sedikit hal yang harus diurai oleh pembaca per baris. Pandangan orang lain mungkin berbeda.do { } while()
, karena di ASM Anda membuat loop dengan cabang bersyarat di bagian akhir. Biasafor(){}
danwhile(){}
loop memerlukan instruksi tambahan untuk menguji kondisi loop satu kali sebelum loop, jika compiler tidak dapat membuktikannya selalu berjalan setidaknya satu kali. Dengan segala cara, gunakanfor()
atauwhile()
kapan berguna untuk memeriksa apakah loop bahkan harus berjalan sekali, atau ketika lebih mudah dibaca.Satu-satunya hal di sini yang dapat mencegah pengoptimalan adalah aturan aliasing yang ketat . Singkatnya :
Pengecualian juga berlaku untuk
unsigned
dansigned
char
pointer .Ini adalah kasus dalam kode Anda: Anda memodifikasi sedang
*p
melaluip
yang merupakanunsigned char*
, sehingga compiler harus mengasumsikan bahwa itu bisa menunjukkanbitmap->width
. Oleh karena itu, caching daribitmap->width
adalah optimasi yang tidak valid. Perilaku pencegahan pengoptimalan ini ditunjukkan dalam jawaban YSC .Jika dan hanya jika
p
diarahkan ke non-char
dan non-decltype(bitmap->width)
tipe, apakah caching akan menjadi pengoptimalan yang memungkinkan.sumber
Pertanyaan awalnya diajukan:
Dan jawaban saya untuk itu (mengumpulkan campuran yang bagus dari suara naik dan turun ..)
Terlepas dari downvote (dan sekarang melihat masalah aliasing), saya masih senang dengan itu sebagai jawaban yang valid. Jika Anda tidak tahu apakah perlu mengoptimalkan sesuatu, mungkin tidak.
Pertanyaan yang agak berbeda, tentu saja, adalah ini:
Pertama, apakah aplikasi atau pustaka Anda perlu berjalan lebih cepat daripada saat ini? Apakah pengguna menunggu terlalu lama? Apakah perangkat lunak Anda meramalkan cuaca kemarin, bukan besok?
Hanya Anda yang benar-benar dapat mengetahui hal ini, berdasarkan untuk apa perangkat lunak Anda dan apa yang diharapkan pengguna Anda.
Dengan asumsi perangkat lunak Anda memerlukan pengoptimalan, hal berikutnya yang harus dilakukan adalah mulai mengukur. Profiler akan memberi tahu Anda di mana kode Anda menghabiskan waktunya. Jika fragmen Anda tidak muncul sebagai hambatan, sebaiknya biarkan saja. Profiler dan alat ukur lainnya juga akan memberi tahu Anda jika perubahan Anda telah membuat perbedaan. Anda dapat menghabiskan waktu berjam-jam untuk mencoba mengoptimalkan kode, hanya untuk mengetahui bahwa Anda tidak membuat perbedaan yang terlihat.
Jika Anda tidak menulis kode 'dioptimalkan', maka kode Anda harus sejelas, bersih, dan ringkas seperti yang Anda bisa membuatnya. Argumen "Pengoptimalan prematur itu jahat" bukanlah alasan untuk kode yang ceroboh atau tidak efisien.
Kode yang dioptimalkan biasanya mengorbankan beberapa atribut di atas untuk performa. Ini bisa melibatkan pengenalan variabel lokal tambahan, memiliki objek dengan cakupan yang lebih luas dari yang diharapkan atau bahkan membalik urutan loop normal. Semua ini mungkin kurang jelas atau ringkas, jadi dokumentasikan kodenya (secara singkat!) Tentang mengapa Anda melakukan ini.
Namun seringkali, dengan kode 'lambat', pengoptimalan mikro ini adalah pilihan terakhir. Tempat pertama untuk melihat adalah algoritme dan struktur data. Adakah cara untuk menghindari melakukan pekerjaan sama sekali? Bisakah pencarian linier diganti dengan pencarian biner? Apakah daftar tertaut lebih cepat di sini daripada vektor? Atau tabel hash? Bisakah saya menyimpan hasil? Membuat keputusan 'efisien' yang baik di sini sering kali dapat memengaruhi kinerja dengan urutan besarnya atau lebih!
sumber
Saya menggunakan pola berikut dalam situasi seperti ini. Ini hampir sependek kasus pertama Anda, dan lebih baik daripada kasus kedua, karena itu membuat variabel sementara tetap lokal ke loop.
Ini akan lebih cepat dengan compiler kurang dari smart, build debug, atau tanda kompilasi tertentu.
Sunting1 : Menempatkan operasi konstan di luar loop adalah pola pemrograman yang baik . Ini menunjukkan pemahaman tentang dasar-dasar operasi mesin, terutama di C / C ++. Saya berpendapat bahwa upaya untuk membuktikan diri harus dilakukan pada orang yang tidak mengikuti praktik ini. Jika kompilator menghukum untuk pola yang baik, itu adalah bug di kompilator.
Sunting2:: Saya telah mengukur saran saya terhadap kode asli pada vs2013, mendapat peningkatan% 1. Bisakah kita berbuat lebih baik? Pengoptimalan manual sederhana memberikan peningkatan 3 kali lipat dari loop asli pada mesin x64 tanpa menggunakan instruksi yang tidak biasa. Kode di bawah ini mengasumsikan sistem little endian dan bitmap selaras dengan benar. TEST 0 adalah asli (9 detik), TEST 1 lebih cepat (3 detik). Saya yakin seseorang dapat membuat ini lebih cepat, dan hasil pengujian akan bergantung pada ukuran bitmap. Pasti segera di masa depan, compiler akan dapat menghasilkan kode tercepat secara konsisten. Saya khawatir ini akan menjadi masa depan ketika kompiler juga akan menjadi programmer AI, jadi kami akan kehilangan pekerjaan. Tetapi untuk saat ini, cukup tulis kode yang menunjukkan bahwa Anda tahu bahwa operasi tambahan dalam loop tidak diperlukan.
sumber
Ada dua hal yang perlu diperhatikan.
A) Seberapa sering pengoptimalan akan berjalan?
Jika jawabannya tidak terlalu sering, seperti hanya ketika pengguna mengklik tombol, maka jangan repot-repot jika membuat kode Anda tidak dapat dibaca. Jika jawabannya 1000 kali per detik maka Anda mungkin ingin melanjutkan dengan pengoptimalan. Jika ini agak rumit, pastikan untuk memberikan komentar untuk menjelaskan apa yang terjadi untuk membantu pria berikutnya yang datang.
B) Apakah ini akan membuat kode lebih sulit untuk dipelihara / dipecahkan?
Jika Anda tidak melihat peningkatan besar dalam kinerja, maka membuat kode Anda samar hanya untuk menghemat beberapa waktu bukanlah ide yang baik. Banyak orang akan memberi tahu Anda bahwa programmer yang baik harus dapat melihat kode dan mencari tahu apa yang sedang terjadi. Ini benar. Masalahnya adalah bahwa dalam dunia bisnis, waktu ekstra untuk menghitungnya membutuhkan uang. Jadi, jika Anda bisa membuatnya lebih cantik untuk dibaca, lakukanlah. Temanmu akan berterima kasih untuk itu.
Yang mengatakan saya pribadi akan menggunakan contoh B.
sumber
Kompiler dapat mengoptimalkan banyak hal. Sebagai contoh, Anda harus mencari readability, mantainability dan apa yang mengikuti standar kode Anda. Untuk informasi lebih lanjut tentang apa yang dapat dioptimalkan (dengan GCC), lihat posting blog ini .
sumber
Sebagai aturan umum, biarkan kompilator melakukan pengoptimalan untuk Anda, sampai Anda memutuskan bahwa Anda harus mengambil alih. Logika untuk ini tidak ada hubungannya dengan kinerja, melainkan dengan keterbacaan manusia. Dalam sebagian besar kasus, keterbacaan program Anda lebih penting daripada kinerjanya. Anda harus bertujuan untuk menulis kode yang lebih mudah dibaca oleh manusia, dan kemudian hanya mengkhawatirkan tentang pengoptimalan jika Anda yakin bahwa kinerja lebih penting daripada pemeliharaan kode Anda.
Setelah Anda melihat bahwa performa itu penting, Anda harus menjalankan profiler pada kode untuk menentukan loop mana yang tidak efisien, dan mengoptimalkannya satu per satu. Mungkin memang ada kasus di mana Anda ingin melakukan pengoptimalan itu (terutama jika Anda bermigrasi ke C ++, di mana penampung STL terlibat), tetapi biaya dalam hal keterbacaan sangat besar.
Selain itu, saya dapat memikirkan situasi patologis yang sebenarnya dapat memperlambat kode. Misalnya, pertimbangkan kasus di mana kompilator tidak dapat membuktikan bahwa
bitmap->width
itu konstan selama proses. Dengan menambahkanwidth
variabel, Anda memaksa kompilator untuk mempertahankan variabel lokal dalam cakupan itu. Jika, karena alasan khusus platform, variabel ekstra itu mencegah beberapa optimasi ruang-tumpukan, mungkin harus mengatur ulang bagaimana ia memancarkan bytecode, dan menghasilkan sesuatu yang kurang efisien.Sebagai contoh, pada Windows x64, seseorang diwajibkan untuk memanggil API call khusus,
__chkstk
dalam pembukaan fungsi jika fungsi tersebut akan menggunakan lebih dari 1 halaman variabel lokal. Fungsi ini memberi jendela kesempatan untuk mengelola halaman penjaga yang mereka gunakan untuk memperluas tumpukan saat diperlukan. Jika variabel ekstra Anda mendorong penggunaan tumpukan dari bawah 1 halaman ke di-atau-di atas 1 halaman, fungsi Anda sekarang wajib dipanggil__chkstk
setiap kali dimasukkan. Jika Anda mengoptimalkan loop ini pada jalur lambat, Anda sebenarnya dapat memperlambat jalur cepat lebih dari yang Anda simpan pada jalur lambat!Tentu, ini sedikit patologis, tetapi inti dari contoh itu adalah Anda sebenarnya dapat memperlambat kompilator. Ini hanya menunjukkan bahwa Anda harus membuat profil pekerjaan Anda untuk menentukan ke mana pengoptimalan pergi. Sementara itu, harap jangan mengorbankan keterbacaan dengan cara apa pun untuk pengoptimalan yang mungkin atau mungkin tidak penting.
sumber
The perbandingan yang salah sejak dua potongan kode
dan
tidak setara
Dalam kasus pertama
width
adalah dependen dan bukan const, dan orang tidak dapat berasumsi bahwa itu mungkin tidak berubah di antara iterasi berikutnya. Jadi tidak dapat dioptimalkan, tetapi harus diperiksa di setiap loop .Dalam kasus Anda yang dioptimalkan, variabel lokal diberi nilai
bitmap->width
di beberapa titik selama eksekusi program. Kompilator dapat memverifikasi bahwa ini tidak benar-benar berubah.Apakah Anda berpikir tentang multi threading, atau mungkin nilainya dapat bergantung secara eksternal sedemikian rupa sehingga nilainya tidak stabil. Bagaimana seseorang mengharapkan kompilator untuk mengetahui semua hal ini jika Anda tidak memberi tahu?
Kompilator hanya dapat melakukan sebaik yang dimungkinkan oleh kode Anda.
sumber
Kecuali Anda tahu bagaimana tepatnya kompilator mengoptimalkan kode, lebih baik lakukan pengoptimalan Anda sendiri dengan menjaga keterbacaan kode, dan desain. Praktisnya sulit untuk memeriksa kode assembly untuk setiap fungsi yang kami tulis untuk versi compiler baru.
sumber
Kompilator tidak dapat mengoptimalkan
bitmap->width
karena nilai dariwidth
dapat diubah di antara iterasi. Ada beberapa alasan paling umum:iterator::end()
ataucontainer::size()
sehingga sulit untuk memprediksi apakah itu akan selalu mengembalikan hasil yang sama.Untuk meringkas (pendapat pribadi saya) untuk tempat-tempat yang membutuhkan optimasi tingkat tinggi Anda perlu melakukannya sendiri, di tempat lain biarkan saja, kompiler dapat mengoptimalkannya atau tidak, jika tidak ada perbedaan besar pembacaan kode adalah target utama.
sumber