Jika saya menyalin pelampung ke variabel lain, apakah mereka akan sama?

167

Saya tahu bahwa menggunakan ==untuk memeriksa persamaan variabel floating-point bukanlah cara yang baik. Tapi saya hanya ingin tahu itu dengan pernyataan berikut:

float x = ...

float y = x;

assert(y == x)

Sejak ydisalin dari x, akankah pernyataan itu benar?

Wei Li
sumber
78
Biarkan saya memberikan hadiah 50 kepada seseorang yang benar-benar membuktikan ketidaksetaraan dengan demonstrasi dengan kode nyata. Saya ingin melihat hal 80 vs 64 bit beraksi. Ditambah 50 lainnya untuk penjelasan tentang kode assembler yang dihasilkan yang menunjukkan satu variabel berada dalam register dan yang lainnya tidak (atau apa pun alasan ketidaksetaraan mungkin, saya ingin dijelaskan pada tingkat rendah).
Thomas Weller
1
@ThomasWeller bug GCC tentang ini: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; Namun, saya baru saja mencoba repro pada sistem x86-64 dan tidak, bahkan dengan -fast-matematika. Saya menduga Anda membutuhkan GCC lama pada sistem 32-bit.
pjc50
5
@ pjc50: Sebenarnya Anda membutuhkan sistem 80-bit untuk mereproduksi bug 323; itu adalah FPU 80x87 yang menyebabkan masalah. x86-64 menggunakan SSE FPU. Bit tambahan menyebabkan masalah, karena mereka bulat ketika menumpahkan nilai ke float 32 bit.
MSalters
4
Jika teori MSalters benar (dan saya kira itu benar), maka Anda dapat repro dengan mengkompilasi 32-bit ( -m32), atau dengan menginstruksikan GCC untuk menggunakan x87 FPU ( -mfpmath=387).
Cody Gray
4
Ubah "48 bit" menjadi "80 bit", dan kemudian Anda dapat menghapus kata sifat "mitos" di sana, @Hot. Itulah tepatnya yang sedang dibahas segera sebelum komentar Anda. X87 (FPU untuk arsitektur x86) menggunakan register 80-bit, format "extended-precision".
Cody Grey

Jawaban:

125

Selain assert(NaN==NaN);case yang ditunjukkan oleh kmdreko, Anda dapat memiliki situasi dengan x87-matematika, ketika float 80bit disimpan sementara untuk memori dan kemudian dibandingkan dengan nilai-nilai yang masih disimpan di dalam register.

Mungkin contoh minimal, yang gagal dengan gcc9.2 saat dikompilasi dengan -O2 -m32 :

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Demo Godbolt: https://godbolt.org/z/X-Xt4R

Itu volatile mungkin dapat dihilangkan, jika Anda berhasil membuat daftar-tekanan yang cukup telah ydisimpan dan reloaded dari memori (tapi membingungkan cukup compiler, tidak menghilangkan perbandingan semua-bersama-sama).

Lihat referensi FAQ GCC:

chtz
sumber
2
Tampaknya aneh bahwa bit tambahan akan dipertimbangkan dalam membandingkan a floatdengan presisi standar dengan presisi ekstra.
Nat
13
@Nat Ini adalah aneh; ini adalah bug .
Lightness Races di Orbit
13
@ ThomasWeller Tidak, itu penghargaan yang masuk akal. Meskipun saya ingin jawaban untuk menunjukkan bahwa ini adalah perilaku yang tidak patuh
Lightness Races in Orbit
4
Saya dapat memperluas jawaban ini, menunjukkan apa yang sebenarnya terjadi dalam kode perakitan, dan bahwa ini benar-benar melanggar standar - meskipun saya tidak akan menyebut diri saya seorang pengacara bahasa, jadi saya tidak dapat menjamin bahwa tidak ada yang tidak jelas. klausa yang secara eksplisit memungkinkan perilaku itu. Saya berasumsi OP lebih tertarik pada komplikasi praktis pada kompiler yang sebenarnya, bukan pada kompiler yang sepenuhnya bebas bug dan sepenuhnya memenuhi syarat (yang secara de facto tidak ada, saya kira).
chtz
4
Layak disebutkan yang -ffloat-storetampaknya menjadi cara untuk mencegah hal ini.
OrangeDog
116

Ini tidak akan benar jika xini NaN, karena perbandingan pada NaNyang selalu salah (ya, bahkan NaN == NaN). Untuk semua kasus lain (nilai normal, nilai subnormal, infinities, nol) pernyataan ini akan benar.

Saran untuk menghindari ==mengapung berlaku untuk perhitungan karena angka floating point tidak dapat mengekspresikan banyak hasil persis ketika digunakan dalam ekspresi aritmatika. Tugas bukanlah perhitungan dan tidak ada alasan bahwa tugas akan menghasilkan nilai yang berbeda dari yang asli.


Evaluasi presisi yang diperluas harus menjadi masalah jika standar diikuti. Dari <cfloat>diwarisi dari C [5.2.4.2.2.8] ( penekanan milik saya ):

Kecuali untuk penugasan dan pemeran (yang menghapus semua jangkauan ekstra dan presisi) , nilai operasi dengan operan mengambang dan nilai yang tunduk pada konversi aritmatika biasa dan konstanta mengambang dievaluasi ke format yang jangkauan dan ketelitiannya mungkin lebih besar dari yang dipersyaratkan oleh Tipe.

Namun, seperti yang ditunjukkan oleh komentar, beberapa kasus dengan penyusun, opsi bangun, dan target tertentu dapat menjadikan ini salah secara paradoks.

kmdreko
sumber
10
Bagaimana jika xdihitung dalam register di baris pertama, menjaga presisi lebih dari minimum untuk a float. The y = xmungkin dalam memori, hanya menjaga floatpresisi. Maka tes untuk kesetaraan akan dilakukan dengan ingatan terhadap register, dengan tindakan berbeda, dan dengan demikian tidak ada jaminan.
David Schwartz
5
x+pow(b,2)==x+pow(a,3)dapat berbeda dari auto one=x+pow(b,2); auto two=y+pow(a,3); one==twokarena satu dapat membandingkan menggunakan lebih presisi daripada yang lain (jika satu / dua adalah nilai 64 bit di ram, sedangkan nilai intermediste adalah bit 80ish pada fpu). Jadi tugas kadang bisa melakukan sesuatu.
Yakk - Adam Nevraumont
22
@ evg Tentu! Jawaban saya cukup mengikuti standar. Semua taruhan dibatalkan jika Anda memberitahu kompiler Anda untuk tidak membatasi, terutama ketika mengaktifkan matematika cepat.
kmdreko
11
@ Voo Lihat kutipan dalam jawaban saya. Nilai RHS ditugaskan ke variabel pada LHS. Tidak ada pembenaran hukum untuk nilai yang dihasilkan LHS berbeda dari nilai RHS. Saya menghargai bahwa beberapa kompiler memiliki bug dalam hal ini. Tetapi apakah sesuatu yang disimpan dalam register seharusnya tidak ada hubungannya dengan itu.
Lightness Races di Orbit
6
@ Vo: Di ISO C ++, pembulatan untuk mengetik lebar seharusnya terjadi pada tugas apa pun. Dalam kebanyakan kompiler yang menargetkan x87, itu benar-benar hanya terjadi ketika kompiler memutuskan untuk menumpahkan / memuat ulang. Anda dapat memaksanya dengan gcc -ffloat-storekepatuhan yang ketat. Tetapi pertanyaan ini adalah tentang x=y; x==y; tanpa melakukan apa pun pada keduanya. Jika ysudah dibulatkan agar sesuai dengan float, mengkonversi ke double atau long double dan kembali tidak akan mengubah nilai. ...
Peter Cordes
34

Ya, ypasti akan mengambil nilai x:

[expr.ass]/2: Dalam penugasan sederhana (=), objek yang dirujuk oleh operan kiri dimodifikasi ([defns.access]) dengan mengganti nilainya dengan hasil operan kanan.

Tidak ada peluang untuk nilai-nilai lain yang ditugaskan.

(Yang lain telah menunjukkan bahwa perbandingan kesetaraan ==akan tetap dievaluasifalse untuk nilai NaN.)

Masalah umum dengan floating-point ==adalah mudah untuk tidak memiliki cukup nilai yang Anda pikir Anda lakukan. Di sini, kita tahu bahwa kedua nilai itu, apa pun itu, adalah sama.

Lightness Races di Orbit
sumber
7
@ThomasWeller Itu adalah bug yang dikenal dalam implementasi yang akibatnya tidak sesuai. Senang menyebutkannya!
Lightness Races di Orbit
Pada awalnya, saya berpikir bahwa bahasa yang memperjelas perbedaan antara "nilai" dan "hasil" akan bertentangan, tetapi perbedaan ini tidak harus tanpa perbedaan dengan bahasa C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6, atau C5.3, 7.1.6 dari draf Standar yang Anda kutip.
Eric Towers
@EricTowers Maaf, bisakah Anda menjelaskan referensi itu? Saya tidak menemukan apa yang Anda tunjukkan
Lightness Races di Orbit
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 , dan C5.3 .
Eric Towers
@ EricTowers Ya, masih tidak mengikuti Anda. Tautan pertama Anda ke indeks Apendiks C (tidak memberi tahu saya apa pun). Keempat tautan Anda selanjutnya semuanya masuk [expr]. Jika saya mengabaikan tautan dan fokus pada kutipan, saya bingung dengan hal itu, misalnya C.5.3 tampaknya tidak membahas penggunaan istilah "nilai" atau istilah "hasil" (meskipun itu tidak gunakan "hasil" sekali dalam konteks bahasa Inggris yang normal). Mungkin Anda bisa lebih jelas menggambarkan di mana menurut Anda standar membuat perbedaan, dan memberikan satu kutipan yang jelas untuk hal ini terjadi. Terima kasih!
Lightness Races di Orbit
3

Ya, dalam semua kasus (mengabaikan masalah NaNs dan x87), ini akan benar.

Jika Anda melakukan memcmppada mereka, Anda akan dapat menguji kesetaraan sambil dapat membandingkan NaNs dan sNaNs. Ini juga akan meminta kompiler mengambil alamat variabel yang akan memaksa nilai menjadi 32-bit floatalih - alih yang 80-bit. Ini akan menghilangkan masalah x87. Penegasan kedua di sini dimaksudkan untuk gagal menunjukkan bahwa ==tidak akan membandingkan NaNs sebagai benar:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Perhatikan bahwa jika NaNs memiliki representasi internal yang berbeda (mis. Mantissa yang berbeda), maka memcmptidak akan membandingkan true.

SS Anne
sumber
1

Dalam kasus biasa, itu akan dievaluasi ke true. (atau pernyataan tegas tidak akan melakukan apa-apa)

Edit :

Dengan 'kasus biasa' yang saya maksud adalah mengecualikan skenario yang disebutkan di atas (seperti nilai NaN dan unit floating point 80x87) sebagaimana ditunjukkan oleh pengguna lain.

Mengingat usang 8087 chip dalam konteks saat ini, masalah ini agak terisolasi dan untuk pertanyaan yang berlaku dalam keadaan saat ini arsitektur floating-point yang digunakan, itu berlaku untuk semua kasus kecuali untuk NaNs.

(referensi tentang 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Kudos to @chtz untuk mereproduksi contoh yang baik dan @kmdreko untuk menyebutkan NaNs - tidak tahu tentang mereka sebelumnya!

Anirban166
sumber
1
Saya pikir itu sepenuhnya mungkin untuk xberada di register floating point sementara ydiambil dari memori. Memori mungkin memiliki presisi kurang dari register, menyebabkan perbandingan gagal.
David Schwartz
1
Itu mungkin satu kasus untuk kesalahan, saya belum berpikir sejauh itu. (karena OP tidak memberikan kasus khusus, saya mengasumsikan tidak ada kendala tambahan)
Anirban166
1
Saya tidak begitu mengerti apa yang Anda katakan. Ketika saya mengerti pertanyaannya, OP bertanya apakah menyalin pelampung dan kemudian menguji kesetaraan dijamin berhasil. Jawaban Anda sepertinya mengatakan "ya". Saya bertanya mengapa jawabannya tidak.
David Schwartz
6
Hasil edit membuat jawaban ini salah. Standar C ++ mengharuskan penugasan mengonversi nilai ke tipe tujuan — presisi berlebih dapat digunakan dalam evaluasi ekspresi tetapi mungkin tidak dipertahankan melalui penugasan. Tidak penting apakah nilainya disimpan dalam register atau memori; standar C ++ mengharuskannya, seperti kode yang ditulis, floatnilai tanpa presisi ekstra.
Eric Postpischil
2
@AProgrammer Mengingat bahwa (n sangat) kompiler buggy secara teoritis dapat menyebabkan int a=1; int b=a; assert( a==b );pernyataan, saya pikir itu hanya masuk akal untuk menjawab pertanyaan ini sehubungan dengan kompiler yang berfungsi dengan baik (sementara mungkin mencatat bahwa beberapa versi dari beberapa kompiler melakukan / memiliki -Dapat diketahui-untuk mendapatkan ini salah). Dalam istilah praktis, jika karena alasan tertentu kompiler tidak menghapus presisi ekstra dari hasil tugas register-disimpan, ia harus melakukannya sebelum menggunakan nilai itu.
TripeHound
-1

Ya, itu akan selalu mengembalikan True , kecuali jika itu NaN . Jika nilai variabel NaN maka selalu mengembalikan False !

Valentin Popescu
sumber