Bagaimana cara membuat loop kosong tanpa batas yang tidak akan dioptimalkan?

131

Standar C11 tampaknya menyiratkan bahwa pernyataan iterasi dengan ekspresi kontrol konstan tidak boleh dioptimalkan. Saya menerima saran saya dari jawaban ini , yang secara khusus mengutip bagian 6.8.5 dari konsep standar:

Pernyataan iterasi yang ekspresi pengontrolnya bukan ekspresi konstan ... dapat diasumsikan oleh implementasi untuk diakhiri.

Dalam jawaban itu disebutkan bahwa perulangan seperti while(1) ;tidak boleh dikenai optimasi.

Jadi ... mengapa Dentang / LLVM mengoptimalkan loop di bawah ini (dikompilasi dengan cc -O2 -std=c11 test.c -o test)?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Di mesin saya, ini dicetak begin, kemudian crash pada instruksi ilegal ( ud2jebakan ditempatkan setelah die()). Pada godbolt , kita dapat melihat bahwa tidak ada yang dihasilkan setelah panggilan ke puts.

Sudah menjadi tugas yang sangat sulit untuk mendapatkan Dentang untuk menghasilkan loop tak terbatas di bawah -O2- sementara saya bisa berulang kali menguji volatilevariabel, yang melibatkan memori baca yang tidak saya inginkan. Dan jika saya melakukan sesuatu seperti ini:

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

... Dentang cetakan begindiikuti unreachableseolah-olah infinite loop tidak pernah ada.

Bagaimana Anda membuat Dentang untuk menghasilkan loop tak terbatas tanpa-memori-akses yang tepat dengan optimisasi dihidupkan?

nneonneo
sumber
3
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Bhargav Rao
2
Tidak ada solusi portabel yang tidak melibatkan efek samping. Jika Anda tidak ingin akses memori, harapan terbaik Anda adalah register char volatile unsigned; tetapi register hilang dalam C ++ 17.
Scott M
25
Mungkin ini bukan dalam cakupan pertanyaan, tetapi saya ingin tahu mengapa Anda ingin melakukan ini. Tentunya ada beberapa cara lain untuk menyelesaikan tugas Anda yang sebenarnya. Atau ini hanya bersifat akademis?
Cruncher
1
@Cruncher: Efek dari setiap upaya tertentu untuk menjalankan program mungkin berguna, pada dasarnya tidak berguna, atau secara substansial lebih buruk daripada tidak berguna. Eksekusi yang mengakibatkan program terjebak dalam loop tanpa akhir mungkin tidak berguna, tetapi masih lebih disukai daripada perilaku lain yang mungkin diganti oleh kompiler.
supercat
6
@Cruncher: Karena kode mungkin berjalan dalam konteks yang berdiri sendiri di mana tidak ada konsep exit(), dan karena kode mungkin telah menemukan situasi di mana ia tidak dapat menjamin bahwa efek dari eksekusi lanjutan tidak akan lebih buruk daripada tidak berguna . Lompat-ke-diri adalah cara yang cukup buruk untuk menangani situasi seperti itu, tetapi mungkin ini cara terbaik untuk menangani situasi yang buruk.
supercat

Jawaban:

77

Standar C11 mengatakan ini, 6.8.5 / 6:

Pernyataan iterasi yang ekspresi pengontrolnya bukan ekspresi konstan, 156) yang tidak melakukan operasi input / output, tidak mengakses objek yang mudah menguap, dan tidak melakukan sinkronisasi atau operasi atom di dalam tubuhnya, mengendalikan ekspresi, atau (dalam kasus a untuk pernyataan) ekspresinya-3, dapat diasumsikan oleh implementasi untuk mengakhiri. 157)

Catatan dua kaki tidak normatif tetapi memberikan informasi yang berguna:

156) Ekspresi kontrol yang dihilangkan digantikan oleh konstanta bukan nol, yang merupakan ekspresi konstan.

157) Ini dimaksudkan untuk memungkinkan transformasi kompiler seperti penghapusan loop kosong bahkan ketika penghentian tidak dapat dibuktikan.

Dalam kasus Anda, while(1)adalah ekspresi konstan sebening kristal, sehingga mungkin tidak diasumsikan oleh implementasi untuk mengakhiri. Implementasi seperti itu akan putus harapan, karena loop "selamanya" adalah konstruksi pemrograman yang umum.

Apa yang terjadi pada "kode yang tidak terjangkau" setelah loop adalah, sejauh yang saya tahu, tidak didefinisikan dengan baik. Namun, dentang memang berperilaku sangat aneh. Membandingkan kode mesin dengan gcc (x86):

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

dentang 9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gcc menghasilkan loop, dentang hanya berjalan ke hutan dan keluar dengan kesalahan 255.

Saya condong ke arah ini karena tingkah laku tidak patuh. Karena saya mencoba untuk memperluas contoh Anda lebih lanjut seperti ini:

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

Saya menambahkan C11 _Noreturndalam upaya untuk membantu kompilator lebih jauh. Harus jelas bahwa fungsi ini akan menutup telepon, dari kata kunci itu saja.

setjmpakan mengembalikan 0 pada eksekusi pertama, jadi program ini seharusnya hanya menabrak while(1)dan berhenti di sana, hanya mencetak "mulai" (dengan asumsi \ n flushes stdout). Ini terjadi dengan gcc.

Jika pengulangan hanya dihapus, itu harus mencetak "mulai" 2 kali kemudian mencetak "tidak terjangkau". Namun pada dentang ( godbolt ), ia mencetak "mulai" 1 kali dan kemudian "tidak dapat dijangkau" sebelum mengembalikan kode keluar 0. Itu benar-benar salah, tidak peduli bagaimana Anda memasukkannya.

Saya tidak dapat menemukan kasus untuk mengklaim perilaku tidak terdefinisi di sini, jadi pendapat saya adalah bahwa ini adalah bug di dentang. Bagaimanapun, perilaku ini membuat dentang 100% tidak berguna untuk program seperti embedded system, di mana Anda harus bisa mengandalkan loop kekal yang menggantung program (sambil menunggu anjing penjaga dll).

Lundin
sumber
15
Saya tidak setuju pada "ini ekspresi konstan jernih, jadi mungkin tidak diasumsikan oleh implementasi untuk mengakhiri" . Ini benar-benar masuk ke lawyering bahasa rewel, tetapi 6.8.5/6dalam bentuk if (ini) maka Anda dapat menganggap (ini) . Itu tidak berarti jika tidak (ini) Anda mungkin tidak menganggap (ini) . Ini spesifikasi hanya untuk saat kondisi terpenuhi, bukan ketika mereka tidak terpenuhi di mana Anda dapat melakukan apa pun yang Anda inginkan dengan standar. Dan jika tidak ada yang bisa diamati ...
kabanus
7
@ kabanus Bagian yang dikutip adalah kasus khusus. Jika tidak (kasing khusus), evaluasi dan urut kode seperti biasa. Jika Anda terus membaca bab yang sama, ekspresi mengendalikan dievaluasi sebagaimana ditentukan untuk setiap pernyataan iterasi ("sebagaimana ditentukan oleh semantik") dengan pengecualian dari kasus khusus yang dikutip. Itu mengikuti aturan yang sama seperti evaluasi dari setiap perhitungan nilai, yang diurutkan dan didefinisikan dengan baik.
Lundin
2
Saya setuju, tetapi Anda tidak akan menduga bahwa di int z=3; int y=2; int x=1; printf("%d %d\n", x, z);sana tidak ada 2di majelis, jadi dalam arti kosong tidak berguna xsetelah ytetapi setelah zkarena optimasi. Jadi, dari kalimat terakhir Anda, kami mengikuti aturan reguler, menganggap sementara itu dihentikan (karena kami tidak dibatasi lebih baik), dan meninggalkan dalam cetakan akhir, "tidak terjangkau". Sekarang, kami mengoptimalkan pernyataan tidak berguna itu (karena kami tidak tahu yang lebih baik).
kabanus
2
@MSalters Salah satu komentar saya telah dihapus, tetapi terima kasih atas masukannya - dan saya setuju. Apa komentar saya katakan adalah saya pikir ini adalah inti dari perdebatan - adalah while(1);sama dengan int y = 2;pernyataan dalam hal apa semantik yang kita boleh optimalkan, bahkan jika logika mereka tetap di sumbernya. Dari n1528 saya mendapat kesan bahwa mereka mungkin sama, tetapi karena orang yang lebih berpengalaman daripada saya berdebat dengan cara lain, dan itu adalah bug resmi rupanya, kemudian melampaui debat filosofis tentang apakah kata-kata dalam standar itu eksplisit. , argumen tersebut diterjemahkan menjadi moot.
kabanus
2
"Implementasi seperti itu akan sangat putus asa, karena loop 'selamanya' adalah konstruksi pemrograman yang umum." - Saya mengerti sentimen tetapi argumennya cacat karena bisa diterapkan secara identik ke C ++, namun kompiler C ++ yang mengoptimalkan loop ini tidak akan rusak tetapi sesuai.
Konrad Rudolph
52

Anda perlu memasukkan ekspresi yang dapat menyebabkan efek samping.

Solusi paling sederhana:

static void die() {
    while(1)
       __asm("");
}

Tautan Godbolt

P__J__
sumber
21
Namun, tidak menjelaskan mengapa dentang berulah.
Lundin
4
Hanya mengatakan "itu bug di dentang" sudah cukup. Saya ingin mencoba beberapa hal di sini dulu, sebelum saya berteriak "bug".
Lundin
3
@Lundin Saya tidak tahu apakah itu bug. Standar dalam hal ini tidak tepat secara teknis
P__J__
4
Untungnya, GCC adalah open source dan saya dapat menulis kompiler yang mengoptimalkan contoh Anda. Dan saya bisa melakukannya untuk setiap contoh yang Anda buat, sekarang dan di masa depan.
Thomas Weller
3
@ThomasWeller: Pengembang GCC tidak akan menerima tambalan yang mengoptimalkan perulangan ini; itu akan melanggar perilaku yang dijamin = didokumentasikan. Lihat komentar saya sebelumnya: asm("")secara implisit asm volatile("");dan dengan demikian pernyataan asm harus berjalan sebanyak yang dilakukannya di mesin abstrak gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Perhatikan bahwa efek sampingnya tidak aman untuk menyertakan memori atau register; Anda perlu Memperpanjang asm dengan "memory"clobber jika Anda ingin membaca atau menulis memori yang pernah Anda akses dari C. Asm dasar hanya aman untuk hal-hal seperti asm("mfence")atau cli.)
Peter Cordes
50

Jawaban lain sudah membahas cara-cara untuk membuat Dentang memancarkan loop tak terbatas, dengan bahasa rakitan inline atau efek samping lainnya. Saya hanya ingin mengkonfirmasi bahwa ini memang bug kompiler. Secara khusus, ini adalah bug LLVM lama - ini menerapkan konsep C ++ dari "semua loop tanpa efek samping harus diakhiri" ke bahasa yang tidak seharusnya, seperti C.

Sebagai contoh, bahasa pemrograman Rust juga memungkinkan loop tak terbatas dan menggunakan LLVM sebagai backend, dan memiliki masalah yang sama.

Dalam jangka pendek, tampaknya LLVM akan terus menganggap bahwa "semua loop tanpa efek samping harus diakhiri". Untuk bahasa apa pun yang memungkinkan loop tak terbatas, LLVM mengharapkan front-end untuk memasukkan llvm.sideeffectopcode ke dalam loop tersebut. Ini adalah apa yang direncanakan Rust untuk dilakukan, jadi Dentang (ketika mengkompilasi kode C) mungkin harus melakukannya juga.

Arnavion
sumber
5
Tidak ada yang seperti bau bug yang lebih tua dari satu dekade ... dengan beberapa perbaikan dan tambalan yang diusulkan ... namun masih belum diperbaiki.
Ian Kemp
4
@IanKemp: Bagi mereka untuk memperbaiki bug sekarang akan membutuhkan pengakuan bahwa mereka telah mengambil sepuluh tahun untuk memperbaiki bug. Lebih baik bertahan harapan bahwa Standar akan berubah untuk membenarkan perilaku mereka. Tentu saja, bahkan jika standar itu berubah, itu masih tidak akan membenarkan perilaku mereka kecuali di mata orang-orang yang akan menganggap perubahan pada Standar sebagai indikasi bahwa mandat perilaku Standar sebelumnya adalah cacat yang harus diperbaiki secara surut.
supercat
4
Itu telah "diperbaiki" dalam arti bahwa LLVM menambahkan sideeffectop (pada 2017) dan mengharapkan front-end untuk memasukkan op itu ke dalam loop atas kebijakan mereka. LLVM harus memilih beberapa default untuk loop, dan kebetulan memilih salah satu yang sejalan dengan perilaku C ++, sengaja atau tidak. Tentu saja, masih ada beberapa pekerjaan optimasi yang harus dilakukan, seperti menggabungkan sideeffectops berturut-turut menjadi satu. (Inilah yang menghalangi front-end Rust untuk menggunakannya.) Jadi atas dasar itu, bug ada di front-end (dentang) yang tidak memasukkan op dalam loop.
Arnavion
@Arnavion: Apakah ada cara untuk menunjukkan bahwa operasi dapat ditunda kecuali atau sampai hasilnya digunakan, tetapi bahwa jika data akan menyebabkan program berulang tanpa henti, mencoba melanjutkan ketergantungan data masa lalu akan membuat program lebih buruk daripada tidak berguna ? Harus menambahkan efek samping palsu yang akan mencegah optimasi sebelumnya yang berguna untuk mencegah optimizer membuat program lebih buruk daripada tidak berguna tidak terdengar seperti resep untuk efisiensi.
supercat
Diskusi itu mungkin termasuk dalam milis LLVM / dentang. FWIW the LLVM commit yang menambahkan op juga mengajarkan beberapa optimisasi pass tentang hal itu. Juga, Rust bereksperimen dengan memasukkan sideeffectops ke awal setiap fungsi dan tidak melihat adanya regresi kinerja runtime. Satu-satunya masalah adalah regresi waktu kompilasi , tampaknya karena kurangnya penggabungan ops berturut-turut seperti yang saya sebutkan dalam komentar saya sebelumnya.
Arnavion
32

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=gnu11vs -std=gnu99tidak 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. asmPernyataan 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 mainyang memanggil infloop() { while(1); }ke yang sama pertama puts, tetapi kemudian hanya berhenti memancarkan instruksi untuk mainsetelah 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 labelloop tak terbatas
  • atau (jika kami menerima bahwa loop tak terbatas dapat dihapus) memancarkan panggilan lain untuk mencetak string 2, dan kemudian return 0dari 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 hltinstruksi itu ada, IDK jika itu menghemat jumlah daya yang berarti sampai CPU mulai memiliki status siaga daya rendah.)

Peter Cordes
sumber
1
Karena penasaran, adakah yang benar-benar menggunakan dentang untuk sistem embedded? Saya belum pernah melihatnya dan saya bekerja secara eksklusif dengan embedded. gcc hanya "baru-baru ini" (10 tahun yang lalu) memasuki pasar tertanam dan saya menggunakannya secara skeptis, lebih disukai dengan optimasi rendah dan selalu dengan -ffreestanding -fno-strict-aliasing. Ini berfungsi baik dengan ARM dan mungkin dengan AVR lama.
Lundin
1
@Lundin: IDK tentang embedded, tapi ya orang membangun kernel dengan dentang, setidaknya terkadang Linux. Agaknya juga Darwin untuk MacOS.
Peter Cordes
2
bugs.llvm.org/show_bug.cgi?id=965 bug ini terlihat relevan, tapi saya tidak yakin apa yang kami lihat di sini.
bracco23
1
@ Lundin - Saya cukup yakin kami menggunakan GCC (dan banyak toolkit lainnya) untuk pekerjaan tertanam sepanjang tahun 90-an, dengan RTOS seperti VxWorks dan PSOS. Saya tidak mengerti mengapa Anda mengatakan GCC baru saja memasuki pasar tertanam.
Jeff Learman
1
@JeffLearman Menjadi arus utama baru-baru ini? Bagaimanapun, kegagalan aliasing ketat gcc hanya terjadi setelah pengenalan C99, dan versi yang lebih baru tampaknya tidak lagi menjadi pisang setelah menghadapi pelanggaran alias yang ketat juga. Namun, saya tetap skeptis setiap kali saya menggunakannya. Adapun dentang, versi terbaru ternyata benar-benar rusak ketika datang ke loop kekal, sehingga tidak dapat digunakan untuk sistem embedded.
Lundin
14

Hanya sebagai catatan, Clang juga bertingkah dengan goto:

static void die() {
nasty:
    goto nasty;
}

int main() {
    int x; printf("begin\n");
    die();
    printf("unreachable\n");
}

Ini menghasilkan output yang sama seperti pada pertanyaan, yaitu:

main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

Saya melihat tidak melihat cara untuk membaca ini sebagaimana diizinkan dalam C11, yang hanya mengatakan:

6.8.6.1 (2) gotoPernyataan menyebabkan lompatan tanpa syarat ke pernyataan diawali oleh label bernama dalam fungsi melampirkan.

Karena gotoini bukan "pernyataan iterasi" (6.8.5 daftar while, dodan for) tidak ada apa-apa tentang indulgensi khusus "diasumsikan pemutusan" berlaku, namun Anda ingin membacanya.

Per kompiler tautan Godbolt pertanyaan asli adalah x86-64 Dentang 9.0.0 dan flag-flagnya adalah -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c

Dengan yang lain seperti x86-64 GCC 9.2 Anda mendapatkan yang cukup sempurna:

.LC0:
  .string "begin"
main:
  sub rsp, 8
  mov edi, OFFSET FLAT:.LC0
  call puts
.L2:
  jmp .L2

Bendera: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c

Jonathanjo
sumber
Implementasi yang sesuai dapat memiliki batas terjemahan tidak berdokumen pada waktu eksekusi atau siklus CPU yang dapat menyebabkan perilaku sewenang-wenang jika terlampaui, atau jika input program yang dibuat melebihi batas yang tidak terhindarkan. Hal-hal seperti itu adalah masalah Kualitas Implementasi, di luar yurisdiksi Standar. Tampaknya aneh bahwa para pemelihara dentang akan sangat bersikeras pada hak mereka untuk menghasilkan implementasi yang berkualitas buruk, tetapi Standar memang mengizinkannya.
supercat
2
@supercat terima kasih atas komentarnya ... mengapa melebihi batas terjemahan akan melakukan hal lain selain gagal dalam fase terjemahan dan menolak untuk mengeksekusi? Juga: " 5.1.1.3 Diagnostik Implementasi yang sesuai harus menghasilkan ... pesan diagnostik ... jika unit terjemahan preprocessing atau unit terjemahan berisi pelanggaran terhadap aturan atau batasan sintaksis apa pun ...". Saya tidak bisa melihat bagaimana perilaku yang keliru pada fase eksekusi bisa sesuai.
jonathanjo
Standar ini akan sangat mustahil untuk diterapkan jika semua batasan implementasi harus diselesaikan pada waktu pembangunan, karena orang dapat menulis program Strictly Conforming yang akan membutuhkan lebih banyak byte tumpukan daripada jumlah atom di alam semesta. Tidak jelas apakah batasan runtime harus disamakan dengan "batas terjemahan", tetapi konsesi seperti itu jelas diperlukan, dan tidak ada kategori lain di mana ia dapat diletakkan.
supercat
1
Saya merespons komentar Anda tentang "batas terjemahan". Tentu saja ada juga batas eksekusi, saya akui saya tidak mengerti mengapa Anda menyarankan mereka harus dikelompokkan dengan batas terjemahan atau mengapa Anda mengatakan itu perlu. Saya hanya tidak melihat alasan untuk mengatakan nasty: goto nastybisa menyesuaikan dan tidak memutar CPU (s) sampai pengguna atau sumber daya mengalami gangguan.
jonathanjo
1
Standar tidak membuat referensi ke "batas eksekusi" yang bisa saya temukan. Hal-hal seperti fungsi panggilan bersarang biasanya ditangani oleh alokasi stack, tetapi pelaksanaan sesuai yang batas berfungsi panggilan ke kedalaman 16 bisa membangun 16 salinan dari setiap fungsi, dan memiliki panggilan untuk bar()dalam foo()diproses sebagai panggilan dari __1fooke __2bar, dari __2fooke __3bar, dll dan dari __16fooke __launch_nasal_demons, yang kemudian akan memungkinkan semua objek otomatis dialokasikan secara statis, dan akan membuat apa yang biasanya menjadi batas "waktu berjalan" menjadi batas terjemahan.
supercat
5

Saya akan berperan sebagai pendukung iblis dan berpendapat bahwa standar tersebut tidak secara eksplisit melarang kompiler untuk mengoptimalkan loop tanpa batas.

Pernyataan iterasi yang ekspresi pengontrolnya bukan ekspresi konstan, 156) yang tidak melakukan operasi input / output, tidak mengakses objek yang mudah menguap, dan tidak melakukan sinkronisasi atau operasi atom di dalam tubuhnya, mengendalikan ekspresi, atau (dalam kasus a untuk pernyataan) ekspresinya-3, dapat diasumsikan oleh implementasi untuk mengakhiri.157)

Mari kita uraikan ini. Pernyataan iterasi yang memenuhi kriteria tertentu dapat diasumsikan berakhir:

if (satisfiesCriteriaForTerminatingEh(a_loop)) 
    if (whatever_reason_or_just_because_you_feel_like_it)
         assumeTerminates(a_loop);

Ini tidak mengatakan apa-apa tentang apa yang terjadi jika kriteria tidak terpenuhi dan dengan asumsi bahwa loop dapat berakhir bahkan tidak dilarang secara eksplisit selama aturan standar lainnya diamati.

do { } while(0)atau while(0){}setelah semua pernyataan iterasi (loop) yang tidak memenuhi kriteria yang memungkinkan kompiler untuk hanya mengasumsikan pada kemauan bahwa mereka mengakhiri dan namun mereka jelas-jelas mengakhiri.

Tetapi dapatkah kompiler hanya mengoptimalkan while(1){}?

5.1.2.3p4 mengatakan:

Dalam mesin abstrak, semua ekspresi dievaluasi sebagaimana ditentukan oleh semantik. Implementasi aktual tidak perlu mengevaluasi bagian dari ekspresi jika dapat menyimpulkan bahwa nilainya tidak digunakan dan tidak ada efek samping yang diperlukan dihasilkan (termasuk yang disebabkan oleh pemanggilan fungsi atau mengakses objek yang mudah menguap).

Ini menyebutkan ekspresi, bukan pernyataan, jadi tidak 100% meyakinkan, tetapi tentu saja memungkinkan panggilan seperti:

void loop(void){ loop(); }

int main()
{
    loop();
}

untuk dilewati. Menariknya, dentang memang melewatkannya, dan gcc tidak .

PSkocik
sumber
"Ini tidak mengatakan apa-apa tentang apa yang terjadi jika kriteria tidak terpenuhi" Tetapi, 6.8.5.1 Pernyataan sementara: "Evaluasi ekspresi pengontrolan berlangsung sebelum setiap eksekusi dari loop body." Itu dia. Ini adalah perhitungan nilai (dari ekspresi konstan), ia berada di bawah aturan mesin abstrak 5.1.2.3 yang mendefinisikan evaluasi istilah: " Evaluasi ekspresi secara umum mencakup perhitungan nilai dan inisiasi efek samping." Dan menurut bab yang sama, semua evaluasi tersebut diurutkan dan dievaluasi sebagaimana ditentukan oleh semantik.
Lundin
1
@Lundin Jadi, while(1){}apakah urutan 1evaluasi yang tak terbatas terkait dengan {}evaluasi, tetapi di mana dalam standarnya dikatakan evaluasi itu perlu waktu yang tidak nol ? Perilaku gcc lebih berguna, saya kira, karena Anda tidak perlu trik yang melibatkan akses memori atau trik di luar bahasa. Tapi saya tidak yakin bahwa standar melarang optimasi ini di dentang. Jika membuat while(1){}tidak dioptimalkan adalah niat, standar harus eksplisit tentang hal itu dan perulangan tak terbatas harus terdaftar sebagai efek samping yang dapat diamati dalam 5.1.2.3p2.
PSkocik
1
Saya pikir itu ditentukan, jika Anda memperlakukan 1kondisi sebagai perhitungan nilai. Waktu eksekusi tidak masalah - yang penting adalah apa yang while(A){} B;mungkin tidak dioptimalkan sepenuhnya, tidak dioptimalkan untuk B;dan tidak diurutkan kembali B; while(A){}. Mengutip mesin abstrak C11, penekanan pada tambang: "Kehadiran titik sekuens antara evaluasi ekspresi A dan B menyiratkan bahwa setiap perhitungan nilai dan efek samping yang terkait dengan A diurutkan sebelum setiap perhitungan nilai dan efek samping yang terkait dengan B. " Nilai Aini jelas digunakan (oleh loop).
Lundin
2
+1 Walaupun bagi saya sepertinya "eksekusi hang tanpa batas tanpa output" adalah "efek samping" dalam definisi "efek samping" apa pun yang masuk akal dan berguna di luar standar dalam ruang hampa, ini membantu menjelaskan pola pikir yang darinya bisa masuk akal bagi seseorang.
mtraceur
1
Dekat "mengoptimalkan loop tak terbatas" : Tidak sepenuhnya jelas apakah "itu" mengacu pada standar atau kompiler - mungkin ulangi? Diberi "meskipun mungkin" dan bukan "meskipun mungkin seharusnya tidak" , itu mungkin standar yang mengacu pada "itu" .
Peter Mortensen
2

Saya yakin ini hanyalah bug lama. Saya meninggalkan tes saya di bawah ini dan khususnya referensi untuk diskusi di komite standar untuk beberapa alasan yang saya miliki sebelumnya.


Saya pikir ini adalah perilaku yang tidak terdefinisi (lihat akhir), dan Dentang hanya memiliki satu implementasi. GCC memang berfungsi seperti yang Anda harapkan, hanya mengoptimalkan unreachablepernyataan cetak tetapi meninggalkan loop. Entah bagaimana Clang secara aneh membuat keputusan saat menggabungkan in-lining dan menentukan apa yang bisa dilakukan dengan loop.

Perilaku ini sangat aneh - menghapus cetakan akhir, jadi "melihat" loop tak terbatas, tetapi kemudian menyingkirkan loop juga.

Itu lebih buruk sejauh yang saya tahu. Menghapus inline yang kita dapatkan:

die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
  jmp .LBB0_1
main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

jadi fungsinya dibuat, dan panggilan dioptimalkan keluar. Ini bahkan lebih tangguh dari yang diharapkan:

#include <stdio.h>

void die(int x) {
    while(x);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

menghasilkan perakitan yang sangat tidak optimal untuk fungsi tersebut, tetapi pemanggilan fungsi kembali dioptimalkan! Lebih buruk lagi:

void die(x) {
    while(x++);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

Saya membuat banyak tes lain dengan menambahkan variabel lokal dan meningkatkannya, melewati pointer, menggunakan gotodll ... Pada titik ini saya akan menyerah. Jika Anda harus menggunakan dentang

static void die() {
    int volatile x = 1;
    while(x);
}

melakukan pekerjaan. Ini payah dalam mengoptimalkan (jelas), dan pergi ke final yang berlebihan printf. Setidaknya programnya tidak berhenti. Mungkin GCC?

Tambahan

Setelah berdiskusi dengan David, saya berpendapat bahwa standar tidak mengatakan "jika kondisinya konstan, Anda mungkin tidak menganggap loop berakhir". Dengan demikian, dan diberikan berdasarkan standar tidak ada perilaku yang dapat diamati (seperti yang didefinisikan dalam standar), saya hanya akan berdebat tentang konsistensi - jika kompiler mengoptimalkan loop karena menganggap itu berhenti, seharusnya tidak mengoptimalkan pernyataan berikut.

Heck n1528 memiliki ini sebagai perilaku yang tidak terdefinisi jika saya membacanya dengan benar. Secara khusus

Masalah utama untuk melakukannya adalah memungkinkan kode bergerak melintasi loop yang berpotensi tidak berakhir

Dari sini saya pikir itu hanya bisa beralih ke diskusi tentang apa yang kita inginkan (diharapkan?) Daripada apa yang diizinkan.

kabanus
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Bhargav Rao
Re "plain all bug" : Maksud Anda " plain old bug" ?
Peter Mortensen
@PeterMortensen "ole" akan baik-baik saja dengan saya juga.
kabanus
2

Tampaknya ini adalah bug dalam kompiler Dentang. Jika tidak ada paksaan pada die()fungsi menjadi fungsi statis, hapus staticdan buat inline:

#include <stdio.h>

inline void die(void) {
    while(1)
        ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Ini berfungsi seperti yang diharapkan ketika dikompilasi dengan kompiler Dentang dan portabel juga.

Explorer Compiler (godbolt.org) - dentang 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"
HS
sumber
Bagaimana dengan static inline?
SS Anne
1

Tampaknya ini berfungsi untuk saya:

#include <stdio.h>

__attribute__ ((optnone))
static void die(void) {
    while (1) ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

di godbolt

Secara eksplisit mengatakan Dentang untuk tidak mengoptimalkan bahwa satu fungsi menyebabkan loop tak terbatas akan dipancarkan seperti yang diharapkan. Semoga ada cara untuk menonaktifkan optimasi tertentu secara selektif daripada hanya mematikannya seperti itu. Dentang masih menolak untuk mengeluarkan kode untuk yang kedua printf, meskipun. Untuk memaksanya melakukan itu, saya harus lebih lanjut memodifikasi kode di dalamnya mainmenjadi:

volatile int x = 0;
if (x == 0)
    die();

Sepertinya Anda harus menonaktifkan pengoptimalan untuk fungsi loop tak terbatas Anda, lalu memastikan bahwa loop tak terbatas Anda disebut kondisional. Di dunia nyata, yang terakhir hampir selalu terjadi.

bta
sumber
1
Tidak perlu untuk yang kedua printfdihasilkan jika loop benar-benar pergi selamanya, karena dalam kasus yang kedua printfbenar-benar tidak dapat dijangkau dan karena itu dapat dihapus. (Kesalahan dentang adalah dalam mendeteksi unreachability dan kemudian menghapus loop sehingga kode yang tidak terjangkau tercapai).
nneonneo
Dokumen GCC __attribute__ ((optimize(1))), tetapi dentang mengabaikannya sebagai tidak didukung: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes
0

Implementasi yang sesuai dapat, dan banyak yang praktis, memberlakukan batasan sewenang-wenang pada berapa lama suatu program dapat dijalankan atau berapa banyak instruksi yang akan dijalankannya, dan berperilaku secara sewenang-wenang jika batasan tersebut dilanggar atau - di bawah aturan "seolah-olah" --Jika itu menentukan bahwa mereka pasti akan dilanggar. Asalkan suatu implementasi dapat berhasil memproses setidaknya satu program yang secara nominal menjalankan semua batas yang tercantum dalam N1570 5.2.4.1 tanpa mencapai batas terjemahan, keberadaan batas, sejauh mana dokumen tersebut didokumentasikan, dan dampak dari melampauinya, adalah semua masalah Kualitas Penerapan di luar yurisdiksi Standar.

Saya pikir maksud Standar cukup jelas bahwa penyusun tidak boleh berasumsi bahwa while(1) {}perulangan tanpa efek samping atau breakpernyataan akan berakhir. Bertentangan dengan apa yang mungkin dipikirkan oleh beberapa orang, penulis Standar tidak mengundang penulis kompiler untuk menjadi bodoh atau bodoh. Implementasi yang sesuai mungkin berguna untuk memutuskan untuk menghentikan program apa pun yang, jika tidak terganggu, akan menjalankan lebih banyak instruksi efek samping daripada ada atom di alam semesta, tetapi implementasi kualitas tidak boleh melakukan tindakan seperti itu berdasarkan asumsi apa pun tentang pemberhentian tetapi lebih atas dasar bahwa hal itu bisa berguna, dan tidak akan (tidak seperti perilaku dentang) lebih buruk daripada tidak berguna.

supercat
sumber
-2

Loop tidak memiliki efek samping, sehingga dapat dioptimalkan. Pengulangan secara efektif merupakan jumlah iterasi tanpa batas dari nol unit kerja. Ini tidak terdefinisi dalam matematika dan logika dan standar tidak mengatakan apakah suatu implementasi diizinkan untuk menyelesaikan banyak hal jika setiap hal dapat dilakukan dalam waktu nol. Interpretasi dentang sangat masuk akal dalam memperlakukan infinity kali nol sebagai nol daripada infinity. Standar tidak mengatakan apakah loop infinite dapat berakhir atau tidak jika semua pekerjaan dalam loop sudah selesai.

Kompiler diizinkan untuk mengoptimalkan segala sesuatu yang bukan perilaku yang dapat diamati sebagaimana didefinisikan dalam standar. Itu termasuk waktu eksekusi. Tidak perlu menjaga fakta bahwa perulangan, jika tidak dioptimalkan, akan membutuhkan waktu yang tidak terbatas. Hal ini diizinkan untuk mengubah itu ke waktu lari yang jauh lebih pendek - pada kenyataannya, itulah poin dari kebanyakan optimasi. Loop Anda dioptimalkan.

Bahkan jika dentang menerjemahkan kode secara naif, Anda dapat membayangkan sebuah CPU yang mengoptimalkan yang dapat menyelesaikan setiap iterasi dalam setengah waktu iterasi sebelumnya. Itu benar-benar akan menyelesaikan loop tak terbatas dalam jumlah waktu yang terbatas. Apakah mengoptimalkan CPU seperti itu melanggar standar? Tampaknya tidak masuk akal untuk mengatakan bahwa mengoptimalkan CPU akan melanggar standar jika terlalu baik dalam mengoptimalkan. Hal yang sama berlaku untuk kompiler.

David Schwartz
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew
4
Menilai dari pengalaman yang Anda miliki (dari profil Anda), saya hanya dapat menyimpulkan bahwa posting ini ditulis dengan itikad buruk hanya untuk membela kompiler. Anda serius berargumen bahwa sesuatu yang membutuhkan jumlah waktu tak terbatas dapat dioptimalkan untuk dieksekusi dalam setengah waktu. Itu konyol di setiap level dan Anda tahu itu.
pipa
@pipe: Saya pikir para pengelola dentang dan gcc berharap versi Standar di masa mendatang akan membuat perilaku penyusun mereka diizinkan, dan pengelola penyusun itu akan dapat berpura-pura bahwa perubahan semacam itu hanyalah koreksi dari cacat yang sudah lama ada. dalam Standar. Begitulah cara mereka memperlakukan jaminan Urutan Awal Umum C89, misalnya.
supercat
@SSAnne: Hmm ... Saya tidak berpikir itu cukup untuk memblokir beberapa kesimpulan gcc dan dentang menarik dari hasil perbandingan pointer-kesetaraan.
supercat
@supercat Ada <s> lainnya </s> ton.
SS Anne
-2

Saya minta maaf jika ini bukan kepalang masalahnya, saya menemukan posting ini dan saya tahu karena tahun-tahun saya menggunakan distro Gentoo Linux bahwa jika Anda ingin kompiler tidak mengoptimalkan kode Anda, Anda harus menggunakan -O0 (Nol). Saya ingin tahu tentang hal itu, dan mengkompilasi dan menjalankan kode di atas, dan loop tidak berjalan tanpa batas. Dikompilasi menggunakan clang-9:

cc -O0 -std=c11 test.c -o test
Fellipe Weno
sumber
1
Intinya adalah membuat infinite loop dengan optimisasi diaktifkan.
SS Anne
-4

whileLoop kosong tidak memiliki efek samping pada sistem.

Oleh karena itu Dentang menghapusnya. Ada beberapa cara "lebih baik" untuk mencapai perilaku yang diinginkan yang memaksa Anda untuk lebih jelas tentang niat Anda.

while(1); adalah baaadd.

Jameis terkenal
sumber
6
Dalam banyak konstruksi yang disematkan, tidak ada konsep abort()atau exit(). Jika situasi muncul di mana fungsi menentukan bahwa (mungkin sebagai akibat dari kerusakan memori) eksekusi lanjutan akan lebih buruk daripada berbahaya, perilaku standar umum untuk pustaka tertanam adalah untuk memanggil fungsi yang melakukan a while(1);. Mungkin bermanfaat bagi kompiler untuk memiliki opsi untuk menggantikan perilaku yang lebih bermanfaat , tetapi penulis kompiler yang tidak dapat menemukan cara memperlakukan konstruksi sederhana seperti itu sebagai penghalang untuk melanjutkan program tidak kompeten untuk dipercaya dengan optimisasi yang kompleks.
supercat
Apakah ada cara Anda bisa lebih eksplisit tentang niat Anda? optimizer ada untuk mengoptimalkan program Anda, dan menghapus loop berlebihan yang tidak melakukan apa-apa ADALAH optimasi. ini benar-benar perbedaan filosofis antara pemikiran abstrak dunia matematika dan dunia teknik yang lebih terapan.
Jameis Terkenal
Sebagian besar program memiliki serangkaian tindakan bermanfaat yang harus mereka lakukan bila memungkinkan, dan serangkaian tindakan lebih buruk daripada tidak berguna yang tidak boleh mereka lakukan dalam keadaan apa pun. Banyak program memiliki seperangkat perilaku yang dapat diterima dalam kasus tertentu, salah satunya, jika waktu eksekusi tidak dapat diamati, akan selalu "menunggu beberapa sewenang-wenang dan kemudian melakukan beberapa tindakan dari set". Jika semua tindakan selain menunggu berada di set tindakan lebih buruk daripada tidak berguna, tidak akan ada jumlah detik N yang "menunggu selamanya" akan berbeda dari ...
supercat
... "tunggu N + 1 detik dan kemudian lakukan beberapa tindakan lain", sehingga fakta bahwa serangkaian tindakan yang dapat ditoleransi selain menunggu kosong tidak akan dapat diamati. Di sisi lain, jika sepotong kode menghapus beberapa tindakan yang tidak dapat ditoleransi dari serangkaian tindakan yang mungkin, dan salah satu tindakan itu tetap dilakukan , itu harus dianggap dapat diamati. Sayangnya, aturan bahasa C dan C ++ menggunakan kata "menganggap" dengan cara yang aneh tidak seperti bidang logika atau usaha manusia yang dapat saya identifikasi.
supercat
1
@FamousJameis ok, tetapi Dentang tidak hanya menghapus loop - itu statis menganalisis semuanya setelah itu tidak dapat dijangkau dan memancarkan instruksi yang tidak valid. Itu tidak seperti yang Anda harapkan jika itu hanya "menghapus" loop.
nneonneo