Ini adalah bug Dentang
... saat menggarisbawahi suatu fungsi yang mengandung infinite loop. Perilaku ini berbeda ketika while(1);
muncul langsung di utama, yang baunya sangat buggy bagi saya.
Lihat jawaban @ Arnavion untuk ringkasan dan tautan. Sisa jawaban ini ditulis sebelum saya mendapat konfirmasi bahwa itu adalah bug, apalagi bug yang dikenal.
Untuk menjawab pertanyaan judul: Bagaimana cara membuat loop kosong tanpa batas yang tidak akan dioptimalkan? ? -
buat die()
makro, bukan fungsi , untuk mengatasi bug ini di Dentang 3.9 dan yang lebih baru. (Sebelumnya versi dentang baik membuat loop atau memancarkancall
ke versi non-inline fungsi dengan loop tak terbatas.) Itu muncul untuk menjadi aman bahkan jika print;while(1);print;
inlines fungsi menjadi nya pemanggil ( Godbolt ). -std=gnu11
vs -std=gnu99
tidak mengubah apa pun.
Jika Anda hanya peduli dengan GNU C, P__J___ di__asm__("");
dalam loop juga berfungsi, dan seharusnya tidak merusak optimasi kode di sekitarnya untuk setiap kompiler yang memahaminya. GNU C Pernyataan asm dasar secara implisitvolatile
, jadi ini dianggap sebagai efek samping yang terlihat yang harus "mengeksekusi" sebanyak yang akan terjadi pada mesin abstrak C. (Dan ya, Clang mengimplementasikan dialek C GNU, seperti yang didokumentasikan oleh manual GCC.)
Beberapa orang berpendapat bahwa mungkin saja mengoptimalkan loop kosong tanpa batas yang sah. Saya tidak setuju 1 , tetapi bahkan jika kami menerimanya, tidak bisa juga legal bagi Dentang untuk mengambil pernyataan setelah perulangan tidak dapat dijangkau, dan membiarkan eksekusi jatuh dari ujung fungsi ke fungsi berikutnya, atau menjadi sampah yang menerjemahkan sebagai instruksi acak.
(Itu akan memenuhi standar untuk Clang ++ (tapi masih tidak terlalu berguna); loop tak terbatas tanpa efek samping adalah UB dalam C ++, tetapi tidak C.
Apakah sementara (1); perilaku tidak terdefinisi dalam C? UB memungkinkan kompiler memancarkan apa pun pada dasarnya untuk kode di jalur eksekusi yang pasti akan menemui UB. asm
Pernyataan dalam loop akan menghindari UB ini untuk C ++. Namun dalam praktiknya, Clang mengkompilasi sebagai C ++ tidak menghapus ekspresi konstan loop kosong tak terbatas kecuali saat inlining, sama seperti ketika kompilasi sebagai C.)
Inlining secara manual while(1);
mengubah cara Clang mengkompilasinya: infinite loop hadir dalam asm. Ini adalah apa yang kami harapkan dari POV aturan-pengacara.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Di explorer compiler Godbolt, Dentang 9.0 -O3 dikompilasi sebagai C ( -xc
) untuk x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Kompiler yang sama dengan opsi yang sama mengkompilasi a main
yang memanggil infloop() { while(1); }
ke yang sama pertama puts
, tetapi kemudian hanya berhenti memancarkan instruksi untuk main
setelah titik itu. Jadi seperti yang saya katakan, eksekusi hanya jatuh dari ujung fungsi, ke fungsi apa pun yang berikutnya (tetapi dengan tumpukan tidak selaras untuk entri fungsi sehingga bahkan tidak ada panggilan masuk yang valid).
Opsi yang valid adalah untuk
- memancarkan
label: jmp label
loop tak terbatas
- atau (jika kami menerima bahwa loop tak terbatas dapat dihapus) memancarkan panggilan lain untuk mencetak string 2, dan kemudian
return 0
dari main
.
Menerjang atau melanjutkan tanpa mencetak "tidak dapat dijangkau" jelas tidak ok untuk implementasi C11, kecuali ada UB yang belum saya perhatikan.
Catatan kaki 1:
Sebagai catatan, saya setuju dengan jawaban @ Lundin yang mengutip standar untuk bukti bahwa C11 tidak mengizinkan asumsi penghentian untuk loop tak terbatas ekspresi konstan, bahkan ketika mereka kosong (tidak ada I / O, volatile, sinkronisasi, atau lainnya efek samping yang terlihat).
Ini adalah serangkaian kondisi yang memungkinkan loop dikompilasi ke loop asm kosong untuk CPU normal. (Bahkan jika tubuh tidak kosong di sumber, tugas ke variabel tidak dapat dilihat oleh utas lain atau penangan sinyal tanpa data-ras UB saat loop sedang berjalan. Jadi implementasi yang sesuai dapat menghapus tubuh loop seperti itu jika diinginkan Kemudian yang meninggalkan pertanyaan apakah loop itu sendiri dapat dihapus. ISO C11 secara eksplisit mengatakan tidak.)
Mengingat bahwa C11 memilih kasus itu sebagai kasus di mana implementasi tidak dapat mengasumsikan loop berakhir (dan bahwa itu bukan UB), tampaknya jelas mereka berniat loop hadir pada saat run-time. Implementasi yang menargetkan CPU dengan model eksekusi yang tidak dapat melakukan jumlah pekerjaan tak terbatas dalam waktu terbatas tidak memiliki alasan untuk menghapus loop infinite konstan kosong. Atau bahkan secara umum, kata-kata yang tepat adalah tentang apakah mereka dapat "diasumsikan berakhir" atau tidak. Jika sebuah loop tidak dapat diakhiri, itu berarti kode yang lebih baru tidak dapat dijangkau, tidak peduli argumen apa yang Anda buat tentang matematika dan ketidakterbatasan dan berapa lama waktu yang diperlukan untuk melakukan pekerjaan tak terbatas pada beberapa mesin hipotetis.
Lebih jauh dari itu, Clang bukan hanya DeathStation 9000 yang sesuai dengan ISO C, itu dimaksudkan untuk berguna untuk pemrograman sistem tingkat rendah dunia nyata, termasuk kernel dan hal-hal yang tertanam. Jadi apakah Anda menerima argumen tentang C11 yang memungkinkan penghapusan while(1);
, tidak masuk akal bahwa Clang ingin benar-benar melakukan itu. Jika Anda menulis while(1);
, itu mungkin bukan kecelakaan. Penghapusan loop yang berakhir tanpa batas oleh kecelakaan (dengan ekspresi kontrol variabel runtime) dapat berguna, dan masuk akal bagi kompiler untuk melakukan itu.
Jarang Anda ingin hanya berputar sampai interupsi berikutnya, tetapi jika Anda menuliskannya dalam C, itulah yang Anda harapkan akan terjadi. (Dan apa yang terjadi di GCC dan dentang, kecuali untuk dentang ketika loop tak terbatas di dalam fungsi pembungkus).
Misalnya, dalam kernel OS primitif, ketika scheduler tidak memiliki tugas untuk menjalankannya mungkin menjalankan tugas idle. Implementasi pertama mungkin while(1);
.
Atau untuk perangkat keras tanpa fitur siaga hemat daya, itu mungkin satu-satunya implementasi. (Sampai awal 2000-an, itu saya pikir tidak jarang pada x86. Meskipun hlt
instruksi itu ada, IDK jika itu menghemat jumlah daya yang berarti sampai CPU mulai memiliki status siaga daya rendah.)