Bu kayan nokta optimizasyonuna izin veriliyor mu?


90

floatBüyük tam sayıları tam olarak temsil etme yeteneğini nerede kaybettiğini kontrol etmeye çalıştım . Ben de bu küçük pasajı yazdım:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Bu kod clang dışında tüm derleyicilerde çalışıyor gibi görünüyor. Clang, basit bir sonsuz döngü oluşturur. Godbolt .

Buna izin verilir mi? Varsa, bu bir QoI sorunu mu?


@geza Ortaya çıkan sayıyı duymak isterim!
nada

5
gccOnun -Ofastyerine ile derlerseniz aynı sonsuz döngü optimizasyonunu yapar , bu nedenle bu bir optimizasyon gccgüvenli sayılmaz, ancak yapabilir.
12345ieee

3
g ++ ayrıca sonsuz bir döngü oluşturur, ancak içindeki işi optimize etmez. Kendisiyle ucomiss xmm0,xmm0karşılaştırdığını görebilirsiniz (float)i. Bu, C ++ kaynağınızın düşündüğünüz şey anlamına gelmediğine dair ilk ipucunuzdu. Yazdırmak / iade etmek için bu döngüye sahip olduğunuzu mu iddia ediyorsunuz 16777216? Hangi derleyici / sürüm / seçeneklerle birlikte bu? Çünkü bu bir derleyici hatası olur. gcc, kodunuzu jnpdöngü dalı olarak doğru şekilde optimize eder ( godbolt.org/z/XJYWeu ): işlenenler != NaN olmadığı sürece döngüye devam edin .
Peter Cordes

4
Spesifik olarak, GCC'nin güvenli olmayan kayan nokta optimizasyonları uygulamasına ve böylece Clang ile aynı kodu oluşturmasına izin -ffast-mathveren, dolaylı olarak etkinleştirilen seçenektir -Ofast. MSVC tam olarak aynı şekilde davranır: olmadan /fp:fast, sonsuz bir döngü ile sonuçlanan bir grup kod üretir; ile /fp:fasttek bir jmptalimat verir. Güvensiz FP optimizasyonlarını açıkça açmadan, bu derleyicilerin NaN değerleriyle ilgili IEEE 754 gereksinimlerine takılıp kalacağını varsayıyorum. Aslında Clang'ın yapmaması oldukça ilginç. Statik analizörü daha iyidir. @ 12345ieee
Cody Grey

1
@geza: Eğer kod amaçladığınız şeyi yaptıysa, matematiksel değerinin matematiksel değerinden ne zaman (float) ifarklı olduğunu kontrol ederseniz i, sonuç ( returnifadede döndürülen değer ) 16.777.216 değil 16.777.217 olacaktır.
Eric Postpischil

Yanıtlar:


49

@Angew'in belirttiği gibi , !=operatörün her iki tarafta da aynı türe ihtiyacı var. (float)i != iRHS'nin de dalgalanması ile sonuçlanır, bu yüzden var (float)i != (float)i.


g ++ ayrıca sonsuz bir döngü oluşturur, ancak içindeki işi optimize etmez. Bunu birlikte int-> şamandıra dönüştürür görebilirsiniz cvtsi2ssve yapar ucomiss xmm0,xmm0karşılaştırmak için (float)ikendisiyle. (Bu, C ++ kaynağınızın sandığınız şeyi ifade etmediğine dair ilk ipucunuzdu @ Angew'in cevabı açıklıyor.)

x != xxNaN olduğundan yalnızca "sırasız" olduğunda doğrudur . ( INFINITYIEEE matematiğinde kendisine eşittir, ancak NaN değildir. NAN == NANyanlıştır, NAN != NANdoğrudur).

gcc7.4 ve daha eski sürüm, kodunuzu jnpdöngü dalı olarak doğru şekilde optimize eder ( https://godbolt.org/z/fyOhW1 ): işlenenler x != x NaN olmadığı sürece döngüye devam . (gcc8 ve sonraki sürümler ayrıca jedöngüde bir kesinti olup olmadığını kontrol eder ve NaN olmayan herhangi bir girdi için her zaman doğru olacağı gerçeğine dayanarak optimizasyon yapamaz). x86 FP, set PF'yi sırasız olarak karşılaştırır.


Ve BTW, bu clang'ın optimizasyonunun da güvenli olduğu anlamına gelir : sadece CSE'ye ihtiyaç duyar(float)i != (implicit conversion to float)i aynı olduğu ve i -> floatbunun olası aralık için asla NaN olmadığını kanıtlaması gerekir int.

(Bu döngünün işaretli taşma UB'ye çarpacağı göz önüne alındığında, ud2yasadışı bir talimat veya döngü gövdesinin gerçekte ne olduğuna bakılmaksızın boş bir sonsuz döngü dahil olmak üzere, kelimenin tam anlamıyla istediği herhangi bir asm yayınlamasına izin verilir .) , bu optimizasyon hala% 100 yasaldır.


GCC , işaretli tamsayı taşmasını iyi tanımlanmış yapmak için bile-fwrapv döngü gövdesini optimize edemiyor (2'nin tamamlayıcı sarmalaması olarak). https://godbolt.org/z/t9A8t_

Etkinleştirmek bile -fno-trapping-mathyardımcı olmuyor. (GCC'nin varsayılanı, maalesef GCC'nin uygulaması bozuk / hatalı
-ftrapping-math olsa bile etkinleştirmektir .) İnt-> float dönüşümü, kesin olmayan bir FP istisnasına neden olabilir (tam olarak temsil edilemeyecek kadar büyük sayılar için), bu nedenle, muhtemelen maskelenmemiş istisnalar dışında, makul değildir. döngü gövdesini optimize edin. (Çünkü gerçek olmayan istisna maskelenmemişse, float türüne dönüştürmek gözlemlenebilir bir yan etkiye sahip olabilir.)16777217

Ancak -O3 -fwrapv -fno-trapping-math, bunu boş bir sonsuz döngüye derlememek% 100 eksik optimizasyondur. Aksi #pragma STDC FENV_ACCESS ONtakdirde, maskelenmiş FP istisnalarını kaydeden yapışkan bayrakların durumu, kodun gözlemlenebilir bir yan etkisi değildir. Hayır int-> floatdönüştürme NaN ile sonuçlanabilir, bu nedenlex != x doğru olamaz.


Bu derleyicilerin tümü, IEEE 754 tek duyarlıklı (binary32) floatve 32-bit kullanan C ++ uygulamaları için optimize ediyorint .

Hata düzeltilmiş(int)(float)i != i döngü, dar 16 bit intve / veya daha geniş olan C ++ uygulamalarında UB'ye sahip olacaktır float, çünkü tam olarak gösterilemeyen ilk tamsayıya ulaşmadan önce işaretli tamsayı taşması UB'ye ulaşırsınızfloat .

Ancak, farklı bir uygulama tanımlı seçimler kümesi altındaki UB, x86-64 System V ABI ile gcc veya clang gibi bir uygulama için derlerken herhangi bir olumsuz sonuç doğurmaz.


BTW, statik olarak bu döngünün sonucu hesaplamak olabilir FLT_RADIXve FLT_MANT_DIGtanımlanan <climits>. Ya da en azından teoride yapabilirsin, eğerfloat bir Pozit / unum gibi başka bir tür gerçek sayı temsilinden ziyade bir IEEE kayan modeline .

ISO C ++ standardının floatdavranış hakkında ne kadar bilgi verdiğinden ve sabit genişlikli üs ve anlamlı alanlara dayalı olmayan bir formatın standartlara uygun olup olmayacağından emin değilim .


Yorumlarda:

@geza Ortaya çıkan sayıyı duymak isterim!

@nada: 16777216

Yazdırmak / geri dönmek için bu döngüye sahip olduğunuzu mu iddia ediyorsunuz 16777216 ?

Güncelleme: bu yorum silindiğinden, sanmıyorum. Muhtemelen OP, floattam olarak 32 bit olarak temsil edilemeyen ilk tam sayıdan önceki alıntıdır float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values yani bu buggy koduyla neyi doğrulamayı umuyorlar.

Hata düzeltilmiş sürüm elbette yazdırılır 16777217, ilk tam sayı değildir doğrusu bundan önce değerinden daha tam olarak gösterilemeyecek.

(Tüm yüksek kayan değerler tam sayılardır, ancak anlamlı ve genişliğinden daha yüksek üslü değerler için 2'nin, sonra 4'ün, ardından 8'in vb. Katlarıdır. Daha yüksek birçok tam sayı değeri gösterilebilir, ancak son sırada 1 birim (anlamlı) 1'den büyük olduğundan bitişik tamsayı değillerdir. En büyük sonlu float2 ^ 128'in hemen altındadır ve bu bile çift için çok büyüktürint64_t .)

Herhangi bir derleyici orijinal döngüden çıkıp bunu yazdırırsa, bu bir derleyici hatası olur.


3
@SombreroChicken: hayır, önce elektroniği öğrendim (babamın etrafta yattığı bazı ders kitaplarından; o bir fizik profesörüydü), sonra dijital mantık ve ondan sonra CPU'lara / yazılıma girdim. : P Bir şeyleri sıfırdan anlamayı her zaman sevmişimdir ya da daha yüksek bir seviyeyle başlarsam, o zaman en azından aşağıdaki seviyeyle ilgili, şu seviyedeki işlerin nasıl / neden işlediğini etkileyen bir şeyler öğrenmek isterim. hakkında düşünüyorum. (ör. asm nasıl çalışır ve nasıl optimize edileceği, CPU tasarım kısıtlamaları / cpu-mimarisi şeylerinden etkilenir. Bu da fizik + matematikten gelir.)
Peter Cordes

1
GCC, bununla bile optimize edemeyebilir frapw, ancak eminim ki GCC 10'lar -ffinite-loopsböyle durumlar için tasarlanmıştır.
MCCCS

64

!=Yerleşik operatörün , işlenenlerinin aynı türden olmasını gerektirdiğini ve gerekirse yükseltme ve dönüşümleri kullanarak bunu başaracağını unutmayın. Başka bir deyişle, durumunuz şuna eşdeğerdir:

(float)i != (float)i

Bu asla başarısız olmamalı ve bu nedenle kod sonunda taşacak i programınıza Tanımsız Davranış verir. Bu nedenle herhangi bir davranış mümkündür.

Neyi kontrol etmek istediğinizi doğru şekilde kontrol etmek için sonucu şuna geri göndermelisiniz int:

if ((int)(float)i != i)

8
@ Džuris Bu UB. Orada olan kimse kesin sonuç. Derleyici, yalnızca UB ile bitebileceğini fark edebilir ve döngüyü tamamen kaldırmaya karar verebilir.
Fund Monica'nın Davası

4
@opa demek istiyorsun static_cast<int>(static_cast<float>(i))? reinterpret_castbariz UB var
Caleth

6
@NicHartley: (int)(float)i != iUB olduğunu mu söylüyorsun ? Bunu nasıl anlıyorsunuz? Evet, uygulama tanımlı özelliklere bağlıdır (çünkü floatIEEE754 ikili32 olması gerekli değildir), ancak herhangi bir uygulamada float, tüm pozitif intdeğerleri tam olarak temsil edemediği sürece iyi tanımlanmıştır , böylece işaretli tamsayı taşması UB elde ederiz. ( en.cppreference.com/w/cpp/types/climits bunu tanımlar FLT_RADIXve FLT_MANT_DIGbelirler). Genel baskı uygulaması tanımlı şeyler gibi std::cout << sizeof(int)UB ... değil
Peter Cordes

2
@Caleth: reinterpret_cast<int>(float)tam olarak UB değil, sadece bir sözdizimi hatası / biçimsiz . Bu sözdiziminin float türüne intalternatif olarak memcpy(iyi tanımlanmış olan) tip-punning'e izin vermesi güzel olurdu , ama reinterpret_cast<>bence sadece işaretçi türleri üzerinde çalışıyor.
Peter Cordes

2
@Peter Just for NaN, x != xdoğru. Coliru'da canlı izleyin . C de.
Deduplicator
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.