Optimize edilmeyecek sonsuz bir boş döngüyü nasıl yapabilirim?


131

C11 standardı, sürekli kontrol ifadelerine sahip yineleme ifadelerinin optimize edilmemesi gerektiği anlamına geliyor. Tavsiyemi taslak standarttan bölüm 6.8.5'ten alıntı yapan bu cevaptan alıyorum :

Kontrol ifadesi sabit bir ifade olmayan bir yineleme ifadesi ... uygulama tarafından sonlandırılacağı varsayılabilir.

Bu cevapta, benzer bir döngünün while(1) ;optimizasyona tabi olmaması gerektiği belirtilmektedir.

Öyleyse ... Clang / LLVM neden aşağıdaki döngüyü optimize ediyor (derlendi 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");
}

Makinemde bu yazdırılıyor begin, sonra yasadışı bir talimatla ( ud2sonra yerleştirilen bir tuzak die()) çöküyor . Godbolt'da , çağrıdan sonra hiçbir şeyin üretilmediğini görebiliriz puts.

Clang'ın sonsuz bir döngü çıkmasını sağlamak şaşırtıcı derecede zor bir işti -O2- tekrar tekrar bir volatiledeğişkeni test edebilirken , istemediğim bir hafıza okumasını içeriyor. Ve eğer böyle bir şey yaparsam:

#include <stdio.h>

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

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

... Clang baskıları ve beginardından unreachablesonsuz döngü hiç yokmuş gibi geliyor.

Optimizasyon açıkken Clang'ın uygun, hafızasız erişime sahip sonsuz bir döngü üretmesini nasıl sağlıyorsunuz?


3
Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Bhargav Rao

2
Yan etkisi olmayan taşınabilir bir çözüm yoktur. Bellek erişimi istemiyorsanız, en iyi umudunuz geçici imzasız karakter kaydı olacaktır; ancak kayıt C ++ 17'de kaybolur.
Scott M

25
Belki bu soru kapsamında değildir, ama bunu neden yapmak istediğinizi merak ediyorum. Elbette gerçek görevinizi yerine getirmenin başka bir yolu var. Yoksa bu sadece akademik nitelikte mi?
Cruncher

1
@Cruncher: Bir programı çalıştırmak için yapılan herhangi bir özel girişimin etkileri faydalı, temelde yararsız veya yararsızdan önemli ölçüde daha kötü olabilir. Bir programın sonsuz bir döngüde sıkışmasına neden olan bir yürütme işe yaramaz olabilir, ancak yine de bir derleyicinin değiştirebileceği diğer davranışlara tercih edilebilir.
supercat

6
@Cruncher: Kod, kavramın olmadığı bağımsız bir bağlamda çalışıyor olabilir exit()ve kod, sürekli yürütmenin etkilerinin işe yaramazdan daha kötü olmayacağını garanti edemediği bir durum keşfetmiş olabilir . Kendine atla döngüsü, bu tür durumları ele almanın oldukça berbat bir yoludur, ancak yine de kötü bir durumu ele almanın en iyi yolu olabilir.
supercat

Yanıtlar:


77

C11 standardı bunu söylüyor, 6.8.5 / 6:

Kontrol ifadesi sabit bir ifade olmayan bir yineleme ifadesi, 156) giriş / çıkış işlemi gerçekleştirmeyen, uçucu nesnelere erişmeyen ve gövdesinde hiçbir senkronizasyon veya atomik işlem gerçekleştirmeyen, ifadeyi kontrol eden veya ifadesi) ifadesi-3, uygulama tarafından sonlandırılacağı varsayılabilir. 157)

İki dip notu normatif değildir ancak faydalı bilgiler sağlar:

156) Atlanan bir kontrol ifadesi, sabit bir ifade olan sıfır olmayan bir sabit ile değiştirilir.

157) Bu, sonlandırma kanıtlanamasa bile boş döngülerin çıkarılması gibi derleyici dönüşümlerine izin vermek için tasarlanmıştır.

Senin durumunda, while(1)bu yüzden olabilir, kristal berraklığında sabit ifadesidir değil sonlandırmak için uygulanması yoluyla varsayılabilir. "Sonsuza dek" döngüler ortak bir programlama yapısı olduğundan böyle bir uygulama umutsuzca kırılacaktır.

Ancak döngüden sonra "ulaşılamaz kod" ne olur, bildiğim kadarıyla, iyi tanımlanmış değil. Ancak, clang gerçekten çok garip davranıyor. Makine kodunu gcc (x86) ile karşılaştırma:

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

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

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

gcc döngüyü oluşturur, clang sadece ormana girer ve hata 255 ile çıkar.

Bu clang'ın uyumlu olmayan davranışına doğru eğildim. Çünkü örneğinizi şöyle genişletmeye çalıştım çünkü:

#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");
}

C11 ekledim _NoreturnDerleyiciye daha da yardımcı olmak için . Bu işlevin yalnızca o anahtar kelimeden kapatılacağı açık olmalıdır.

setjmpilk yürütme üzerine 0 döndürecektir, bu nedenle bu program sadece şut while(1)ve orada durmak gerekir , sadece baskı "başlar" (\ n stdout temizler varsayalım). Bu gcc ile olur.

Döngü basitçe kaldırılmışsa, 2 kez "start" yazmalı, sonra "ulaşılamaz" yazmalıdır. Ancak clang'da ( godbolt ), çıkış kodu 0'ı döndürmeden önce 1 kez "başlar" ve sonra "ulaşılamaz" yazdırır.

Burada tanımsız davranış iddia için hiçbir dava bulamıyorum, bu yüzden benim almak bu clang bir hata olmasıdır. Her halükarda, bu davranış, gömülü sistemler gibi programlar için% 100 işe yaramaz hale gelir, burada programı asılı olan sonsuz döngülere güvenmeniz gerekir (bir bekçi köpeği vb. Beklerken).


15
“Bu kristal berraklığında sabit bir ifade, bu yüzden uygulama tarafından sonlandırılamayacağı varsayılmayabilir” diye katılmıyorum . Bu gerçekten sirke seçici dil avukatlık içine alır, ama 6.8.5/6biçimindedir (bu) daha sonra Takdir edersiniz ki eğer (bu) . Bu, eğer (bunlar) varsayamayacağınız (bu) anlamına gelmez . Standartlarla istediğiniz her şeyi yapabileceğiniz yerde karşılanmadığı zaman değil, yalnızca koşullar karşılandığında geçerlidir. Ve gözlemlenebilir yoksa ...
kabanus

7
@kabanus Alıntılanan bölüm özel bir durumdur. Değilse (özel durum), kodu normalde yaptığınız gibi değerlendirin ve sıralayın. Aynı bölümü okumaya devam ederseniz, kontrol ifadesi, belirtilen özel durum haricinde her bir yineleme ifadesi ("anlambilim tarafından belirtildiği gibi") için belirtildiği gibi değerlendirilir. Sıralı ve iyi tanımlanmış herhangi bir değer hesaplamasının değerlendirilmesi ile aynı kuralları izler.
Lundin

2
Kabul ediyorum, ancak mecliste int z=3; int y=2; int x=1; printf("%d %d\n", x, z);hiç olmadığı konusunda şaşıracaksınız 2, bu yüzden boş işe yaramaz anlamda optimizasyondan xsonra ydeğil, sonra tayin edildi z. Son cümlenizden yola çıkarak, normal kuralları takip ediyoruz, durma zamanını varsayıyoruz (çünkü daha iyi kısıtlanmadık) ve son "ulaşılamaz" baskıyı bıraktık. Şimdi, bu işe yaramaz ifadeyi optimize ediyoruz (çünkü daha iyisini bilmiyoruz).
kabanus

2
@MSalters Yorumlarımdan biri silindi, ancak giriş için teşekkürler - ve katılıyorum. Yorumumun söylediği şey, bunun tartışmanın kalbi olduğunu düşünüyorum - mantığı kaynakta kalsa bile, hangi anlambilimin optimize edilmesine izin verdiğimizle ilgili while(1);bir int y = 2;ifadeyle aynı . N1528'den beri aynı olabilecekleri izlenimindeydim, ama benden çok daha deneyimli insanlar başka bir şekilde tartışıyorlar ve görünüşe göre resmi bir hata, sonra standarttaki ifadelerin açık olup olmadığı konusunda felsefi bir tartışmanın ötesinde , argüman tartışmalıdır.
kabanus

2
“'Sonsuza dek' döngüler ortak bir programlama yapısı olduğundan böyle bir uygulama umutsuzca kırılacaktır.” - Duyguyu anlıyorum, ancak argüman kusurlu çünkü C ++ 'a aynı şekilde uygulanabilir, ancak bu döngüyü optimize eden bir C ++ derleyicisi kırılmayacak, uyumlu olmayacaktır.
Konrad Rudolph

52

Yan etkiye neden olabilecek bir ifade eklemeniz gerekir.

En basit çözüm:

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

Godbolt bağlantısı


21
Bununla birlikte, clang'ın neden hareket ettiğini açıklamaz.
Lundin

4
Sadece "bu clang bir hata" demek yeterli. Ben "böcek" diye bağırmadan önce, ilk önce burada birkaç şey denemek istiyorum.
Lundin

3
@Lundin Bir hata olup olmadığını bilmiyorum. Standart bu durumda teknik olarak kesin değildir
P__J__

4
Neyse ki, GCC açık kaynak kodludur ve örneğinizi optimize eden bir derleyici yazabilirim. Bunu şimdi ve gelecekte ortaya çıkardığınız herhangi bir örnek için yapabilirim.
Thomas Weller

3
@ThomasWeller: GCC geliştiricileri bu döngüyü optimize eden bir yamayı kabul etmez; belgelenmiş = garantili davranışı ihlal eder. Önceki yorumuma bakın: asm("")dolaylı olarak asm volatile("");ve bu nedenle asm ifadesi, gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html soyut makinesinde olduğu kadar çok çalışmalıdır . ( Yan etkilerinin herhangi bir bellek veya kayıt içermesi güvenli olmadığına dikkat edin ; "memory"C'den eriştiğiniz belleği okumak veya yazmak istiyorsanız Genişletilmiş asm'a ihtiyacınız var . Temel asm yalnızca asm("mfence")veya gibi şeyler için güvenlidir cli.)
Peter Cordes

50

Diğer cevaplar, Clang'ı sıralı montaj dili veya diğer yan etkilerle sonsuz döngüyü yaymanın yollarını zaten içeriyordu. Sadece bunun gerçekten bir derleyici hatası olduğunu onaylamak istiyorum. Özellikle, uzun süredir devam eden bir LLVM hatası - C ++ "yan etkileri olmayan tüm döngülerin sona ermesi gerekir" kavramını, C gibi olmaması gereken dillere uygular.

Örneğin, Rust programlama dili sonsuz döngülere izin verir ve LLVM'yi arka uç olarak kullanır ve aynı sorunu yaşar.

Kısa vadede, LLVM'nin "yan etkileri olmayan tüm döngülerin sona ermesi gerektiğini" varsaymaya devam edeceği görülüyor. Sonsuz döngülere izin veren herhangi bir dil için, LLVM ön uçtan llvm.sideeffectbu tür döngülere opcode girmesini bekler . Rust'un yapmayı planladığı şey budur, bu yüzden Clang (C kodunu derlerken) muhtemelen bunu yapmak zorunda kalacaktır.


5
On yıldan daha eski bir hatanın kokusu gibi bir şey yok ... önerilen birçok düzeltme ve düzeltme eki ile ... ama hala düzeltilmedi.
Ian Kemp

4
@IanKemp: Hatayı düzeltmeleri için şimdi hatayı düzeltmek için on yıl aldıklarını kabul etmeleri gerekiyor. Standardın davranışlarını haklı çıkarmak için değişeceğini ümit etmek daha iyidir. Elbette, standart değişse bile, bu, Standardı değiştirmenin Standardın önceki davranışsal görevinin geriye dönük olarak düzeltilmesi gereken bir kusur olduğunu gösteren bir gösterge olarak görecek insanların gözleri dışında davranışlarını haklı çıkarmayacaktır.
supercat

4
LLVM'nin op'u sideeffect(2017'de) eklediği ve bu op'u kendi takdirine bağlı olarak döngülere yerleştirmesini beklediği anlamıyla "düzeltildi" . LLVM, döngüler için bazı varsayılanlar seçmek zorunda kaldı ve C ++ 'ın davranışıyla kasıtlı olarak veya başka bir şekilde hizalananı seçti. Tabii ki, art arda sideeffectop'ları birleştirmek gibi hala yapılması gereken optimizasyon çalışmaları var . (Bu, Rust ön ucunun onu kullanmasını engelleyen şeydir.) Bu nedenle, hata ön uçta (clang) op'ları döngülere eklemez.
Arnavion

@Arnavion: Sonuçlar kullanılmadıkça veya yapılıncaya kadar işlemlerin ertelenebileceğini, ancak verilerin bir programın sürekli döngü yapmasına neden olacağı takdirde, geçmiş veri bağımlılıklarını sürdürmeye çalışmanın programı yararsızdan daha kötü hale getireceğini belirtmenin bir yolu var mı? Optimize edicinin bir programı işe yaramazdan daha kötü bir hale getirmesini önlemek için eski yararlı optimizasyonları engelleyecek sahte yan etkiler eklemek zorunda kalmak, verimlilik için bir reçete gibi gelmiyor.
supercat

Bu tartışma muhtemelen LLVM / clang posta listelerine aittir. Operasyona ek olarak LLVM taahhüdünü FWIW da bu konuda çeşitli optimizasyon geçişleri öğretti. Ayrıca, Rust sideeffecther fonksiyonun başına ops yerleştirmeyi denedi ve herhangi bir çalışma zamanı performans gerilemesi görmedi. Tek sorun , görünüşe göre önceki yorumumda bahsettiğim gibi ardışık ops füzyonu eksikliği nedeniyle derleme zamanı regresyonudur.
Arnavion

32

Bu bir Clang hatası

... sonsuz döngü içeren bir işlevi satır içine alırken. Davranış while(1);doğrudan anada göründüğünde farklıdır , bu da bana çok adamcağız kokuyor.

Özet ve bağlantılar için @ Arnavion'un cevabına bakınız . Bu cevabın geri kalanı, bilinen bir hata olsun, bunun bir hata olduğunu onaylamadan önce yazılmıştı.


Başlık sorusunu cevaplamak için: Optimize edilmeyen sonsuz bir boş döngüyü nasıl yapabilirim? ? -
yapmak die()bir makro değil, bir işlev , Clang 3.9 ve daha sonra bu hatayı gidermek için. (Daha önceki Clang sürümleri döngüyü tutar veyacall sonsuz döngü ile işlevin satıriçi olmayan bir sürümüne yayar .) print;while(1);print;İşlev , arayana ( Godbolt ) girdiğinde bile güvenli görünüyor . -std=gnu11vs. -std=gnu99hiçbir şeyi değiştirmez.

Sadece GNU C ile ilgileniyorsanız__asm__(""); , döngü içindeki P__J __ 'ler de çalışır ve onu anlayan herhangi bir derleyici için herhangi bir çevre kodunun optimizasyonuna zarar vermemelidir. GNU C Basic asm ifadeleri dolaylı olarakvolatile , bu nedenle bu C soyut makinesinde olduğu gibi "yürütülmesi" gereken görünür bir yan etki olarak sayılır. (Ve evet, Clang, GCC kılavuzunda belgelendiği gibi C'nin GNU lehçesini uygular.)


Bazı insanlar boş bir sonsuz döngüyü optimize etmenin yasal olabileceğini öne sürdüler. Katılmıyorum 1 , ama biz bunu kabul bile, o olamaz da Clang döngü sonra ifadeleri ulaşılamaz olduğunu varsayıyorum, için yasal olması ve bir sonraki fonksiyonu içine fonksiyonun sonuna kapalı infaz düşürdüm veya çöpe rastgele talimatlar olarak çözülür.

(Bu Clang ++ için standartlara uygun olacaktır (ancak yine de çok yararlı değildir); herhangi bir yan etkisi olmayan sonsuz döngüler C ++ 'da UB'dir, ancak C değildir.
(1); C? UB'deki tanımlanmamış davranış derleyicinin temelde herhangi bir şey yayınlamasına izin verir UB ile karşılaşacak bir yürütme yolu üzerindeki kod asmiçin Döngüdeki bir ifade, C ++ için bu UB'yi önleyecektir.Ancak pratikte, C ++ olarak derlenen Clang, satır içi olanlar hariç, sabit ifade sonsuz boş döngüleri kaldırmaz. C olarak derleme)


Manuel olarak satır içine alma, while(1);Clang'ın derleme şeklini değiştirir: asm'de sonsuz döngü vardır. Kurallar avukatý POV'dan beklediđimiz bu.

#include <stdio.h>
int main() {
    printf("begin\n");
    while(1);
    //infloop_nonconst(1);
    //infloop();
    printf("unreachable\n");
}

Godbolt derleyici gezgininde Clang 9.0-O3 -xc, x86-64 için C ( ) olarak derlenir :

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"

Aynı seçeneklere sahip aynı derleyici, önce aynı mainçağrıyı çağıran bir derleme yapar , ancak o noktadan sonra için talimat infloop() { while(1); }vermeyi putsdurdurur main. Dediğim gibi, yürütme sadece fonksiyonun sonundan, sonraki herhangi bir fonksiyona düşer (ancak yığın, işlev girişi için yanlış hizalanmış olduğundan, geçerli bir kuyruk araması bile değildir).

Geçerli seçenekler

  • label: jmp labelsonsuz döngü yayar
  • veya daha sonra 2 dize yazdırmak için başka bir çağrı yayan ve (biz sonsuz döngü kaldırılabilir kabul varsa) return 0den main.

Farkedilmediğim UB olmadığı sürece, "ulaşılamıyor" yazdırılmadan çökme veya başka bir şekilde devam etme, bir C11 uygulaması için açıkça uygun değildir.


Dipnot 1:

Kayıt için, C11'in boş ifadeli sonsuz döngüler için sonlandırma varsayımına izin vermediğine dair kanıt standardını belirten @ Lundin'in cevabı ile aynı fikirdeyim (G / Ç, uçucu, senkronizasyon veya diğer görünür yan etkiler).

Bu, bir döngünün normal bir CPU için boş bir asm döngüsüne derlenmesine izin verecek koşullar kümesidir . (Gövde kaynakta boş olmasa bile, değişkenler için atamalar döngü çalışırken veri yarışı UB'si olmayan diğer iş parçacıkları veya sinyal işleyicileri tarafından görülemez. Bu nedenle, uygun bir uygulama isterse bu döngü gövdelerini kaldırabilir Bu, halkanın kendisinin kaldırılıp kaldırılamayacağı sorusunu bırakır. ISO C11 açıkça hayır diyor.)

C11'in, uygulamanın döngünün sona erdiğini varsayamayacağı (ve bunun UB olmadığı) bir durum olduğu göz önüne alındığında, döngünün çalışma zamanında mevcut olmasını amaçladıkları açık görünüyor. Sonlu zamanda sonsuz miktarda iş yapamayan bir yürütme modeline sahip CPU'ları hedefleyen bir uygulamanın, boş bir sabit sonsuz döngüyü kaldırmak için bir gerekçesi yoktur. Ya da genel olarak bile, kesin ifadeler "sonlandıkları varsayılıp varsayılmayacakları" ile ilgilidir. Bir döngü sonlanamazsa, matematik ve sonsuzluklarla ilgili hangi argümanları yaptığınız ve bazı varsayımsal makinelerde sonsuz miktarda iş yapmanın ne kadar sürdüğü önemli değil, daha sonra kodlara erişilemez demektir .

Bunun yanı sıra, Clang sadece ISO C uyumlu bir DeathStation 9000 değil, çekirdekler ve gömülü nesneler de dahil olmak üzere gerçek dünyadaki düşük seviyeli sistem programlaması için faydalı olması amaçlanmıştır. Dolayısıyla , C11'in kaldırılmasına izin veren argümanları kabul edip while(1);etmediğiniz, Clang'ın bunu gerçekten yapmak isteyeceği mantıklı değildir. Eğer yazarsanız while(1);, bu muhtemelen bir kaza değildi. Kaza ile sonsuz olan döngülerin kaldırılması (çalışma zamanı değişken kontrol ifadeleriyle) yararlı olabilir ve derleyicilerin bunu yapması mantıklıdır.

Bir sonraki kesintiye kadar sadece döndürmek istersiniz, ancak C'de yazarsanız kesinlikle olmasını beklediğiniz şey budur. (Ya gelmez , GCC ve Clang meydana sonsuz döngü bir sarıcı işlevi içinde olduğunda Clang için hariç).

Örneğin, ilkel bir OS çekirdeğinde, zamanlayıcının çalışacak hiçbir görevi olmadığında, boşta kalma görevini çalıştırabilir. Bunun ilk uygulaması olabilir while(1);.

Veya herhangi bir güç tasarruflu rölanti özelliği olmayan donanımlar için tek uygulama bu olabilir. (2000'lerin başına kadar, bu x86'da nadir olmadığını düşünüyorum. hltTalimat mevcut olmasına rağmen , IDK, CPU'lar düşük güçlü boşta durumlara sahip olana kadar anlamlı miktarda güç tasarrufu yaptıysa.)


1
Merakla, aslında gömülü sistemler için clang kullanan var mı? Hiç görmedim ve sadece gömülü olarak çalışıyorum. gcc sadece "son zamanlarda" (10 yıl önce) gömülü pazara girdi ve ben bunu bir sceptically, tercihen düşük optimizasyon ve her zaman ile kullanın -ffreestanding -fno-strict-aliasing. ARM ve belki de eski AVR ile iyi çalışır.
Lundin

1
@Lundin: IDK gömülü hakkında, ama evet insanlar en azından bazen Linux ile clang ile çekirdekler inşa ediyorlar. Muhtemelen MacOS için Darwin.
Peter Cordes

2
bugs.llvm.org/show_bug.cgi?id=965 bu hata alakalı görünüyor, ancak burada gördüğümüzden emin değilim.
bracco23

1
@lundin - Eminim 90'lı yıllara kadar gömülü çalışma için GCC'yi (ve diğer birçok araç setini) kullandık ve RTOS'lar VxWorks ve PSOS gibi. GCC'nin neden sadece gömülü pazara girdiğini söylediğini anlamıyorum.
Jeff Learman

1
@JeffLearman Son zamanlarda ana akım oldu mu? Her neyse, gcc sıkı takma fiyasko sadece C99'un piyasaya sürülmesinden sonra oldu ve yeni sürümleri artık sıkı takma ad ihlallerine maruz kaldıktan sonra muz gibi görünmüyor. Yine de, her kullandığımda şüpheci kalıyorum. Clang'a gelince, en son sürüm ebedi döngüler söz konusu olduğunda açıkça tamamen kırılmıştır, bu nedenle gömülü sistemler için kullanılamaz.
Lundin

14

Sadece rekor için, Clang ayrıca şu şekilde yanlış davranır goto:

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

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

Sorudakiyle aynı çıktıyı üretir, yani:

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

Bunu, C11'de izin verildiği gibi okumak için herhangi bir yol göremiyorum, sadece şunu söylüyor:

6.8.6.1 (2) Bir gotoifade, kapatma fonksiyonunda adlandırılmış etiketin önüne gelen ifadeye koşulsuz bir sıçramaya neden olur.

As gotobir "yineleme deyimi" (6.8.5 listeleri değil while, dove for"sonlandırma-varsayılır" hoşgörü uygulamak, ancak bunları okumak istiyorum) special hakkında hiçbir şey.

Orijinal sorunun Godbolt bağlantı derleyicisi başına x86-64 Clang 9.0.0 ve bayraklar -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

X86-64 GCC 9.2 gibi diğerleriyle oldukça mükemmel hale gelirsiniz:

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

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


Uygun bir uygulama, yürütme süresi veya CPU döngüleri üzerinde aşılırsa keyfi bir davranışa neden olabilecek veya sınırı aşan bir program girdisi kaçınılmazsa belgelenmemiş bir çeviri sınırına sahip olabilir. Bu tür şeyler, Standardın yetki alanı dışında bir Uygulama Kalitesi konusudur. Clang sahiplerinin kalitesiz bir uygulama üretme haklarında çok ısrarcı olmaları tuhaf görünebilir, ancak Standart buna izin verir.
supercat

2
@supercat yorum için teşekkürler ... neden bir çeviri sınırını aşmak, çeviri aşamasında başarısız olmaktan ve yürütmeyi reddetmekten başka bir şey yapsın ki? Ayrıca: " 5.1.1.3 Teşhis Uygun bir uygulama ... bir önişleme çeviri birimi veya çeviri birimi herhangi bir sözdizimi kuralının veya kısıtlamasının ihlali içeriyorsa ... İnfaz aşamasındaki hatalı davranışların ne kadar uyumlu olabileceğini göremiyorum.
jonathanjo

Standart, uygulama sınırlarının tümünün derleme zamanında çözülmesi gerektiğinde uygulanması imkansız olacaktır, çünkü biri evrendeki atomlardan daha fazla yığın bayt gerektiren bir Kesinlikle Uygun program yazabilir. Çalışma zamanı sınırlamalarının "çeviri sınırları" ile toplanması gerekip gerekmediği belirsizdir, ancak böyle bir imtiyaz açıkça gereklidir ve konabilecek başka bir kategori yoktur.
supercat

1
"Çeviri sınırları" hakkındaki yorumunuza yanıt veriyordum. Elbette yürütme sınırları da var, itiraf ediyorum ki neden çeviri sınırları ile dolu olmalarını önerdiğinizi ya da bunun neden gerekli olduğunu söylediğinizi anlamıyorum. nasty: goto nastyKullanıcı veya kaynak tükenmesi müdahale edene kadar CPU'ları döndürmeyip söylemenin herhangi bir nedeni görmüyorum .
jonathanjo

1
Standart, bulabildiğim "yürütme sınırları" na atıfta bulunmaz. İşlev çağrısı yuvalama gibi şeyler genellikle yığın ayırma tarafından işlenir, ancak limitler her fonksiyonun 16 kopya inşa edebileceğini 16 derinliğe çağrıları işlev ve bir çağrı var ki uygun uygulama bar()içinde foo()gelen bir çağrı olarak işlenecek __1fooiçin __2bargelen __2fooiçin __3bar, vb ve gelen __16fooiçin __launch_nasal_demonsdaha sonra tüm otomatik nesneleri statik tahsis edilecek ve ne yapacak sağlayacak olan genellikle bir çeviri sınırı içine "çalışma zamanı" sınırı.
supercat

5

Şeytanın avukatını oynayacağım ve standardın bir derleyicinin sonsuz bir döngüyü optimize etmesini açıkça yasaklamadığını savunacağım.

Kontrol ifadesi sabit bir ifade olmayan bir yineleme ifadesi, 156) giriş / çıkış işlemi gerçekleştirmeyen, uçucu nesnelere erişmeyen ve gövdesinde hiçbir senkronizasyon veya atomik işlem gerçekleştirmeyen, ifadeyi kontrol eden veya ifadesi) ifadesi-3, uygulama tarafından sonlandırılacağı varsayılabilir.157)

Bunu ayrıştıralım. Belirli kriterleri karşılayan bir yineleme ifadesinin sona erdirileceği varsayılabilir:

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

Bu, kriterler karşılanmadığında ve bir döngünün sona erebileceğini varsayarsak ne olacağı hakkında hiçbir şey söylemez, standardın diğer kuralları gözetildiği sürece açıkça yasak değildir.

do { } while(0)ya while(0){}da sonuçta bir derleyicinin sadece sonlandıkları bir hevesle varsaymalarına izin veren kriterleri karşılamayan ve yine de sonlandıkları açık olan yineleme ifadeleri (döngüler).

Ancak derleyici sadece optimize edebilir while(1){}mi?

5.1.2.3p4 diyor ki:

Soyut makinede, tüm ifadeler semantik tarafından belirtildiği şekilde değerlendirilir. Gerçek bir uygulamanın, değerinin kullanılmadığını ve gerekli hiçbir yan etkinin üretilmediğini (bir işlevi çağırmaktan veya geçici bir nesneye erişmekten kaynaklananlar da dahil olmak üzere) bir ifadenin bir kısmını değerlendirmesine gerek yoktur.

Bu ifadelerden değil, ifadelerden bahsediyor, bu yüzden% 100 ikna edici değil, ancak kesinlikle aşağıdaki gibi çağrılara izin veriyor:

void loop(void){ loop(); }

int main()
{
    loop();
}

atlanacak. İlginçtir, clang onu atlamaz ve gcc yapmaz .


"Bu, ölçütler karşılanmadığında ne olduğu hakkında hiçbir şey söylemez" Ancak, 6.8.5.1 while ifadesi: "Kontrol eden ifadenin değerlendirilmesi, döngü gövdesinin her yürütülmesinden önce gerçekleşir." Bu kadar. Bu, bir değer hesaplamasıdır (sabit bir ifadenin), değerlendirme terimini tanımlayan soyut makinenin 5.1.2.3 kuralına girer: " Genel olarak bir ifadenin değerlendirilmesi hem değer hesaplamalarını hem de yan etkilerin başlatılmasını içerir." Aynı bölüme göre, tüm bu değerlendirmeler semantik tarafından belirtildiği gibi sıralanır ve değerlendirilir.
Lundin

1
@Lundin Öyleyse while(1){}, sonsuz bir 1değerlendirme dizisi değerlendirmelerle iç içedir {}, ancak bu değerlendirmelerin standartta nerede sıfırdan fazla zaman alması gerektiğini söylüyor ? Gcc davranışı daha yararlıdır, sanırım, çünkü bellek erişimi içeren hilelere veya dilin dışındaki hilelere ihtiyacınız yok. Ancak standardın clang'da bu optimizasyonu yasakladığına ikna olmadım. while(1){}Niyet edilemez kılmak niyetinde ise, standart bu konuda açık olmalı ve sonsuz döngü 5.1.2.3p2'de gözlemlenebilir bir yan etki olarak listelenmelidir.
PSkocik

1
1Durumu bir değer hesaplama olarak ele alırsanız, belirtildiğini düşünüyorum . Önemli değil Uygulama süresi - önemli olan ne while(A){} B;olabilir değil için optimize edilmemiş, uzakta tamamen optimize edilmesi B;ve-sıralandı yeniden değildir B; while(A){}. C11 soyut makine, ağırlık madeni alıntı: "ifadeleri, A ve B değerlendirilmesi arasında bir dizi noktanın varlığını ifade eder , her değeri hesaplaması ve yan etki ile ilintili her değeri hesaplaması öncesi sekanslanır ve yan etki B ile ilişkili ". Değeri Aaçıkça (döngü tarafından) kullanılır.
Lundin

2
+1 Bana öyle geliyor gibi görünse de "yürütme süresiz olarak kilitleniyor" herhangi bir "yan etki" tanımında anlam ifade eden ve bir boşluktaki standardın ötesinde faydalı olan bir "yan etki" dir, bu açıklamaya yardımcı olur birisine mantıklı geldiği zihniyet.
mtraceur

1
Yakın "sonsuz döngüye dışarı optimize" : Tamamen olmadığını belli değil "o" standardına veya derleyici atıfta - belki başka bir şekilde ifade? Verilen "olsa muhtemelen gerektiği" değil, "gerçi muhtemelen olmamalı" , muhtemelen "bu" nun ifade ettiği standarttır .
Peter Mortensen

2

Bu sadece düz eski bir hata olduğuna ikna oldum. Testlerimi aşağıda ve özellikle de daha önce sahip olduğum bazı gerekçelerle standart komitede tartışmaya atıfta bulunuyorum.


Bence bu tanımsız davranış (sonuna bakın) ve Clang'ın sadece bir uygulaması var. GCC gerçekten beklediğiniz gibi çalışır, yalnızca unreachableprint deyimini optimize eder, ancak döngüyü terk eder. Clang, astarı birleştirirken ve döngü ile neler yapabileceğini belirlerken garip bir şekilde kararlar veriyor.

Davranış çok tuhaf - son baskıyı kaldırır, bu yüzden sonsuz döngüyü "görür", ancak daha sonra döngüden de kurtulur.

Anlayabildiğim kadarıyla daha da kötü. Satır içi kaldırdığımızda:

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"

böylece işlev oluşturulur ve çağrı optimize edilir. Bu beklenenden daha dayanıklı:

#include <stdio.h>

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

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

fonksiyon için çok uygun olmayan bir montaj ile sonuçlanır, ancak fonksiyon çağrısı tekrar optimize edilir! Daha da kötüsü:

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

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

Yerel bir değişken ekleyerek ve artırarak, bir işaretçi geçirerek, gotovb. Kullanarak bir sürü test yaptım ... Bu noktada vazgeçerdim. Clang kullanmanız gerekiyorsa

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

işi yapar. Optimizasyon (berrak) için berbat ve gereksiz finalde bırakır printf. En azından program durmuyor. Belki sonuçta GCC?

ek

David ile tartışmayı takiben, standardın "koşul sabitse, döngünün sona erdiğini varsaymayabilirsiniz" demediğini veriyorum. Bu nedenle ve standart altında (standartta tanımlandığı gibi) gözlemlenebilir davranış yoktur, sadece tutarlılık için tartışacağım - eğer bir derleyici bir döngüyü sonlandırdığı için optimize ederse, aşağıdaki ifadeleri optimize etmemelidir.

Heck n1528 bunları doğru okursam tanımsız davranış olarak görür. özellikle

Bunu yapmak için önemli bir sorun, kodun sonlandırılmayan bir döngüde hareket etmesine izin vermesidir

Buradan sadece izin verilenlerden ziyade istediğimiz (beklenen?) Bir tartışmaya dönüşebileceğini düşünüyorum .


Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Bhargav Rao

Re "plain all bug" : Şunu mu demek istediniz: " plain old bug" ?
Peter Mortensen

@PeterMortensen "ole" de benimle iyi olurdu.
kabanus

2

Görünüşe göre bu Clang derleyicisinde bir hata. die()Statik bir işlev olması için herhangi bir zorlama yoksa,static ve yapın inline:

#include <stdio.h>

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

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

Clang derleyicisiyle derlendiğinde beklendiği gibi çalışıyor ve taşınabilir.

Derleyici Gezgini (godbolt.org) - clang 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"

Ne olmuş static inline?
SS Anne

1

Aşağıdakiler benim için çalışıyor gibi görünüyor:

#include <stdio.h>

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

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

en Godbolt

Clang'a bir fonksiyonun optimize edilmemesini açıkça bildirmesi, beklendiği gibi sonsuz bir döngü yayılmasına neden olur. Umarım belirli optimizasyonları hepsini böyle kapatmak yerine seçici olarak devre dışı bırakmanın bir yolu vardır. Yine de Clang, ikincisi için kod yayınlamayı reddediyor printf. Bunu yapmaya zorlamak için, içinde kodu daha fazla değiştirmek zorunda mainkaldı:

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

Sonsuz döngü işleviniz için optimizasyonları devre dışı bırakmanız ve ardından sonsuz döngünüzün koşullu olarak çağrıldığından emin olmanız gerekir. Gerçek dünyada, ikincisi zaten her zaman böyledir.


1
printfDöngü gerçekten sonsuza dek giderse ikincinin oluşturulması gerekli değildir , çünkü bu durumda ikinciye printfgerçekten ulaşılamaz ve bu nedenle silinebilir. (Clang'ın hatası hem ulaşılamazlığı tespit etmekte, hem de ulaşılamaz koda ulaşılacak şekilde döngüyü silmektir).
nneonneo

GCC belgeleri __attribute__ ((optimize(1))), ancak clang bunu desteklenmediği gibi görmezden geliyor: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes

0

Uygun bir uygulama ve birçok pratik uygulama, bir programın ne kadar süre çalışabileceği veya kaç komut yürüteceği konusunda keyfi sınırlamalar getirebilir ve bu sınırlar ihlal edilirse veya - "eğer" kuralı uyarınca keyfi şekilde davranabilir - kaçınılmaz olarak ihlal edileceğini tespit ederse. Bir uygulamanın, N1570 5.2.4.1'de listelenen tüm limitleri, herhangi bir çeviri limitine, sınırların varlığına, belgelendirilme derecesine ve bunları aşmanın etkilerine çarpmadan nominal olarak uygulayan en az bir programı başarıyla işleyebilmesi şartıyla, Standardın yetki alanı dışındaki tüm Uygulama Kalitesi sorunları.

Standardın amacı, derleyicilerin while(1) {}hiçbir yan etkisi veya breakifadesi olmayan bir döngünün sona ereceğini varsaymaması gerektiğinin oldukça açık olduğunu düşünüyorum . Bazı insanların düşündüklerinin aksine, Standardın yazarları derleyici yazarlarını aptal veya küstah olmaya davet etmiyorlardı. Uygun bir uygulama, kesintiye uğramamışsa, evrendeki atomlardan daha fazla yan etki içermeyen talimatlar yürütecek olan herhangi bir programı sonlandırmaya karar vermek için yararlı olabilir, ancak kaliteli bir uygulama, bu tür herhangi bir varsayım temelinde bu tür bir eylem gerçekleştirmemelidir. sonlandırma yerine, yararlı olabileceğine ve (clang'ın davranışından farklı olarak) işe yaramayacağından daha kötü olmayacağı temelinde.


-2

Döngünün hiçbir yan etkisi yoktur ve bu nedenle optimize edilebilir. Döngü, sıfır iş biriminin sonsuz sayıda yinelemesidir. Bu, matematikte ve mantıkta tanımlanmamıştır ve standart, her şeyin sıfır zamanda yapılabilmesi durumunda bir uygulamanın sonsuz sayıda şeyi tamamlamasına izin verilip verilmediğini söylemez. Clang'ın yorumu, sonsuzluk zamanlarının sonsuzluktan ziyade sıfır olarak muamele edilmesinde mükemmel bir şekilde makul. Standart, döngülerdeki tüm çalışmalar aslında tamamlandığında sonsuz bir döngünün sona erip bitmeyeceğini söylemez.

Derleyicinin, standartta tanımlanan gözlemlenebilir davranış olmayan her şeyi optimize etmesine izin verilir. Bu, yürütme süresini de içerir. Optimize edilmezse, döngünün sonsuz miktarda zaman alacağı gerçeğini korumak gerekli değildir. Bunu çok daha kısa çalışma süresine değiştirebilir - aslında, çoğu optimizasyon noktası. Döngünüz optimize edildi.

Clang kodu safça çevirse bile, her yinelemeyi bir önceki yinelemenin yarısında tamamlayabilen bir optimize CPU düşünebilirsiniz. Bu kelimenin tam anlamıyla sonsuz döngüyü sınırlı bir sürede tamamlardı. Böyle bir optimize edici CPU standardı ihlal ediyor mu? Optimize etmek için çok iyi olursa, optimize edici bir CPU'nun standardı ihlal edeceğini söylemek oldukça saçma görünüyor. Aynı şey bir derleyici için de geçerlidir.


Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Samuel Liew

4
Sahip olduğunuz deneyime dayanarak (profilinizden) bu yazının sadece derleyiciyi savunmak için kötü niyetle yazıldığı sonucuna varabilirim. Sonsuz bir zaman alan bir şeyin yarı sürede yürütülecek şekilde optimize edilebileceğini ciddi olarak savunuyorsunuz. Bu her seviyede saçma ve bunu biliyorsun.
boru

@pipe: Bence clang ve gcc sahipleri Standard'ın gelecekteki bir versiyonunun derleyicilerinin davranışlarına izin verileceğini umuyorlar ve bu derleyicilerin koruyucular böyle bir değişikliğin sadece uzun süredir devam eden bir hatanın düzeltilmesi olduğunu iddia edebilecekler. Standart. Örneğin, C89'un Ortak Başlangıç ​​Dizisi garantilerini bu şekilde ele aldılar.
supercat

@SSAnne: Hmm ... Bunun gcc ve clang draw'un bazı çıkarımsal çıkarımlarını işaretçi eşitliği karşılaştırmalarının sonuçlarından engellemek için yeterli olduğunu sanmıyorum.
supercat

@supercat <s> diğerleri </s> ton var.
SS Anne

-2

Bu durum saçma değilse üzgünüm, bu yazı üzerine tökezledi ve biliyorum çünkü Gentoo Linux dağıtımını kullanarak yıllarım derleyicinin kodunuzu optimize etmemesini istiyorsanız -O0 (Sıfır) kullanmalısınız. Bunu merak ettim ve yukarıdaki kodu derledim ve çalıştırdım ve döngü süresiz olarak devam ediyor. Clang-9 kullanılarak derlendi:

cc -O0 -std=c11 test.c -o test

1
Burada amaç optimizasyonların etkin olduğu sonsuz bir döngü yapmaktır.
SS Anne

-4

Boş bir whiledöngünün sistem üzerinde herhangi bir yan etkisi yoktur.

Bu yüzden Clang onu kaldırır. Sizi niyetlerinizden daha belirgin olmaya zorlayan amaçlanan davranışa ulaşmanın "daha iyi" yolları vardır.

while(1); baaadd.


6
Birçok gömülü yapıda, abort()veya kavramı yoktur exit(). Bir fonksiyonun (belki de bellek bozulması nedeniyle) devam eden yürütmenin tehlikeli olmaktan daha kötü olacağını belirlediği bir durum ortaya çıkarsa, gömülü kütüphaneler için ortak bir varsayılan davranış, a işlevini yerine getirmektir while(1);. Derleyicinin daha yararlı bir davranışı değiştirme seçeneklerine sahip olması yararlı olabilir , ancak böyle basit bir yapının sürekli program yürütülmesine engel olarak nasıl davranılacağını anlayamayan herhangi bir derleyici yazarı, karmaşık optimizasyonlara güvenilemez.
supercat

Niyetlerinizden daha açık olmanın bir yolu var mı? optimizer programınızı optimize etmek için orada ve hiçbir şey yapmayan gereksiz döngüler kaldırmak bir optimizasyon IS. bu gerçekten matematik dünyasının soyut düşüncesi ile daha uygulamalı mühendislik dünyası arasındaki felsefi bir farktır.
Ünlü Jameis

Çoğu programın, mümkün olduğunda gerçekleştirmeleri gereken bir dizi yararlı eylemi ve hiçbir koşulda asla gerçekleştirmemesi gereken bir dizi işe yaramaz eylemleri vardır. Birçok program, belirli bir durumda kabul edilebilir davranışlara sahiptir, bunlardan biri, yürütme süresi gözlemlenebilir değilse, her zaman "biraz beklemek ve sonra kümeden bir eylem gerçekleştirmek" olacaktır. Beklemek dışındaki tüm eylemler işe yaramazdan daha kötü eylemler kümesindeyse, "sonsuza kadar bekle" nin gözle görülür şekilde farklı olacağı saniye sayısı N olmaz.
supercat

... "N + 1 saniye bekleyin ve sonra başka bir eylem gerçekleştirin", bu yüzden beklemenin dışında tolere edilebilir eylemler kümesinin boş olması gözlemlenemez. Öte yandan, bir kod parçası olası eylemler grubundan dayanılmaz bir eylemi kaldırırsa ve bu eylemlerden biri yine de gerçekleştirilebilirse , bu gözlemlenebilir olarak değerlendirilmelidir. Ne yazık ki, C ve C ++ dil kuralları "varsayalım" sözcüğünü, tanımlayabildiğim herhangi bir mantık veya insan çabasının aksine garip bir şekilde kullanıyor.
supercat

1
@FamousJameis tamam, ancak Clang sadece döngüyü kaldırmakla kalmıyor, aynı zamanda her şeyi erişilemez olarak statik olarak analiz ediyor ve geçersiz bir talimat veriyor. Döngüyü "kaldırırsa" bu beklediğiniz gibi değil.
nneonneo
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.