C # Ekspresi float: perilaku aneh saat casting hasilnya float ke int

128

Saya memiliki kode sederhana berikut:

int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;

speed1dan speed2harus memiliki nilai yang sama, tetapi pada kenyataannya, saya punya:

speed1 = 61
speed2 = 62

Saya tahu saya mungkin harus menggunakan Math.Round daripada casting, tapi saya ingin mengerti mengapa nilainya berbeda.

Saya melihat bytecode yang dihasilkan, tetapi kecuali store dan load, opcodesnya sama.

Saya juga mencoba kode yang sama di java, dan saya mendapatkan 62 dan 62 dengan benar.

Adakah yang bisa menjelaskan hal ini?

Sunting: Dalam kode asli, itu tidak langsung 6.2f * 10 tetapi panggilan fungsi * sebuah konstanta. Saya memiliki bytecode berikut:

untuk speed1:

IL_01b3:  ldloc.s    V_8
IL_01b5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ba:  ldc.r4     10.
IL_01bf:  mul
IL_01c0:  conv.i4
IL_01c1:  stloc.s    V_9

untuk speed2:

IL_01c3:  ldloc.s    V_8
IL_01c5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ca:  ldc.r4     10.
IL_01cf:  mul
IL_01d0:  stloc.s    V_10
IL_01d2:  ldloc.s    V_10
IL_01d4:  conv.i4
IL_01d5:  stloc.s    V_11

kita dapat melihat bahwa operan mengapung dan satu-satunya perbedaan adalah stloc/ldloc.

Sedangkan untuk mesin virtual, saya mencoba dengan Mono / Win7, Mono / MacOS, dan .NET / Windows, dengan hasil yang sama.

Baalrukh
sumber
9
Dugaan saya adalah bahwa salah satu operasi dilakukan dalam presisi tunggal sementara yang lain dilakukan dalam presisi ganda. Salah satunya mengembalikan nilai sedikit kurang dari 62, sehingga menghasilkan 61 ketika memotong ke integer.
Gabe
2
Ini adalah masalah presisi titik Float yang khas.
TJHeuvel
3
Mencoba ini di .Net / WinXP, .Net / Win7, Mono / Ubuntu dan Mono / OSX memberikan hasil Anda untuk kedua versi Windows, tetapi 62 untuk speed1 dan speed2 di kedua versi Mono. Terima kasih @BoltClock
Eugen Rieck
6
Pak Lippert ... Anda di sekitar ??
ay 74
6
Pengevaluasi ekspresi konstan kompiler tidak memenangkan hadiah apa pun di sini. Jelas itu memotong 6.2f dalam ekspresi pertama, itu tidak memiliki representasi yang tepat di basis 2 sehingga berakhir sebagai 6.199999. Tetapi tidak melakukannya dalam ekspresi ke-2, mungkin dengan mengaturnya agar tetap dalam ketelitian ganda. Jika tidak demikian, tentu saja untuk kursus, konsistensi floating point tidak pernah menjadi masalah. Ini tidak akan diperbaiki, Anda tahu solusinya.
Hans Passant

Jawaban:

168

Pertama-tama, saya berasumsi bahwa Anda tahu bahwa 6.2f * 10itu bukan 62 karena pembulatan titik mengambang (itu sebenarnya nilai 61.99999809265137 ketika dinyatakan sebagai a double) dan bahwa pertanyaan Anda hanya tentang mengapa dua perhitungan yang tampaknya identik menghasilkan nilai yang salah.

Jawabannya adalah dalam kasus (int)(6.2f * 10), Anda mengambil doublenilai 61.99999809265137 dan memotongnya menjadi bilangan bulat, yang menghasilkan 61.

Dalam kasus float f = 6.2f * 10, Anda mengambil nilai ganda 61.99999809265137 dan membulatkan ke terdekat float, yaitu 62. Anda kemudian memotongnya floatmenjadi bilangan bulat, dan hasilnya adalah 62.

Latihan: Jelaskan hasil dari urutan operasi berikut.

double d = 6.2f * 10;
int tmp2 = (int)d;
// evaluate tmp2

Pembaruan: Seperti yang tercantum dalam komentar, ekspresi 6.2f * 10secara formal a floatkarena parameter kedua memiliki konversi implisit floatyang lebih baik daripada konversi implisit double.

Masalah sebenarnya adalah bahwa kompiler diizinkan (tetapi tidak diharuskan) untuk menggunakan perantara yang presisi lebih tinggi dari tipe formal (bagian 11.2.2) . Itu sebabnya Anda melihat perilaku yang berbeda pada sistem yang berbeda: Dalam ekspresi (int)(6.2f * 10), kompiler memiliki opsi untuk menjaga nilai 6.2f * 10dalam bentuk perantara presisi tinggi sebelum dikonversi ke int. Jika ya, maka hasilnya adalah 61. Jika tidak, maka hasilnya adalah 62.

Dalam contoh kedua, tugas eksplisit untuk floatmemaksa pembulatan terjadi sebelum konversi ke bilangan bulat.

Raymond Chen
sumber
6
Saya tidak yakin ini benar-benar menjawab pertanyaan. Mengapa (int)(6.2f * 10)mengambil doublenilai, seperti yang fditentukan itu float? Saya pikir poin utama (masih belum terjawab) ada di sini.
ken2k
1
Saya pikir itu adalah kompiler yang melakukan itu, karena itu float literal * int literal kompiler telah memutuskan bebas untuk menggunakan jenis numerik terbaik, dan untuk menghemat presisi itu pergi untuk ganda (mungkin). (juga akan menjelaskan bahwa IL sama)
George Duckett
5
Poin yang bagus. Jenis 6.2f * 10sebenarnya float, bukan double. Saya pikir kompiler mengoptimalkan perantara, sebagaimana diizinkan oleh paragraf terakhir 11.1.6 .
Raymond Chen
3
Itu memang memiliki nilai yang sama (nilainya 61.99999809265137). Perbedaannya adalah jalur yang dibutuhkan nilai untuk menjadi bilangan bulat. Dalam satu kasus, ia pergi langsung ke integer, dan yang lain melewati floatkonversi terlebih dahulu.
Raymond Chen
38
Jawaban Raymond di sini tentu saja sepenuhnya benar. Saya perhatikan bahwa kompiler C # dan kompiler jit keduanya diperbolehkan untuk menggunakan lebih presisi setiap saat , dan melakukannya secara tidak konsisten . Dan faktanya, mereka melakukan hal itu. Pertanyaan ini telah muncul puluhan kali di StackOverflow; lihat stackoverflow.com/questions/8795550/… untuk contoh terbaru.
Eric Lippert
11

Deskripsi

Angka mengambang jarang tepat. 6.2fadalah sesuatu seperti 6.1999998.... Jika Anda melemparkan ini ke int, itu akan memotongnya dan ini * 10 menghasilkan 61.

Lihat DoubleConverterkelas Jon Skeets . Dengan kelas ini Anda benar-benar dapat memvisualisasikan nilai angka mengambang sebagai string. Doubledan floatkeduanya angka mengambang , desimal tidak (itu adalah angka titik tetap).

Sampel

DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875

Informasi Lebih Lanjut

dknaack
sumber
5

Lihatlah IL:

IL_0000:  ldc.i4.s    3D              // speed1 = 61
IL_0002:  stloc.0
IL_0003:  ldc.r4      00 00 78 42     // tmp = 62.0f
IL_0008:  stloc.1
IL_0009:  ldloc.1
IL_000A:  conv.i4
IL_000B:  stloc.2

Compiler mengurangi ekspresi konstanta waktu kompilasi ke nilai konstannya, dan saya pikir itu membuat perkiraan yang salah di beberapa titik ketika ia mengubah konstanta menjadi int. Dalam kasus speed2, konversi ini dibuat bukan oleh kompiler, tetapi oleh CLR, dan mereka tampaknya menerapkan aturan yang berbeda ...

Thomas Levesque
sumber
1

Dugaan saya adalah bahwa 6.2frepresentasi nyata dengan pelampung presisi 6.1999999sementara 62fmungkin sesuatu yang mirip dengan 62.00000001. (int)casting selalu memotong nilai desimal jadi itu sebabnya Anda mendapatkan perilaku itu.

EDIT : Menurut komentar saya telah mengubah perilaku intcasting ke definisi yang jauh lebih tepat.

Diantara
sumber
Casting ke intmemotong nilai desimal, itu tidak bulat.
Jim D'Angelo
@ James D'Angelo: Maaf bahasa Inggris bukan bahasa utama saya. Tidak tahu kata yang tepat jadi saya mendefinisikan perilaku sebagai "pembulatan saat berurusan dengan angka positif" yang pada dasarnya menggambarkan perilaku yang sama. Tapi ya, poin yang diambil, truncate adalah kata yang tepat untuk itu.
Perantara
tidak masalah, itu hanya symantics tetapi dapat menyebabkan masalah jika seseorang mulai berpikir float-> intmelibatkan pembulatan. = D
Jim D'Angelo
1

Saya mengkompilasi dan membongkar kode ini (pada Win7 / .NET 4.0). Saya kira kompilator mengevaluasi ekspresi konstanta mengambang sebagai dobel.

int speed1 = (int)(6.2f * 10);
   mov         dword ptr [rbp+8],3Dh       //result is precalculated (61)

float tmp = 6.2f * 10;
   movss       xmm0,dword ptr [000004E8h]  //precalculated (float format, xmm0=0x42780000 (62.0))
   movss       dword ptr [rbp+0Ch],xmm0 

int speed2 = (int)tmp;
   cvttss2si   eax,dword ptr [rbp+0Ch]     //instrunction converts float to Int32 (eax=62)
   mov         dword ptr [rbp+10h],eax 
Rodji
sumber
0

Singlehanya mempertahankan 7 digit dan ketika casting ke Int32kompiler memotong semua angka floating point. Selama konversi satu atau lebih digit signifikan dapat hilang.

Int32 speed0 = (Int32)(6.2f * 100000000); 

memberikan hasil 619999980 sehingga (Int32) (6.2f * 10) memberikan 61.

Ini berbeda ketika dua Single dikalikan, dalam hal ini tidak ada operasi terpotong tetapi hanya perkiraan.

Lihat http://msdn.microsoft.com/en-us/library/system.single.aspx

Massimo Zerbini
sumber
-4

Apakah ada alasan Anda mengetik casting untuk intbukannya parsing?

int speed1 = (int)(6.2f * 10)

akan membaca

int speed1 = Int.Parse((6.2f * 10).ToString()); 

Perbedaannya mungkin berkaitan dengan pembulatan: jika Anda melemparkan ke doubleAnda mungkin akan mendapatkan sesuatu seperti 61.78426.

Harap perhatikan output berikut

int speed1 = (int)(6.2f * 10);//61
double speed2 = (6.2f * 10);//61.9999980926514

Itu sebabnya Anda mendapatkan nilai yang berbeda!

Neo
sumber
1
Int.Parsemengambil string sebagai parameter.
ken2k
Anda hanya dapat mengurai string, saya kira maksud Anda mengapa Anda tidak menggunakan System.Convert
vc 74