Apakah level optimisasi -O3 berbahaya di g ++?

233

Saya telah mendengar dari berbagai sumber (meskipun sebagian besar dari kolega saya), bahwa kompilasi dengan level optimisasi -O3dalam g ++ entah bagaimana 'berbahaya', dan harus dihindari secara umum kecuali terbukti perlu.

Apakah ini benar, dan jika demikian, mengapa? Haruskah saya bertahan -O2?

Dunnie
sumber
38
Ini hanya berbahaya jika Anda mengandalkan perilaku yang tidak terdefinisi. Dan bahkan kemudian saya akan terkejut jika itu adalah tingkat optimasi yang mengacaukan sesuatu.
Seth Carnegie
5
Kompilator masih dibatasi untuk menghasilkan program yang berperilaku "seolah-olah" itu mengkompilasi kode Anda dengan tepat. Saya tidak tahu itu -O3dianggap sangat buggy? Saya pikir mungkin itu dapat membuat perilaku yang tidak terdefinisi "lebih buruk" karena dapat melakukan hal-hal aneh dan indah berdasarkan asumsi tertentu, tetapi itu akan menjadi kesalahan Anda sendiri. Jadi secara umum, saya akan mengatakan itu baik-baik saja.
BoBTFish
5
Memang benar bahwa tingkat optimasi yang lebih tinggi lebih rentan terhadap bug penyusun. Saya sendiri telah menemukan beberapa kasus, tetapi secara umum masih sangat jarang.
Mysticial
21
-O2menyala -fstrict-aliasing, dan jika kode Anda bertahan maka mungkin akan bertahan optimasi lain, karena itu salah bahwa orang salah lagi. Yang mengatakan, -fpredictive-commoninghanya dalam -O3, dan memungkinkan yang memungkinkan bug dalam kode Anda disebabkan oleh asumsi yang salah tentang konkurensi. Semakin sedikit salah kode Anda, optimisasi yang kurang berbahaya adalah ;-)
Steve Jessop
6
@PlasmaHH, saya tidak berpikir "lebih ketat" adalah deskripsi yang baik tentang -Ofast, mematikan penanganan yang sesuai dengan IEEE misalnya
Jonathan Wakely

Jawaban:

223

Pada hari-hari awal gcc (2,8 dll) dan pada masa egcs, dan redhat 2,96 -O3 kadang-kadang cukup buggy. Tapi ini lebih dari satu dekade yang lalu, dan -O3 tidak jauh berbeda dari tingkat optimisasi lainnya (dalam buggyness).

Namun, ia cenderung mengungkapkan kasus-kasus di mana orang mengandalkan perilaku yang tidak terdefinisi, karena lebih mengandalkan aturan, dan terutama kasus sudut, dari bahasa.

Sebagai catatan pribadi, saya menjalankan perangkat lunak produksi di sektor keuangan selama bertahun-tahun sekarang dengan -O3 dan belum menemukan bug yang tidak akan ada di sana jika saya akan menggunakan -O2.

Dengan permintaan populer, berikut tambahan:

-O3 dan terutama flag-flag tambahan seperti -funroll-loop (tidak diaktifkan oleh -O3) terkadang dapat menyebabkan lebih banyak kode mesin yang dihasilkan. Dalam keadaan tertentu (misalnya pada cpu dengan cache instruksi L1 sangat kecil) ini dapat menyebabkan perlambatan karena semua kode misalnya beberapa loop batin sekarang tidak pas lagi ke L1I. Umumnya gcc berusaha cukup keras untuk tidak menghasilkan kode begitu banyak, tetapi karena biasanya mengoptimalkan kasus generik, ini bisa terjadi. Opsi yang cenderung rentan terhadap hal ini (seperti loop terbuka) biasanya tidak termasuk dalam -O3 dan ditandai sesuai di halaman manual. Karena itu umumnya merupakan ide yang baik untuk menggunakan -O3 untuk menghasilkan kode cepat, dan hanya kembali ke -O2 atau -Os (yang mencoba untuk mengoptimalkan ukuran kode) saat yang tepat (misalnya ketika profiler menunjukkan L1I meleset).

Jika Anda ingin mengambil optimasi ke ekstrem, Anda dapat men-tweak dalam gcc melalui --param biaya yang terkait dengan optimasi tertentu. Selain itu perhatikan bahwa gcc sekarang memiliki kemampuan untuk menempatkan atribut pada fungsi yang mengontrol pengaturan optimisasi hanya untuk fungsi-fungsi ini, jadi ketika Anda menemukan Anda memiliki masalah dengan -O3 dalam satu fungsi (atau ingin mencoba flag khusus untuk fungsi itu), Anda tidak perlu mengkompilasi seluruh file atau bahkan seluruh proyek dengan O2.

otoh tampaknya harus diperhatikan saat menggunakan -Ofast, yang menyatakan:

-Ofast memungkinkan semua -O3 optimasi. Ini juga memungkinkan optimasi yang tidak valid untuk semua program yang sesuai standar.

yang membuat saya menyimpulkan bahwa -O3 dimaksudkan untuk sepenuhnya memenuhi standar.

PlasmaHH
sumber
2
Saya hanya menggunakan sesuatu seperti yang sebaliknya. Saya selalu menggunakan -Os atau -O2 (kadang-kadang O2 menghasilkan executable yang lebih kecil) .. setelah profil saya menggunakan O3 pada bagian kode yang membutuhkan waktu eksekusi lebih dan itu saja dapat memberikan kecepatan hingga 20% lebih banyak.
CoffeDeveloper
3
Saya melakukan itu untuk kecepatan. O3 paling sering membuat segalanya lebih lambat. Tidak tahu persis mengapa, saya menduga itu mencemari cache Instruksi.
CoffeDeveloper
4
@DarioOO Saya merasa seperti memohon "kode mengasapi" adalah hal yang populer untuk dilakukan, tetapi saya hampir tidak pernah melihatnya didukung dengan tolok ukur. Itu sangat tergantung pada arsitektur, tetapi setiap kali saya melihat tolok ukur yang dipublikasikan (misalnya phoronix.com/... ) itu menunjukkan O3 menjadi lebih cepat di sebagian besar kasus. Saya telah melihat pembuatan profil dan analisis cermat yang diperlukan untuk membuktikan bahwa kode mengasapi sebenarnya merupakan masalah, dan biasanya hanya terjadi pada orang yang menggunakan templat secara ekstrem.
Nir Friedman
1
@NirFriedman: Ini cenderung mendapatkan masalah ketika model biaya inlining dari kompiler memiliki bug, atau ketika Anda mengoptimalkan untuk target yang sama sekali berbeda dari yang Anda jalankan. Menariknya ini berlaku untuk semua tingkat optimasi ...
PlasmaHH
1
@PlasmaHH: masalah menggunakan-cmov akan sulit untuk diperbaiki untuk kasus umum. Biasanya Anda tidak hanya mengurutkan data Anda, jadi ketika gcc mencoba untuk memutuskan apakah cabang dapat diprediksi atau tidak, analisis statis mencari panggilan ke std::sortfungsi tidak mungkin membantu. Menggunakan sesuatu seperti stackoverflow.com/questions/109710/… akan membantu, atau mungkin menulis sumber untuk mengambil keuntungan dari pengurutan: pindai hingga Anda melihat> = 128, kemudian mulai menjumlahkan. Adapun kode kembung, ya saya bermaksud untuk melaporkannya. : P
Peter Cordes
42

Dalam pengalaman saya yang agak kotak-kotak, menerapkan -O3ke seluruh program hampir selalu membuatnya lebih lambat (relatif terhadap -O2), karena itu diaktifkan pada loop agresif membuka gulungan dan inlining yang membuat program tidak lagi sesuai dengan cache instruksi. Untuk program yang lebih besar, ini juga berlaku untuk -O2relatif -Os!

Pola penggunaan yang dimaksudkan -O3adalah, setelah membuat profil program Anda, Anda menerapkannya secara manual ke segelintir kecil file yang berisi loop internal kritis yang sebenarnya mendapat manfaat dari pengorbanan ruang-untuk-kecepatan yang agresif ini. Versi GCC yang lebih baru memiliki mode optimasi berpandu profil yang dapat (IIUC) secara selektif menerapkan -O3optimasi ke fungsi-fungsi panas - mengotomatiskan proses ini secara efektif.

zwol
sumber
10
"hampir selalu"? Jadikan "50-50", dan kita akan sepakat ;-).
No-Bugs Hare
12

Opsi -O3 mengaktifkan pengoptimalan yang lebih mahal, seperti fungsi inlining, selain semua pengoptimalan level bawah '-O2' dan '-O1'. Level optimisasi '-O3' dapat meningkatkan kecepatan yang dapat dieksekusi, tetapi juga dapat meningkatkan ukurannya. Dalam beberapa keadaan di mana optimasi ini tidak menguntungkan, opsi ini mungkin sebenarnya membuat program lebih lambat.

neel
sumber
3
Saya mengerti bahwa beberapa "optimisasi nyata" mungkin membuat program lebih lambat, tetapi apakah Anda memiliki sumber yang mengklaim bahwa GCC -O3 telah membuat program lebih lambat?
Mooing Duck
1
@ MoingDuck: Walaupun saya tidak bisa mengutip sumber, saya ingat mengalami kasus seperti itu dengan beberapa prosesor AMD yang lebih tua yang memiliki cache L1I yang cukup kecil (~ instruksi 10k). Saya yakin Google memiliki lebih banyak untuk yang tertarik, tetapi terutama opsi seperti loop membuka gulungan bukan bagian dari O3, dan itu meningkatkan ukuran banyak. -Os adalah saat Anda ingin membuat eksekusi terkecil. Bahkan -O2 dapat meningkatkan ukuran kode. Alat yang bagus untuk bermain dengan hasil dari berbagai tingkat optimasi adalah penjelajah gcc.
PlasmaHH
@PlasmaHH: Sebenarnya, ukuran cache yang kecil adalah sesuatu yang bisa dikompilasi oleh kompiler, bagus. Itu contoh yang sangat bagus. Silakan letakkan di jawabannya.
Mooing Duck
1
@PlasmaHH Pentium III memiliki cache kode 16KB. AMD K6 ke atas sebenarnya memiliki cache instruksi 32KB. P4 dimulai dengan nilai sekitar 96KB. Core I7 sebenarnya memiliki kode cache L1 32KB. Dekoder instruksi kuat saat ini, jadi L3 Anda cukup bagus untuk digunakan kembali di hampir semua loop.
doug65536
1
Anda akan melihat peningkatan kinerja yang luar biasa setiap kali ada fungsi yang disebut dalam satu lingkaran dan itu dapat melakukan penghapusan subekspresi umum yang signifikan dan mengangkat perhitungan ulang yang tidak perlu dari fungsi sebelum loop.
doug65536
8

Ya, O3 adalah buggier. Saya seorang pengembang kompiler dan saya telah mengidentifikasi bug gcc yang jelas dan jelas disebabkan oleh O3 yang menghasilkan instruksi perakitan SIMD buggy ketika membangun perangkat lunak saya sendiri. Dari apa yang saya lihat, sebagian besar perangkat lunak produksi menyertakan O2 yang berarti O3 akan kurang mendapat perhatian pengujian pengujian dan perbaikan bug.

Pikirkan seperti ini: O3 menambahkan lebih banyak transformasi di atas O2, yang menambahkan lebih banyak transformasi di atas O1. Secara statistik, lebih banyak transformasi berarti lebih banyak bug. Itu berlaku untuk kompiler mana pun.

David Yeager
sumber
3

Baru-baru ini saya mengalami masalah menggunakan optimasi dengan g++. Masalahnya terkait dengan kartu PCI, di mana register (untuk perintah dan data) diwakili oleh alamat memori. Driver saya memetakan alamat fisik ke sebuah pointer di dalam aplikasi dan memberikannya ke proses yang dipanggil, yang bekerja dengannya seperti ini:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

Kartu tidak bertindak seperti yang diharapkan. Ketika saya melihat perakitan aku mengerti bahwa compiler hanya menulis someCommand[ the last ]ke dalam pciMemory, menghilangkan semua menulis sebelumnya.

Kesimpulannya: akurat dan penuh perhatian dengan optimasi.

Borisbn
sumber
38
Tetapi intinya di sini adalah bahwa program Anda hanya memiliki perilaku yang tidak jelas; pengoptimal tidak melakukan kesalahan. Khususnya Anda perlu menyatakan pciMemorysebagai volatile.
Konrad Rudolph
11
Sebenarnya bukan UB, tetapi kompiler berhak menghilangkan semua kecuali yang terakhir ditulis pciMemorykarena semua penulisan yang lain terbukti tidak berpengaruh. Untuk pengoptimal itu luar biasa karena dapat menghapus banyak instruksi yang tidak berguna dan memakan waktu.
Konrad Rudolph
4
Saya menemukan ini dalam standar (setelah 10+ tahun))) - Deklarasi volatile dapat digunakan untuk menggambarkan objek yang sesuai dengan port input / output yang dipetakan memori atau objek yang diakses oleh fungsi yang mengganggu secara tidak sinkron. Tindakan pada objek yang dideklarasikan tidak boleh '' dioptimalkan '' oleh implementasi atau disusun ulang kecuali sebagaimana diizinkan oleh aturan untuk mengevaluasi ekspresi.
borisbn
2
@borisbn Agak di luar topik, tetapi bagaimana Anda tahu bahwa perangkat Anda telah mengambil perintah sebelum mengirim perintah baru?
user877329
3
@ user877329 Saya melihatnya berdasarkan perilaku perangkat, tapi itu adalah pencarian yang bagus
borisbn