Apakah pengoptimalan floating-point ini diperbolehkan?

90

Saya mencoba untuk memeriksa di mana floatkehilangan kemampuan untuk secara tepat mewakili bilangan bulat besar. Jadi saya menulis cuplikan kecil ini:

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

Kode ini sepertinya bekerja dengan semua kompiler, kecuali clang. Clang menghasilkan loop tak terbatas sederhana. Godbolt .

Apakah ini diperbolehkan? Jika ya, apakah ini masalah QoI?

geza
sumber
@geza Saya akan tertarik untuk mendengar nomor yang dihasilkan!
nada
5
gccmelakukan pengoptimalan loop tak terbatas yang sama jika Anda mengompilasi -Ofast, jadi pengoptimalan ini gccdianggap tidak aman, tetapi dapat melakukannya.
12345ieee
3
g ++ juga menghasilkan loop tak terbatas, tetapi tidak mengoptimalkan pekerjaan dari dalamnya. Anda dapat melihatnya ucomiss xmm0,xmm0dibandingkan (float)idengan dirinya sendiri. Itu adalah petunjuk pertama Anda bahwa sumber C ++ Anda tidak berarti seperti yang Anda pikirkan. Apakah Anda mengklaim memiliki simpul ini untuk dicetak / dikembalikan 16777216? Dengan kompilator / versi / opsi apa itu? Karena itu akan menjadi bug kompilator. gcc mengoptimalkan kode Anda dengan benar jnpsebagai cabang perulangan ( godbolt.org/z/XJYWeu ): terus lakukan perulangan selama operan != tidak menjadi NaN.
Peter Cordes
4
Secara khusus, ini adalah -ffast-mathopsi yang secara implisit diaktifkan oleh -Ofastyang memungkinkan GCC untuk menerapkan pengoptimalan floating-point yang tidak aman dan dengan demikian menghasilkan kode yang sama seperti Clang. MSVC berperilaku persis sama: tanpa /fp:fast, MSVC menghasilkan sekumpulan kode yang menghasilkan loop tak terbatas; dengan /fp:fast, itu memancarkan satu jmpinstruksi. Saya berasumsi bahwa tanpa secara eksplisit mengaktifkan pengoptimalan FP yang tidak aman, compiler ini akan terpaku pada persyaratan IEEE 754 terkait nilai NaN. Agak menarik bahwa Clang sebenarnya tidak. Alat analisa statisnya lebih baik. @ 12345ieee
Cody Gray
1
@geza: Jika kode melakukan apa yang Anda inginkan, memeriksa kapan nilai matematika (float) iberbeda dari nilai matematika i, maka hasilnya (nilai yang dikembalikan dalam returnpernyataan) adalah 16.777.217, bukan 16.777.216.
Eric Postpischil

Jawaban:

49

Seperti yang ditunjukkan @Angew , !=operator membutuhkan tipe yang sama di kedua sisi. (float)i != iHasil promosi RHS juga mengambang, jadi kami punya (float)i != (float)i.


g ++ juga menghasilkan loop tak terbatas, tetapi tidak mengoptimalkan pekerjaan dari dalamnya. Anda dapat melihatnya mengubah int-> float dengan cvtsi2ssdan tidak ucomiss xmm0,xmm0membandingkan (float)idengan dirinya sendiri. (Itu adalah petunjuk pertama Anda bahwa sumber C ++ Anda tidak berarti apa yang Anda pikir seperti yang dijelaskan oleh jawaban @ Angew.)

x != xhanya benar jika "tidak diurutkan" karena xadalah NaN. ( INFINITYmembandingkan sama dengan dirinya dalam matematika IEEE, tetapi NaN tidak. NAN == NANsalah, NAN != NANbenar).

gcc7.4 dan yang lebih lama mengoptimalkan kode Anda dengan benar jnpsebagai cabang perulangan ( https://godbolt.org/z/fyOhW1 ): terus lakukan perulangan selama operan x != x tidak NaN. (gcc8 dan yang lebih baru juga memeriksa jekeluarnya loop, gagal mengoptimalkan berdasarkan fakta bahwa itu akan selalu benar untuk input non-NaN apa pun). x86 FP membandingkan set PF pada unordered.


Dan BTW, itu berarti pengoptimalan clang juga aman : hanya CSE (float)i != (implicit conversion to float)iyang harus sama, dan membuktikan bahwa i -> floattidak pernah NaN untuk rentang yang memungkinkan int.

(Meskipun mengingat bahwa loop ini akan mengenai UB yang ditandatangani-overflow, diizinkan untuk memancarkan secara harfiah asm apa pun yang diinginkannya, termasuk ud2instruksi ilegal, atau loop tanpa batas kosong terlepas dari apa badan loop sebenarnya.) Tetapi mengabaikan UB yang ditandatangani-overflow , pengoptimalan ini masih 100% legal.


GCC gagal untuk mengoptimalkan badan pengulangan bahkan dengan -fwrapvuntuk membuat luapan bilangan bulat yang ditandatangani terdefinisi dengan baik (sebagai sampul pelengkap 2). https://godbolt.org/z/t9A8t_

Bahkan mengaktifkan -fno-trapping-mathtidak membantu. ( Sayangnya , default GCC diaktifkan
-ftrapping-mathmeskipun penerapan GCC-nya rusak / buggy .) Konversi int-> float dapat menyebabkan pengecualian FP yang tidak tepat (untuk angka yang terlalu besar untuk diwakili dengan tepat), jadi dengan pengecualian yang mungkin dibuka kedoknya, masuk akal untuk tidak mengoptimalkan tubuh lingkaran. (Karena mengonversi 16777217ke float bisa memiliki efek samping yang dapat diamati jika pengecualian tidak tepat dibuka kedoknya.)

Tapi dengan -O3 -fwrapv -fno-trapping-math, itu 100% melewatkan pengoptimalan untuk tidak mengkompilasi ini ke loop tanpa batas kosong. Tanpa #pragma STDC FENV_ACCESS ON, status sticky flag yang merekam pengecualian FP terselubung bukanlah efek samping kode yang dapat diamati. Tidak int-> floatkonversi dapat menghasilkan NaN, jadi x != xtidak mungkin benar.


Semua compiler ini mengoptimalkan implementasi C ++ yang menggunakan presisi tunggal IEEE 754 (binary32) floatdan 32-bit int.

The bugfixed(int)(float)i != i lingkaran akan memiliki UB pada C ++ implementasi dengan sempit 16-bit intdan / atau lebih luas float, karena Anda akan memukul menandatangani-bulat meluap UB sebelum mencapai bilangan bulat pertama yang tidak persis representable sebagai float.

Tetapi UB di bawah kumpulan pilihan yang ditentukan implementasi yang berbeda tidak memiliki konsekuensi negatif saat mengompilasi untuk implementasi seperti gcc atau clang dengan ABI Sistem V x86-64.


BTW, Anda dapat menghitung hasil loop ini secara statis dari FLT_RADIXdan FLT_MANT_DIG, didefinisikan dalam <climits>. Atau setidaknya Anda bisa secara teori, jika floatbenar-benar sesuai dengan model float IEEE daripada beberapa jenis representasi bilangan nyata seperti Posit / unum.

Saya tidak yakin seberapa besar standar ISO C ++ tentang floatperilaku dan apakah format yang tidak didasarkan pada bidang eksponen dan signifikansi lebar tetap akan memenuhi standar.


Dalam komentar:

@geza Saya akan tertarik untuk mendengar nomor yang dihasilkan!

@nada: ini 16777216

Apakah Anda mengklaim memiliki simpul ini untuk dicetak / dikembalikan 16777216?

Pembaruan: karena komentar itu telah dihapus, saya kira tidak. Mungkin OP hanya mengutip floatsebelum bilangan bulat pertama yang tidak dapat secara tepat direpresentasikan sebagai 32-bit float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values yaitu apa yang ingin mereka verifikasi dengan kode buggy ini.

Versi perbaikan bug tentu saja akan dicetak 16777217, bilangan bulat pertama yang tidak dapat direpresentasikan, bukan nilai sebelumnya.

(Semua nilai float yang lebih tinggi adalah bilangan bulat yang tepat, tetapi merupakan kelipatan 2, lalu 4, lalu 8, dll. Untuk nilai eksponen yang lebih tinggi dari lebar signifikan. Banyak nilai bilangan bulat yang lebih tinggi dapat diwakili, tetapi 1 unit di tempat terakhir (dari signifikan) lebih besar dari 1 jadi mereka bukan bilangan bulat yang berdekatan. Terbatas terbesar floatadalah tepat di bawah 2 ^ 128, yang terlalu besar untuk genap int64_t.)

Jika ada kompilator yang keluar dari loop asli dan mencetaknya, itu akan menjadi bug kompilator.

Peter Cordes
sumber
3
@SombreroChicken: tidak, saya belajar elektronika terlebih dahulu (dari beberapa buku teks yang dimiliki ayah saya; dia adalah seorang profesor fisika), kemudian logika digital dan masuk ke CPU / perangkat lunak setelah itu. : P Saya sangat suka memahami sesuatu dari bawah ke atas, atau jika saya memulai dengan level yang lebih tinggi maka saya ingin mempelajari setidaknya sesuatu tentang level di bawah yang memengaruhi bagaimana / mengapa sesuatu bekerja di level saya memikirkan tentang. (misalnya bagaimana asm bekerja dan bagaimana mengoptimalkannya dipengaruhi oleh batasan desain CPU / hal-hal arsitektur cpu. Yang pada gilirannya berasal dari fisika + matematika.)
Peter Cordes
1
GCC mungkin tidak dapat dioptimalkan bahkan dengan frapw, tetapi saya yakin GCC 10 -ffinite-loopsdirancang untuk situasi seperti ini.
MCCCS
64

Perhatikan bahwa operator built-in !=mengharuskan operandnya memiliki tipe yang sama, dan akan mencapainya menggunakan promosi dan konversi jika perlu. Dengan kata lain, kondisi Anda setara dengan:

(float)i != (float)i

Itu seharusnya tidak pernah gagal, dan kode akhirnya akan meluap i, memberikan Program Anda Perilaku Tidak Terdefinisi. Oleh karena itu, perilaku apa pun dimungkinkan.

Untuk memeriksa dengan benar apa yang ingin Anda periksa, Anda harus mengembalikan hasilnya ke int:

if ((int)(float)i != i)
Angew tidak lagi bangga dengan SO
sumber
8
@ Džuris Ini UB. Ada adalah tidak ada satu hasil yang pasti. Kompilator mungkin menyadari bahwa itu hanya dapat berakhir di UB dan memutuskan untuk menghapus loop seluruhnya.
Gugatan Dana Monica
4
@opa maksudmu static_cast<int>(static_cast<float>(i))? reinterpret_castjelas UB di sana
Caleth
6
@NicHartley: Apakah yang Anda katakan (int)(float)i != iadalah UB? Bagaimana Anda menyimpulkan itu? Ya itu tergantung pada implementasi properti yang ditentukan (karena floattidak diharuskan menjadi IEEE754 binary32), tetapi pada implementasi yang diberikan itu didefinisikan dengan baik kecuali floatdapat secara tepat mewakili semua intnilai positif sehingga kami mendapatkan bilangan bulat bertanda tangan meluap UB. ( en.cppreference.com/w/cpp/types/climits mendefinisikan FLT_RADIXdan FLT_MANT_DIGmenentukan ini). Dalam hal-hal yang ditentukan implementasi pencetakan umum, seperti std::cout << sizeof(int)tidak UB ...
Peter Cordes
2
@ Caleth: reinterpret_cast<int>(float)tidak persis UB, itu hanya kesalahan sintaks / salah bentuk. Akan lebih baik jika sintaks itu diizinkan untuk jenis-punning float intsebagai alternatif memcpy(yang didefinisikan dengan baik), tetapi reinterpret_cast<>hanya berfungsi pada jenis penunjuk, saya kira.
Peter Cordes
2
@Peter Hanya untuk NaN, x != xitu benar. Lihat langsung di coliru . Di C juga.
Deduplicator