C # Float ifadesi: sonuç float'ı int'e dönüştürürken garip davranış


128

Aşağıdaki basit koda sahibim:

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

speed1ve speed2aynı değere sahip olmalı, ama aslında:

speed1 = 61
speed2 = 62

Muhtemelen döküm yerine Math.Round kullanmam gerektiğini biliyorum, ancak değerlerin neden farklı olduğunu anlamak istiyorum.

Oluşturulan bayt koduna baktım, ancak bir mağaza ve bir yük dışında işlem kodları aynı.

Aynı kodu java'da da denedim ve 62 ve 62'yi doğru bir şekilde aldım.

Biri bunu açıklayabilir mi?

Düzenleme: Gerçek kodda, doğrudan 6.2f * 10 değil, bir işlev çağrısı * sabittir. Aşağıdaki bayt koduna sahibim:

için 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

için 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

işlenenlerin float olduğunu ve tek farkın stloc/ldloc.

Sanal makineye gelince, aynı sonuçlarla Mono / Win7, Mono / MacOS ve .NET / Windows ile denedim.


9
Tahminimce, işlemlerden biri tek hassasiyette, diğeri ise çift hassasiyetle yapıldı. Bunlardan biri 62'den biraz daha düşük bir değer döndürdü, bu nedenle bir tam sayıya kırpıldığında 61 sonucunu verdi.
Gabe

2
Bunlar tipik Kayan nokta hassasiyeti sorunlarıdır.
TJHeuvel

3
Bunu .Net / WinXP, .Net / Win7, Mono / Ubuntu ve Mono / OSX üzerinde denemek, sonuçlarınızı her iki Windows sürümü için verir, ancak her iki Mono sürümünde speed1 ve speed2 için 62 sonuç verir. Teşekkürler @BoltClock
Eugen Rieck

6
Bay Lippert ... buralarda mısınız?
vc 74

6
Derleyicinin sabit ifade değerlendiricisi burada herhangi bir ödül kazanmıyor. Açıkçası, ilk ifadede 6.2f'yi kesiyor, 2 tabanında tam bir gösterimi yok, bu yüzden 6.199999 olarak bitiyor. Ancak 2. ifadede bunu muhtemelen bir şekilde çift kesinlikte tutmayı başararak yapmaz. Aksi takdirde bu ders için eşittir, kayan nokta tutarlılığı asla bir problem değildir. Bu düzeltilmeyecek, çözümü biliyorsunuz.
Hans Passant

Yanıtlar:


168

Her şeyden önce, bunu bildiğinizi varsayıyorum 6.2f * 10 kayan nokta yuvarlamasından dolayı tam olarak 62 olmadığını (aslında a olarak ifade edildiğinde 61.99999809265137 değeri double) ve sorunuz sadece görünüşte aynı olan iki hesaplamanın neden yanlış değerle sonuçlandığıyla ilgili.

Cevap şu durumda: (int)(6.2f * 10) , sen götürüyorsun doubledeğerini 61,99999809265137 ve 61 verir bir tamsayı, bunu kesiliyor.

Bu durumda float f = 6.2f * 10, çift değeri 61.99999809265137'yi alıp en yakın değere yuvarlarsınız .float 62. Ardından söz konusu kesmek olan floatbir tam sayıya ve sonuç 62'dir.

Alıştırma: Aşağıdaki işlem sırasının sonuçlarını açıklayın.

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

Güncelleme: açıklamalarda belirtildiği gibi, ifade 6.2f * 10resmen olduğu floatikinci parametre beri bir örtülü dönüşüm vardır floathangi daha iyi a'dır double.

Asıl sorun, derleyicinin resmi türden (bölüm 11.2.2) daha yüksek hassasiyete sahip bir ara ürünü kullanmasına izin verilmesidir (ancak zorunlu değildir ) . Bu nedenle farklı sistemlerde farklı davranışlar görüyorsunuz: İfadede (int)(6.2f * 10), derleyicinin değeri tutma seçeneği vardır.6.2f * 10 dönüştürmeden önce yüksek hassasiyetli bir ara formdaint . Varsa, sonuç 61'dir. Aksi takdirde, sonuç 62'dir.

İkinci örnekte, floatyuvarlamayı tam sayıya dönüştürmeden önce gerçekleşmeye zorlayan açık atama .


6
Bunun soruyu gerçekten cevapladığından emin değilim. Neden a olduğunu belirttiği gibi değeri (int)(6.2f * 10)alıyor ? Sanırım asıl nokta (hala cevaplanmamış) burada. doubleffloat
ken2k

1
Bence bunu yapan derleyici, çünkü float literal * int literal olduğundan derleyici en iyi sayısal türü kullanmakta özgür olduğuna karar verdi ve hassasiyetten tasarruf etmek için iki katına (belki) gitti. (aynı zamanda
IL'nin

5
İyi bir nokta. Türü 6.2f * 10aslında floatdeğil double. Derleyicinin, 11.1.6'nın son paragrafında izin verildiği gibi, ara düzeyi optimize ettiğini düşünüyorum .
Raymond Chen

3
Aynı değere sahip (değer 61.99999809265137). Aradaki fark, değerin tam sayı olma yolunda aldığı yoldur. Bir durumda, doğrudan bir tam sayıya gider ve başka bir durumda birfloat önce dönüşümden .
Raymond Chen

38
Raymond'un buradaki cevabı elbette tamamen doğrudur. C # derleyicisinin ve jit derleyicisinin herhangi bir zamanda daha fazla hassasiyet kullanmasına ve bunu tutarsız bir şekilde yapmasına izin verildiğini not ediyorum . Ve aslında, bunu yapıyorlar. Bu soru StackOverflow'da onlarca kez gündeme geldi; Yakın tarihli bir örnek için stackoverflow.com/questions/8795550/… adresine bakın .
Eric Lippert

11

Açıklama

Kayan sayılar nadiren tamdır. 6.2fgibi bir şey 6.1999998.... Bunu bir int'e çevirirseniz, keser ve bu * 10, 61 ile sonuçlanır.

Jon Skeets DoubleConvertersınıfına bakın. Bu sınıfla, bir kayan sayının değerini dize olarak gerçekten görselleştirebilirsiniz. Doublevefloat her ikisi de kayan sayılardır , ondalık değildir (sabit bir noktalı sayıdır).

Örneklem

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

Daha fazla bilgi


5

IL'ye bakın:

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

Derleyici, derleme zamanı sabiti ifadelerini sabit değerlerine indirger ve sabiti dönüştürdüğünde bir noktada yanlış bir yaklaşım yaptığını düşünüyorum int. Durumda speed2, bu dönüşüm derleyici tarafından değil, CLR tarafından yapılır ve farklı kurallar uyguluyor gibi görünüyorlar ...


1

Tahminim şudur ki 6.2f, float hassasiyetli gerçek temsil, 6.1999999while 62fise muhtemelen benzer bir şeydir 62.00000001. (int)çevrim her zaman ondalık değeri keser bu yüzden bu davranışı elde edersiniz.

DÜZENLEME : Yorumlara göre, intçok daha kesin bir tanıma göre döküm davranışını yeniden ifade ettim .


Bir intsayıya çevrim, ondalık değeri keser, yuvarlama yapmaz.
Jim D'Angelo

@James D'Angelo: Üzgünüm ingilizce benim ana dilim değil. Tam kelimeyi bilmediğim için davranışı, temelde aynı davranışı tanımlayan "pozitif sayılarla uğraşırken aşağı yuvarlama" olarak tanımladım. Ama evet, alınan nokta, truncate bunun için kesin bir kelimedir.
Inbetween

sorun değil, bu sadece simantiktir ama birisi düşünmeye başlarsa sorun yaratabilir float-> intyuvarlamayı içerir. = D
Jim D'Angelo

1

Bu kodu derledim ve demonte ettim (Win7 / .NET 4.0 üzerinde). Derleyicinin kayan sabit ifadeyi double olarak değerlendirdiğini tahmin ediyorum.

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 

0

Singlesadece 7 basamaklı bir yer tutar ve bunu bir Int32derleyiciye çevirirken tüm kayan noktalı basamakları keser. Dönüştürme sırasında bir veya daha fazla önemli basamak kaybolabilir.

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

619999980 sonucunu verir, böylece (Int32) (6.2f * 10), 61 verir.

İki Single çarpıldığında farklıdır, bu durumda kesik işlem yoktur, sadece yaklaşık değer vardır.

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


-4

intAyrıştırmak yerine yazı yazmanızın bir nedeni var mı ?

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

sonra okurdu

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

Aradaki fark büyük olasılıkla yuvarlamayla ilgilidir: eğer doubleona çevirirseniz muhtemelen 61.78426 gibi bir şey elde edeceksiniz.

Lütfen aşağıdaki çıktıya dikkat edin

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

Bu yüzden farklı değerler alıyorsunuz!


1
Int.Parseparametre olarak bir dizge alır.
ken2k

Yalnızca dizeleri ayrıştırabilirsiniz, sanırım neden System.Convert'ı kullanmadığınızı kastediyorsunuz
vc 74
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.