C ++ 'da imzalı taşma ve tanımsız davranış (UB)


56

Aşağıdaki gibi kod kullanımını merak ediyorum

int result = 0;
int factor = 1;
for (...) {
    result = ...
    factor *= 10;
}
return result;

Döngü ndefalarca yinelenirse , tam olarak factorçarpılır . Ancak, sadece toplam kez çarpıldıktan sonra kullanılır . Döngünün son yinelemesi dışında asla taşmadığını varsayarsak , ancak döngünün son yinelemesinde taşabilirse, bu kod kabul edilebilir mi? Bu durumda, taşma gerçekleştikten sonra değeri kesinlikle kullanılamaz.10nfactor10n-1factorfactor

Böyle bir kod kabul edilmesi gerekip gerekmediği hakkında bir tartışma yaşıyorum. Çarpmayı bir if-ifadesinin içine koymak ve sadece çarpmayı taşabildiği zaman döngünün son yinelemesinde yapmak mümkün değildir. Dezavantajı, kodun kümelenmesi ve önceki tüm döngü yinelemelerinde kontrol edilmesi gereken gereksiz bir dal eklemesidir. Ayrıca döngü üzerinde bir kaç kez yineleme ve döngü sonra bir kez döngü gövdesi çoğaltmak, yine, bu kodu karmaşık.

Söz konusu gerçek kod, gerçek zamanlı bir grafik uygulamasında toplam CPU süresinin büyük bir kısmını tüketen sıkı bir iç döngüde kullanılır.


5
Bu soruyu konu dışı olarak kapatmak için oy kullanıyorum çünkü bu soru burada bulunmamak için codereview.stackexchange.com olmalıdır .
Kevin Anderson

31
@KevinAnderson, hayır burada geçerli, çünkü örnek kod düzeltilecek, sadece geliştirilmiş değil.
Bathsheba

1
@harold Çok yakınlar.
Yörüngedeki Hafiflik Yarışları

1
@LightnessRaceswithMonica: Standardın yazarları, çeşitli platformlar ve amaçlar için tasarlanan uygulamaların, programcıların kullanabileceği semantikleri, bu platformlar için yararlı olan yollarla ve Standartların kendisinden talep edip etmediği amaçlarla anlamlı bir şekilde işleyerek programcıları genişletmesini amaçlamış ve beklemiştir, ayrıca taşınabilir olmayan kodları çıkarmak istemediklerini ifade etti. Bu nedenle, sorular arasındaki benzerlik hangi uygulamaların desteklenmesi gerektiğine bağlıdır.
supercat

2
@supercat Uygulama tanımlı davranışlar için elbette ve araç zincirinizin bir uzantısı olduğunu biliyorsanız (ve taşınabilirliği umursamıyorsanız) iyi. UB için mi? Şüpheli.
Yörüngedeki Hafiflik Yarışları

Yanıtlar:


51

Derleyiciler, geçerli bir C ++ programının UB içermediğini varsayar. Örneğin şunu düşünün:

if (x == nullptr) {
    *x = 3;
} else {
    *x = 5;
}

Eğer x == nullptro zaman çözümleyecek ve bir değer atama UB olduğunu. Bu nedenle, bunun geçerli bir programda sona erebilmesinin tek yolu x == nullptr, hiçbir zaman gerçek vermeyeceği ve derleyicinin sanki kural altında olduğunu varsayabileceğidir, yukarıdaki:

*x = 5;

Şimdi kodunuzda

int result = 0;
int factor = 1;
for (...) {      // Loop until factor overflows but not more
   result = ...
   factor *= 10;
}
return result;

factorGeçerli bir programda son çarpımı gerçekleşemez (imzalı taşma tanımsızdır). Dolayısıyla, ödev resultyapılamaz. Son yinelemeden önce dallanmanın bir yolu olmadığından, önceki yineleme de gerçekleşemez. Sonunda, kodun doğru olan kısmı (yani, tanımlanmamış davranış oluşmaz):

// nothing :(

6
"Tanımsız Davranış", bir programı bir bütün olarak nasıl etkileyebileceğini açıkça açıklamadan SO cevaplarında çok şey duyduğumuz bir ifadedir. Bu cevap işleri daha açık hale getiriyor.
Gilles-Philippe Paillé

1
Ve eğer fonksiyon sadece daha küçük INT_MAX >= 10000000000olan durumlarda çağrılan farklı bir fonksiyon ile hedeflere çağrılırsa "yararlı bir optimizasyon" bile olabilir INT_MAX.
R .. GitHub BUZA YARDIMCI DURDUR

2
@ Gilles-PhilippePaillé Bu konuda bir yazı yapıştırabileceğimiz zamanlar var. Benign Data Races , ne kadar kötü olabileceklerini yakalamak için favorilerimden biri. Ayrıca MySQL'de tekrar bulamadığım harika bir hata raporu da var - yanlışlıkla UB'yi harekete geçiren bir arabellek taşması denetimi. Belirli bir derleyicinin belirli bir sürümü, UB'nin asla gerçekleşmediğini varsaydı ve tüm taşma kontrolünü optimize etti.
Cort Ammon

1
@SolomonSlow: UB'nin tartışmalı olduğu ana durumlar, Standardın ve uygulamaların belgelerinin bazı eylemlerin davranışını tanımladığı, ancak Standardın başka bir bölümünün UB olarak nitelendirdiği durumlardır. Standart yazılmadan önceki yaygın uygulama, derleyici yazarlarının bu tür eylemleri, müşterilerinin başka bir şey yaparak kendilerinden faydalanmaları dışında anlamlı bir şekilde işlemeleri için olmuştu ve Standardın yazarlarının derleyici yazarlarının isteyerek başka bir şey yapacaklarını hayal etmediğini düşünüyorum. .
supercat

2
@ Gilles-PhilippePaillé: Her C Programcısının LLVM blogundaki Tanımsız Davranış Hakkında Bilmesi Gerekenler de iyidir. Örneğin işaretli tamsayı taşması UB'nin derleyicilerin i <= ndöngüler gibi döngülerin her zaman sonsuz olmadığını nasıl kanıtlayabileceğini açıklar i<n. Ve int iilk 4G dizi öğelerine olası kaydırma dizini dizine ekleme işlemini yeniden yapmak yerine bir döngüde işaretçi genişliğine yükselt.
Peter Cordes

34

intTaşma davranışı tanımsız.

factorDöngü gövdesinin dışında okumanız önemli değildir ; o zaman taşmışsa, kodunuzun taşma tanımsız hale gelmeden önce , sonra ve biraz paradoksal olarak davranışı .

Bu kodun korunmasında ortaya çıkabilecek bir sorun, derleyiciler optimizasyon konusunda gittikçe daha agresifleşiyor. Özellikle, tanımsız davranışların asla gerçekleşmediğini varsaydıkları bir alışkanlık geliştiriyorlar. Bu durumda, fordöngüyü tamamen kaldırabilirler .

Bir kullanılamaz unsignediçin türünü factoro zaman istenmeyen dönüşümü hakkında endişe gerekiyordu rağmen intetmek unsignedhem içeren ifadelerde?


12
@nicomp; Neden olmasın?
Bathsheba

12
@ Gilles-PhilippePaillé: Cevabım bunun sorunlu olduğunu söylemiyor mu? Açılış cezam mutlaka OP için değil, daha geniş bir topluluktır Ve factorödevde kendisine geri "kullanılır".
Bathsheba

8
@ Gilles-PhilippePaillé ve bu cevap neden sorunlu olduğunu açıklıyor
idclev 463035818

1
@Bathsheba Haklısın, cevabını yanlış anladım.
Gilles-Philippe Paillé

4
Tanımlanmamış davranışa örnek olarak, bu kod çalışma zamanı denetimleri etkinleştirilmiş olarak derlendiğinde, sonuç döndürmek yerine sonlandırılır. Çalışmak için tanılama işlevlerini kapatmamı gerektiren kod bozuldu.
Simon Richter

23

Gerçek dünyadaki optimize edicileri değerlendirmek anlayışlı olabilir. Döngü açma bilinen bir tekniktir. Temel fikir op döngü unrolling olduğunu

for (int i = 0; i != 3; ++i)
    foo()

perde arkasında daha iyi uygulanabilir

 foo()
 foo()
 foo()

Sabit bir sınır ile kolay durum budur. Ancak modern derleyiciler bunu değişken sınırlar için de yapabilir:

for (int i = 0; i != N; ++i)
   foo();

olur

__RELATIVE_JUMP(3-N)
foo();
foo();
foo();

Açıkçası bu sadece derleyici N <3 olduğunu biliyorsa işe yarar. Orijinal soruya geri dönüyoruz. Derleyici, imzalı taşmanın gerçekleşmediğini bildiğinden, döngünün 32 bit mimarilerde en fazla 9 kez çalışabileceğini bilir.10^10 > 2^32. Bu nedenle 9 iterasyon döngüsü açma yapabilir. Ancak amaçlanan maksimum değer 10 yinelemeydi! .

Ne olabilir, N = 10 ile bir montaj komutuna (9-N) göreceli bir sıçrama elde edersiniz, bu nedenle atlama komutunun kendisi olan -1 ofseti. Hata. Bu, iyi tanımlanmış C ++ için mükemmel geçerli bir döngü optimizasyonudur, ancak verilen örnek sıkı bir sonsuz döngüye dönüşür.


9

İmzalı tamsayı taşması, taşan değerin okunup okunmadığına bakılmaksızın tanımlanmamış davranışla sonuçlanır.

Belki kullanım durumunuzda, ilk yinelemeyi döngüden kaldırabilir ve bunu çevirebilirsiniz.

int result = 0;
int factor = 1;
for (int n = 0; n < 10; ++n) {
    result += n + factor;
    factor *= 10;
}
// factor "is" 10^10 > INT_MAX, UB

bunun içine

int factor = 1;
int result = 0 + factor; // first iteration
for (int n = 1; n < 10; ++n) {
    factor *= 10;
    result += n + factor;
}
// factor is 10^9 < INT_MAX

Optimizasyon etkinken, derleyici yukarıdaki ikinci döngüyü bir koşullu sıçramaya açabilir.


6
Bu biraz fazla teknik olabilir, ancak "imzalı taşma tanımsız davranıştır" aşırı basitleştirilmiştir. Resmi olarak, taşan imzalı bir programın davranışı tanımlanmamıştır. Yani, standart size bu programın ne yaptığını söylemez. Sadece sonuçta taşan bir yanlışlık yok; tüm programda bir sorun var.
Pete Becker

Adil gözlem, cevabımı düzelttim.
elbrunovsky

Veya daha basit olarak, son yinelemeyi soyun ve ölüleri kaldırınfactor *= 10;
Peter Cordes

9

Bu UB; ISO C ++ terimlerinde, tüm programın tüm davranışı, sonunda UB'ye çarpan bir yürütme için tamamen belirtilmez . Klasik örnek, C ++ standardının umduğu kadarıyla, şeytanların burnunuzdan uçmasını sağlayabilir. (Nazal iblislerin gerçek bir olasılık olduğu bir uygulamaya karşı öneriyorum). Daha fazla ayrıntı için diğer yanıtlara bakın.

Derleyiciler derleme zamanında görülebilen UB'ye yol açan görebileceği yürütme yolları için derleme zamanında "soruna neden olabilir", örneğin bu temel bloklara asla ulaşılmadığını varsayın.

Ayrıca bkz. Her C Programcısının Tanımsız Davranış Hakkında Bilmesi Gerekenler (LLVM blogu). Burada açıklandığı gibi, imzalı taşma UB, derleyicilerin for(... i <= n ...), bilinmeyenler için bile, döngülerin sonsuz döngüler olmadığını kanıtlamasını sağlar n. Ayrıca int döngü sayaçlarını işaret uzantısını yeniden yapmak yerine işaretçi genişliğine "yükseltmelerine" olanak tanır. (Dolayısıyla, bu durumda UB'nin sonucu, bir dizinin düşük 64k veya 4G öğelerinin dışına erişiyor olabilir,i değer aralığına .)

Bazı durumlarda derleyiciler, ud2yürütüldüğünde UB'ye neden olabilecek bir blok için x86 gibi yasadışı bir talimat yayınlarlar . (Bir işlev olabilir Not o değil derleyiciler değil genel go Berserk ve UB vurma bir fonksiyonu sayesinde diğer işlevleri, hatta mümkünse yolları kırabilir hiç çağrılabilir, böylece. Yani makine kodu ne için zorunluluk hala işe derler UB'ye yönlendirmeyen tüm girişler.)


Muhtemelen en verimli çözüm, gereksiz olanlardan factor*=10kaçınılması için son yinelemeyi manuel olarak soymaktır.

int result = 0;
int factor = 1;
for (... i < n-1) {   // stop 1 iteration early
    result = ...
    factor *= 10;
}
 result = ...      // another copy of the loop body, using the last factor
 //   factor *= 10;    // and optimize away this dead operation.
return result;

Veya döngü gövdesi büyükse, yalnızca işaretsiz bir tür kullanmayı düşünün factor. Daha sonra imzasızın taşmasına izin verebilirsiniz ve sadece 2 gücüne (imzasız türdeki değer bitlerinin sayısı) iyi tanımlanmış bir sarma yapar.

Bu Kullanmaya bile gayet ile sizin unsigned-> imzalı dönüşüm taşıyor asla, özellikle imzalı türleri.

İmzasız ve imzalanmış tamamlayıcı arasındaki dönüşüm ücretsizdir (tüm değerler için aynı bit örüntüsü); C ++ standardı tarafından belirtilen int -> unsigned için modulo kaydırma, tamamlayıcı veya işaret / büyüklüğünün aksine, sadece aynı bit desenini kullanmayı kolaylaştırır.

Ve imzasız-> imzalı benzer şekilde önemsizdir, ancak daha büyük değerler için uygulama tanımlıdır INT_MAX. Eğer değilseniz kullanarak son yineleme gelen büyük işaretsiz bir sonuç, endişelenecek bir şey yok. Ancak öyleyse bkz . İmzasızdan imzalıya dönüşüm tanımsız mı? . Value-fit-case durumu, uygulama tanımlıdır ; bu, bir uygulamanın bir miktar davranış seçmesi gerektiği anlamına gelir ; aklı başında olanlar sadece imzasız bit desenini keser (gerekirse) ve imzalı olarak kullanırlar, çünkü bu aralık içi değerler için ekstra çalışma olmadan aynı şekilde çalışır. Ve kesinlikle UB değil. Böylece büyük işaretsiz değerler negatif işaretli tamsayılar haline gelebilir. örneğin gcc ve clang'dan sonra int x = u; optimizasyon yokx>=0-fwrapvdavranışı tanımladıkları için, onlar olmadan da her zaman doğrudurlar .


2
Buradaki düşüşü anlamıyorum. Çoğunlukla son yinelemenin soyulması hakkında mesaj göndermek istedim. Ama yine de soruyu cevaplamak için, UB'yi nasıl atacağımıza dair bazı noktaları bir araya getirdim. Daha fazla ayrıntı için diğer yanıtlara bakın.
Peter Cordes

5

Döngüde birkaç montaj talimatını tolere edebiliyorsanız,

int factor = 1;
for (int j = 0; j < n; ++j) {
    ...
    factor *= 10;
}

Yazabilirsin:

int factor = 0;
for (...) {
    factor = 10 * factor + !factor;
    ...
}

son çarpmayı önlemek için. !factorbir şube tanıtmayacak:

    xor     ebx, ebx
L1:                       
    xor     eax, eax              
    test    ebx, ebx              
    lea     edx, [rbx+rbx*4]      
    sete    al    
    add     ebp, 1                
    lea     ebx, [rax+rdx*2]      
    mov     edi, ebx              
    call    consume(int)          
    cmp     r12d, ebp             
    jne     .L1                   

Bu kod

int factor = 0;
for (...) {
    factor = factor ? 10 * factor : 1;
    ...
}

optimizasyondan sonra da dalsız montaj ile sonuçlanır:

    mov     ebx, 1
    jmp     .L1                   
.L2:                               
    lea     ebx, [rbx+rbx*4]       
    add     ebx, ebx
.L1:
    mov     edi, ebx
    add     ebp, 1
    call    consume(int)
    cmp     r12d, ebp
    jne     .L2

(GCC 8.3.0 ile derlenmiştir -O3)


1
Döngü gövdesi büyük olmadığı sürece son yinelemeyi soymak daha basittir. Bu akıllıca bir saldırıdır, ancak döngü ile taşınan bağımlılık zincirinin gecikmesini factorhafifçe arttırır . Ya da değil: Bu 2x LEA derlediğinde sadece yapmak LEA + ADD olarak verimli olarak ilgili f *= 10olarak f*5*2sahip testilk olarak gizlidir gecikmesi LEA. Ancak, döngü içinde ekstra uopslara mal olur, bu nedenle olası bir iş akışı olumsuz (veya en azından hiper iş parçacığı dostu bir sorun)
Peter Cordes

4

İfadenin parantezinde ne olduğunu göstermediniz for, ancak bunun böyle bir şey olduğunu varsayacağım:

for (int n = 0; n < 10; ++n) {
    result = ...
    factor *= 10;
}

Sayaç artışını ve döngü sonlandırma kontrolünü gövdeye taşıyabilirsiniz:

for (int n = 0; ; ) {
    result = ...
    if (++n >= 10) break;
    factor *= 10;
}

Döngüdeki montaj talimatlarının sayısı aynı kalacaktır.

Andrei Alexandrescu'nun "Hız İnsanların Aklında Bulunuyor" sunumundan esinlenmiştir.


2

İşlevi düşünün:

unsigned mul_mod_65536(unsigned short a, unsigned short b)
{
  return (a*b) & 0xFFFFu;
}

Yayınlanan Gerekçe göre, Standard yazarları bu işlev (örneğin) 0xc000 ve 0xc000 argümanlarıyla bir sıradan 32 bit bilgisayar üzerinde çağrılan olsaydı ait işlenen teşvik umuyordum *için signed inthesaplama -0x10000000 verim neden olur dönüştürüldüklerinde , teşvik ettikleri gibi unsignedverilecek 0x90000000ucevap . Bununla birlikte, gcc bazen bir taşma meydana geldiğinde bu işlevi saçma sapan davranışlarda optimize eder. Bazı giriş kombinasyonlarının taşmaya neden olabileceği herhangi bir kod , kasıtlı olarak yanlış biçimlendirilmiş girdilerin içerik oluşturucularının seçtikleri rastgele kod yürütmesine izin verilemezse , seçenekle işlenmelidir .unsigned shortunsigned-fwrapv


1

Neden olmasın:

int result = 0;
int factor = 10;
for (...) {
    factor *= 10;
    result = ...
}
return result;

Bu, ...döngü gövdesini factor = 1veya factor = 10yalnızca 100 ve üstü için çalıştırmaz . İlk yinelemeyi soymanız vefactor = 1 bunun işe yaramasını istiyorsanız yine de başlamanız gerekir.
Peter Cordes

1

Tanımsız Davranışın birçok farklı yüzü vardır ve kabul edilebilir olan şey kullanıma bağlıdır.

gerçek zamanlı grafik uygulamasında toplam CPU süresinin büyük bir kısmını tüketen sıkı iç döngü

Bu, kendi başına, alışılmadık bir şey, ama öyle olabilir mi ... eğer gerçekten durum buysa, UB büyük olasılıkla âlemin içindedir "izin verilebilir, kabul edilebilir" . Grafik programlama kesmek ve çirkin şeyler için kötü şöhretlidir. "Çalıştığı" sürece ve genellikle bir çerçeve üretmek için 16,6 ms'den uzun sürmez, genellikle kimse umursamaz. Ama yine de, UB'yi çağırmanın ne anlama geldiğinin farkında olun.

Birincisi, standart var. Bu açıdan, tartışacak bir şey yoktur ve haklı göstermenin bir yolu yoktur, kodunuz geçersizdir. Ifs veya whens yoktur, sadece geçerli bir kod değildir. Bunun sizin bakış açınızdan orta parmak yukarı olduğunu ve zamanın% 95-99'unu yine de iyi olacaksınız diyebilirsiniz.

Sonra, donanım tarafı var. Bazı vardır nadir, garip bu sorunun mimariler. Demek "nadir, tuhaf" çünkü üzerinde bir (ya da tüm bilgisayarların% 80 oluşturan mimariye iki arada tüm bilgisayarların% 95'ini oluşturan mimarileri) taşma bir olan "evet, ne olursa olsun, umurumda değil" donanım düzeyinde bir şey. Bir çöp (hala tahmin edilebilir olmasına rağmen) sonuç aldığınızdan emin olursunuz, ancak kötü bir şey olmaz.
Bu değilher mimaride, taşma üzerinde bir tuzak elde edebilirsiniz (bir grafik uygulamasından nasıl konuştuğunuzu görseniz de, böyle garip bir mimaride olma şansı oldukça azdır). Taşınabilirlik bir sorun mu? Eğer öyleyse, kaçınmak isteyebilirsiniz.

Son olarak, derleyici / optimize edici tarafı var. Taşmanın tanımsız olmasının bir nedeni, sadece bir kerede donanım ile başa çıkmak için en kolay olanı bırakmasıdır. Ancak başka bir neden, örneğin her zamankinden daha büyük x+1olması garanti edilir xve derleyici / optimizer bu bilgiden yararlanabilir. Şimdi, daha önce bahsedilen durum için, derleyicilerin bu şekilde hareket ettiği ve sadece tam blokları çıkardığı bilinmektedir (birkaç yıl önce tam olarak bu nedenle bazı doğrulama kodlarına sahip olan derleyiciye dayanan bir Linux istismarı vardı).
Sizin durumunuz için, derleyicinin bazı özel, tuhaf, optimizasyonlar yaptığından şüpheliyim. Ancak, ne biliyorsun, ne biliyorum. Şüphe duyduğunuzda deneyin. Eğer işe yararsa, gitmekte fayda var.

(Ve son olarak, elbette kod denetimi var, eğer şanssızsanız bunu bir denetçiyle tartışmak için zaman harcamanız gerekebilir.)

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.