Saya mencoba untuk memeriksa di mana float
kehilangan 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?
c++
floating-point
clang
geza
sumber
sumber
gcc
melakukan pengoptimalan loop tak terbatas yang sama jika Anda mengompilasi-Ofast
, jadi pengoptimalan inigcc
dianggap tidak aman, tetapi dapat melakukannya.ucomiss xmm0,xmm0
dibandingkan(float)i
dengan 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 / dikembalikan16777216
? Dengan kompilator / versi / opsi apa itu? Karena itu akan menjadi bug kompilator. gcc mengoptimalkan kode Anda dengan benarjnp
sebagai cabang perulangan ( godbolt.org/z/XJYWeu ): terus lakukan perulangan selama operan!=
tidak menjadi NaN.-ffast-math
opsi yang secara implisit diaktifkan oleh-Ofast
yang 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 satujmp
instruksi. 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(float) i
berbeda dari nilai matematikai
, maka hasilnya (nilai yang dikembalikan dalamreturn
pernyataan) adalah 16.777.217, bukan 16.777.216.Jawaban:
Seperti yang ditunjukkan @Angew ,
!=
operator membutuhkan tipe yang sama di kedua sisi.(float)i != i
Hasil 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
cvtsi2ss
dan tidakucomiss xmm0,xmm0
membandingkan(float)i
dengan dirinya sendiri. (Itu adalah petunjuk pertama Anda bahwa sumber C ++ Anda tidak berarti apa yang Anda pikir seperti yang dijelaskan oleh jawaban @ Angew.)x != x
hanya benar jika "tidak diurutkan" karenax
adalah NaN. (INFINITY
membandingkan sama dengan dirinya dalam matematika IEEE, tetapi NaN tidak.NAN == NAN
salah,NAN != NAN
benar).gcc7.4 dan yang lebih lama mengoptimalkan kode Anda dengan benar
jnp
sebagai cabang perulangan ( https://godbolt.org/z/fyOhW1 ): terus lakukan perulangan selama operanx != x
tidak NaN. (gcc8 dan yang lebih baru juga memeriksaje
keluarnya 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)i
yang harus sama, dan membuktikan bahwai -> float
tidak pernah NaN untuk rentang yang memungkinkanint
.(Meskipun mengingat bahwa loop ini akan mengenai UB yang ditandatangani-overflow, diizinkan untuk memancarkan secara harfiah asm apa pun yang diinginkannya, termasuk
ud2
instruksi 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
-fwrapv
untuk membuat luapan bilangan bulat yang ditandatangani terdefinisi dengan baik (sebagai sampul pelengkap 2). https://godbolt.org/z/t9A8t_Bahkan mengaktifkan
-fno-trapping-math
tidak membantu. ( Sayangnya , default GCC diaktifkan-ftrapping-math
meskipun 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 mengonversi16777217
ke 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. Tidakint
->float
konversi dapat menghasilkan NaN, jadix != x
tidak mungkin benar.Semua compiler ini mengoptimalkan implementasi C ++ yang menggunakan presisi tunggal IEEE 754 (binary32)
float
dan 32-bitint
.The bugfixed
(int)(float)i != i
lingkaran akan memiliki UB pada C ++ implementasi dengan sempit 16-bitint
dan / atau lebih luasfloat
, karena Anda akan memukul menandatangani-bulat meluap UB sebelum mencapai bilangan bulat pertama yang tidak persis representable sebagaifloat
.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_RADIX
danFLT_MANT_DIG
, didefinisikan dalam<climits>
. Atau setidaknya Anda bisa secara teori, jikafloat
benar-benar sesuai dengan model float IEEE daripada beberapa jenis representasi bilangan nyata seperti Posit / unum.Saya tidak yakin seberapa besar standar ISO C ++ tentang
float
perilaku dan apakah format yang tidak didasarkan pada bidang eksponen dan signifikansi lebar tetap akan memenuhi standar.Dalam komentar:
Apakah Anda mengklaim memiliki simpul ini untuk dicetak / dikembalikan
16777216
?Pembaruan: karena komentar itu telah dihapus, saya kira tidak. Mungkin OP hanya mengutip
float
sebelum bilangan bulat pertama yang tidak dapat secara tepat direpresentasikan sebagai 32-bitfloat
. 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
float
adalah tepat di bawah 2 ^ 128, yang terlalu besar untuk genapint64_t
.)Jika ada kompilator yang keluar dari loop asli dan mencetaknya, itu akan menjadi bug kompilator.
sumber
frapw
, tetapi saya yakin GCC 10-ffinite-loops
dirancang untuk situasi seperti ini.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)
sumber
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
jelas UB di sana(int)(float)i != i
adalah UB? Bagaimana Anda menyimpulkan itu? Ya itu tergantung pada implementasi properti yang ditentukan (karenafloat
tidak diharuskan menjadi IEEE754 binary32), tetapi pada implementasi yang diberikan itu didefinisikan dengan baik kecualifloat
dapat secara tepat mewakili semuaint
nilai positif sehingga kami mendapatkan bilangan bulat bertanda tangan meluap UB. ( en.cppreference.com/w/cpp/types/climits mendefinisikanFLT_RADIX
danFLT_MANT_DIG
menentukan ini). Dalam hal-hal yang ditentukan implementasi pencetakan umum, sepertistd::cout << sizeof(int)
tidak UB ...reinterpret_cast<int>(float)
tidak persis UB, itu hanya kesalahan sintaks / salah bentuk. Akan lebih baik jika sintaks itu diizinkan untuk jenis-punning floatint
sebagai alternatifmemcpy
(yang didefinisikan dengan baik), tetapireinterpret_cast<>
hanya berfungsi pada jenis penunjuk, saya kira.x != x
itu benar. Lihat langsung di coliru . Di C juga.