การแสดงออกของ C # ลอย: พฤติกรรมแปลก ๆ เมื่อส่งผลลอยไปยัง int


128

ฉันมีรหัสง่ายๆดังต่อไปนี้:

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

speed1และspeed2ควรมีค่าเท่ากัน แต่อันที่จริงฉันมี:

speed1 = 61
speed2 = 62

ฉันรู้ว่าฉันควรใช้ Math.Round แทนการคัดเลือกนักแสดง แต่ฉันต้องการที่จะเข้าใจว่าทำไมค่าต่างกัน

ฉันดูโค้ดไบต์ที่สร้างขึ้น แต่ยกเว้นร้านค้าและโหลด opcodes เหมือนกัน

ฉันยังลองรหัสเดียวกันใน java และฉันได้รับ 62 และ 62 อย่างถูกต้อง

มีคนอธิบายเรื่องนี้ได้ไหม

แก้ไข: ในรหัสจริงมันไม่ได้โดยตรง 6.2f * 10 แต่ฟังก์ชั่นการโทร * ค่าคงที่ ฉันมีรหัสไบต์ต่อไปนี้:

สำหรับ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

สำหรับ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

stloc/ldlocเราสามารถมองเห็นตัวถูกดำเนินการที่มีลอยและที่แตกต่างเพียงอย่างเดียวคือ

สำหรับเครื่องเสมือนฉันลองกับ Mono / Win7, Mono / MacOS และ. NET / Windows ด้วยผลลัพธ์เดียวกัน


9
ฉันเดาว่าการดำเนินการอย่างใดอย่างหนึ่งดำเนินการด้วยความแม่นยำเดียวในขณะที่อีกการดำเนินการดำเนินการด้วยความแม่นยำสองเท่า หนึ่งในนั้นส่งคืนค่าน้อยกว่า 62 เล็กน้อยดังนั้นจึงให้ผล 61 เมื่อตัดให้เป็นจำนวนเต็ม
Gabe

2
เหล่านี้เป็นปัญหาความแม่นยำจุดลอยทั่วไป
TJHeuvel

3
ลองใช้วิธีนี้ใน. Net / WinXP, .Net / Win7, Mono / Ubuntu และ Mono / OSX ให้ผลลัพธ์ของคุณสำหรับทั้งรุ่น Windows แต่ 62 สำหรับ speed1 และ speed2 ใน Mono ทั้งสองรุ่น ขอบคุณ @BoltClock
Eugen Rieck

6
นาย Lippert ... คุณอยู่แถวนี้ ??
74 74

6
ผู้ประเมินค่าคงที่ของคอมไพเลอร์ไม่ได้รับรางวัลใด ๆ ที่นี่ เห็นได้ชัดว่ามันถูกตัดทอน 6.2f ในนิพจน์แรก แต่ก็ไม่มีตัวแทนที่แน่นอนในฐานที่ 2 ดังนั้นจึงกลายเป็น 6.199999 แต่ไม่ได้ทำในการแสดงออกที่ 2 อาจโดยการจัดการเพื่อให้มันมีความแม่นยำสองครั้งอย่างใด สิ่งนี้ถือว่าเป็นเรื่องที่แน่นอนสำหรับหลักสูตรความสอดคล้องของจุดลอยตัวนั้นไม่เคยมีปัญหาเลย สิ่งนี้จะไม่ได้รับการแก้ไขคุณรู้วิธีแก้ปัญหา
Hans Passant

คำตอบ:


168

ก่อนอื่นฉันคิดว่าคุณรู้ว่า6.2f * 10ไม่ใช่ 62 อย่างแน่นอนเนื่องจากการปัดเศษทศนิยม (จริง ๆ แล้วมีค่า 61.99999809265137 เมื่อแสดงเป็น a double) และคำถามของคุณเป็นเพียงว่าทำไมการคำนวณเหมือนกันสองครั้งจึงทำให้ค่าผิด

คำตอบคือในกรณีของ(int)(6.2f * 10)คุณจะใช้doubleค่า 61.99999809265137 และตัดให้เป็นจำนวนเต็มซึ่งให้ผล 61

ในกรณีของfloat f = 6.2f * 10คุณจะใช้ค่าสองครั้ง 61.99999809265137 และปัดเศษเป็นค่าที่ใกล้ที่สุดfloatซึ่งก็คือ 62 จากนั้นคุณตัดทอนfloatให้เป็นจำนวนเต็มและผลลัพธ์คือ 62

แบบฝึกหัด: อธิบายผลลัพธ์ของลำดับการปฏิบัติการต่อไปนี้

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

ปรับปรุง: ตามที่ระบุไว้ในความคิดเห็นที่แสดงออก6.2f * 10เป็นอย่างเป็นทางการfloatตั้งแต่พารามิเตอร์ที่สองมีการแปลงส่อไปfloatซึ่งเป็นที่ดีขึ้นdoubleกว่าการแปลงนัยไป

ปัญหาที่เกิดขึ้นจริงคือว่าคอมไพเลอร์จะได้รับอนุญาต ( แต่ไม่จำเป็น) เพื่อใช้เป็นสื่อกลางซึ่งเป็นที่มีความแม่นยำสูงกว่าประเภทอย่างเป็นทางการ (มาตรา 11.2.2) นั่นเป็นเหตุผลที่คุณเห็นพฤติกรรมที่แตกต่างกันในระบบที่แตกต่างกันในการแสดงออก(int)(6.2f * 10)คอมไพเลอร์มีตัวเลือกในการรักษาค่าในรูปแบบที่เป็นสื่อกลางความแม่นยำสูงก่อนการแปลง6.2f * 10 intหากเป็นเช่นนั้นผลลัพธ์จะเป็น 61 หากไม่เป็นเช่นนั้นผลที่ได้คือ 62

ในตัวอย่างที่สองการมอบหมายอย่างชัดเจนเพื่อfloatบังคับให้การปัดเศษเกิดขึ้นก่อนการแปลงเป็นจำนวนเต็ม


6
ฉันไม่แน่ใจว่าสิ่งนี้ตอบคำถามได้จริง ทำไม(int)(6.2f * 10)การรับdoubleค่าตามที่fระบุไว้floatคือ ฉันคิดว่าประเด็นหลัก (ยังไม่ได้ตอบ) อยู่ที่นี่
ken2k

1
ฉันคิดว่ามันเป็นคอมไพเลอร์ที่ทำเช่นนี้เพราะมันลอยตามตัวอักษร * ตามตัวอักษรคอมไพเลอร์ได้ตัดสินใจว่าจะใช้ประเภทตัวเลขที่ดีที่สุดได้ฟรีและเพื่อประหยัดความแม่นยำ (จะอธิบาย IL เหมือนกัน)
George Duckett

5
จุดดี. ประเภทของการ6.2f * 10เป็นจริงไม่ได้float doubleผมคิดว่าคอมไพเลอร์คือการเพิ่มประสิทธิภาพสื่อกลางในขณะที่ได้รับอนุญาตตามวรรคสุดท้ายของ11.1.6
Raymond Chen

3
มันมีค่าเหมือนกัน (ค่าคือ 61.99999809265137) ความแตกต่างคือเส้นทางที่ค่าใช้ในการเป็นจำนวนเต็ม ในกรณีหนึ่งมันจะไปที่จำนวนเต็มโดยตรงและในอีกกรณีจะผ่านการfloatแปลงก่อน
Raymond Chen

38
คำตอบของ Raymond ที่นี่แน่นอนว่าถูกต้องสมบูรณ์ ผมทราบว่าคอมไพเลอร์ C # และคอมไพเลอร์ JIT จะถูกทั้งสองได้รับอนุญาตให้ใช้ความแม่นยำมากขึ้นในเวลาใด ๆและจะทำเช่นนั้นไม่ลงรอยกัน และในความเป็นจริงพวกเขาทำอย่างนั้น คำถามนี้เกิดขึ้นหลายสิบครั้งใน StackOverflow ดูstackoverflow.com/questions/8795550/…สำหรับตัวอย่างล่าสุด
Eric Lippert

11

ลักษณะ

ตัวเลขลอยตัวไม่ค่อยแน่นอน เป็นสิ่งที่ต้องการ6.2f 6.1999998...หากคุณส่งไปยัง int มันจะตัดทอนมันและ * 10 ผลลัพธ์ใน 61

ตรวจสอบDoubleConverterคลาสJon Skeets ด้วยคลาสนี้คุณสามารถเห็นภาพค่าตัวเลขลอยตัวเป็นสตริงได้ Doubleและfloatเป็นทั้งตัวเลขลอยตัวทศนิยมไม่ใช่ (มันเป็นหมายเลขจุดคงที่)

ตัวอย่าง

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

ข้อมูลมากกว่านี้


5

ดู 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

คอมไพเลอร์ช่วยลดการแสดงออกคงที่คอมไพล์เวลาให้เป็นค่าคงที่ของพวกเขาและฉันคิดว่ามันทำให้การประมาณที่ไม่ถูกต้องในบางจุดเมื่อมันแปลงค่าคงที่intเป็น ในกรณีของspeed2การแปลงนี้ไม่ได้ทำโดยคอมไพเลอร์ แต่โดย CLR และพวกเขาดูเหมือนจะใช้กฎที่แตกต่าง ...


1

ฉันเดาว่า6.2fเป็นตัวแทนที่แท้จริงที่มีความแม่นยำลอยคือ6.1999999ในขณะที่อาจจะเป็นสิ่งที่คล้ายกับ62f การคัดเลือกจะตัดทอนค่าทศนิยมเสมอเพื่อให้คุณได้รับพฤติกรรมนั้น62.00000001(int)

แก้ไข : ตามความคิดเห็นที่ฉันได้ rephrased พฤติกรรมของintการคัดเลือกที่มีความแม่นยำมากขึ้น


การชี้ไปที่การintตัดทอนค่าทศนิยมจะไม่ปัดเศษ
Jim D'Angelo

@James D'Angelo: ขออภัยภาษาอังกฤษไม่ใช่ภาษาหลักของฉัน ไม่ทราบคำที่แน่นอนดังนั้นฉันจึงกำหนดพฤติกรรมเป็น "ปัดเศษ downards เมื่อจัดการกับจำนวนบวก" ซึ่งโดยทั่วไปอธิบายพฤติกรรมเดียวกัน แต่ใช่จุดถูกตัดทอนเป็นคำที่แน่นอนสำหรับมัน
ระหว่าง

ไม่มีปัญหามันเป็นเพียงความเห็นใจ แต่อาจทำให้เกิดปัญหาได้ถ้าใครบางคนเริ่มคิดfloat-> intเกี่ยวข้องกับการปัดเศษ = D
Jim D'Angelo

1

ฉันรวบรวมและถอดรหัสนี้ (บน Win7 / .NET 4.0) ฉันเดาว่าคอมไพเลอร์ประเมินนิพจน์คงที่แบบลอยตัวเป็นสองเท่า

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

Singlemantains เพียง 7 หลักและเมื่อส่งไปInt32ยังคอมไพเลอร์จะตัดทอนตัวเลขทศนิยมทั้งหมด ในระหว่างการแปลงหนึ่งหลักที่สำคัญอาจสูญหายได้

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

ให้ผลลัพธ์ของ 619999980 ดังนั้น (Int32) (6.2f * 10) ให้ 61

มันแตกต่างกันเมื่อสอง Single ถูกคูณในกรณีนั้นไม่มีการดำเนินการที่ถูกตัดทอน แต่ประมาณเท่านั้น

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


-4

มีเหตุผลที่คุณพิมพ์แบบหล่อintแทนการแยกวิเคราะห์หรือไม่?

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

ก็จะอ่าน

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

ความแตกต่างน่าจะเกี่ยวกับการปัดเศษ: ถ้าคุณโยนไป doubleคุณอาจจะได้อะไรแบบ 61.78426

โปรดทราบผลลัพธ์ต่อไปนี้

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

นั่นคือเหตุผลที่คุณได้รับคุณค่าที่แตกต่าง!


1
Int.Parseใช้สตริงเป็นพารามิเตอร์
ken2k

คุณสามารถแยกสตริงได้เท่านั้นฉันคิดว่าคุณหมายความว่าทำไมคุณไม่ใช้ System.Convert
vc 74
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.