Biểu thức float C #: hành vi lạ khi truyền kết quả float vào int


128

Tôi có mã đơn giản sau:

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

speed1speed2nên có cùng giá trị, nhưng thực tế, tôi có:

speed1 = 61
speed2 = 62

Tôi biết có lẽ tôi nên sử dụng Math.Round thay vì truyền, nhưng tôi muốn hiểu tại sao các giá trị lại khác nhau.

Tôi đã xem mã byte được tạo, nhưng ngoại trừ một cửa hàng và tải, các opcode đều giống nhau.

Tôi cũng đã thử mã tương tự trong java và tôi đã nhận được chính xác 62 và 62.

Ai đó có thể giải thích điều này?

Chỉnh sửa: Trong mã thực, nó không trực tiếp 6.2f * 10 mà là một hàm gọi * một hằng số. Tôi có mã byte sau:

cho 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

cho 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

chúng ta có thể thấy rằng toán hạng là số float và sự khác biệt duy nhất là stloc/ldloc.

Đối với máy ảo, tôi đã thử với Mono / Win7, Mono / MacOS và .NET / Windows, với kết quả tương tự.


9
Tôi đoán là một trong các thao tác được thực hiện với độ chính xác đơn trong khi các thao tác khác được thực hiện với độ chính xác kép. Một trong số chúng trả về một giá trị nhỏ hơn 62, do đó thu được 61 khi cắt thành một số nguyên.
Gabe

2
Đây là những vấn đề chính xác điểm nổi điển hình.
TJHeuvel

3
Thử điều này trên .Net / WinXP, .Net / Win7, Mono / Ubuntu và Mono / OSX cho kết quả của bạn cho cả hai phiên bản Windows, nhưng 62 cho speed1 và speed2 trong cả hai phiên bản Mono. Cảm ơn @BoltClock
Eugen Rieck

6
Ông Lippert ... bạn xung quanh ??
vc 74

6
Người đánh giá biểu thức liên tục của nhà soạn nhạc không giành được bất kỳ giải thưởng nào ở đây. Rõ ràng nó đang cắt ngắn 6.2f trong biểu thức đầu tiên, nó không có biểu diễn chính xác trong cơ sở 2 nên kết thúc là 6.199999. Nhưng không làm như vậy trong biểu thức thứ 2, có lẽ bằng cách quản lý để giữ nó ở độ chính xác gấp đôi bằng cách nào đó. Đây là mệnh giá khác cho khóa học, tính nhất quán của dấu phẩy động không bao giờ là vấn đề. Điều này sẽ không được sửa chữa, bạn biết cách giải quyết.
Hans Passant

Câu trả lời:


168

Trước hết, tôi giả sử rằng bạn biết rằng đó 6.2f * 10không chính xác là 62 do làm tròn điểm trôi nổi (thực ra đó là giá trị 61.99999809265137 khi được biểu thị bằng a double) và câu hỏi của bạn chỉ là tại sao hai phép tính dường như giống nhau dẫn đến giá trị sai.

Câu trả lời là trong trường hợp (int)(6.2f * 10), bạn đang lấy doublegiá trị 61.99999809265137 và cắt nó thành một số nguyên, mang lại 61.

Trong trường hợp float f = 6.2f * 10, bạn đang lấy giá trị kép 61.99999809265137 và làm tròn đến giá trị gần nhất float, là 62. Sau đó, bạn cắt nó floatthành một số nguyên và kết quả là 62.

Bài tập: Giải thích kết quả của chuỗi thao tác sau.

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

Cập nhật: Như đã đề cập trong các ý kiến, khái niệm 6.2f * 10là chính thức một floattừ tham số thứ hai có một chuyển đổi ngầm để floatđó là tốt hơn so với chuyển đổi ngầm để double.

Vấn đề thực tế là trình biên dịch được phép (nhưng không bắt buộc) sử dụng một trung gian có độ chính xác cao hơn loại chính thức (phần 11.2.2) . Đó là lý do tại sao bạn thấy các hành vi khác nhau trên các hệ thống khác nhau: Trong biểu thức (int)(6.2f * 10), trình biên dịch có tùy chọn giữ giá trị 6.2f * 10ở dạng trung gian có độ chính xác cao trước khi chuyển đổi sang int. Nếu có, thì kết quả là 61. Nếu không, thì kết quả là 62.

Trong ví dụ thứ hai, việc gán rõ ràng để floatbuộc làm tròn diễn ra trước khi chuyển đổi thành số nguyên.


6
Tôi không chắc điều này thực sự trả lời câu hỏi. Tại sao lại là(int)(6.2f * 10) lấy doublegiá trị, như fchỉ định nó là một float? Tôi nghĩ rằng điểm chính (vẫn chưa được trả lời) là ở đây.
ken2k

1
Tôi nghĩ rằng đó là trình biên dịch đang làm điều đó, vì nó trôi nổi theo nghĩa đen * int nghĩa là trình biên dịch đã quyết định sử dụng loại số tốt nhất và để tiết kiệm độ chính xác, nó có thể tăng gấp đôi (có thể). (cũng sẽ giải thích IL giống nhau)
George Duckett

5
Điểm tốt. Các loại 6.2f * 10là thực sự float, không double. Tôi nghĩ rằng trình biên dịch đang tối ưu hóa trung gian, như được cho phép bởi đoạn cuối của 11.1.6 .
Raymond Chen

3
Nó có cùng giá trị (giá trị là 61.99999809265137). Sự khác biệt là đường dẫn mà giá trị thực hiện trên con đường trở thành số nguyên. Trong một trường hợp, nó đi trực tiếp đến một số nguyên và trong trường hợp khác, nó đi qua một floatchuyển đổi đầu tiên.
Raymond Chen

38
Câu trả lời của Raymond ở đây tất nhiên là hoàn toàn chính xác. Tôi lưu ý rằng trình biên dịch C # và trình biên dịch jit đều được phép sử dụng độ chính xác cao hơn bất cứ lúc nào và làm như vậy không nhất quán . Và trên thực tế, họ làm điều đó. Câu hỏi này đã được đưa ra hàng chục lần trên StackOverflow; xem stackoverflow.com/questions/8795550/v để biết ví dụ gần đây.
Eric Lippert

11

Sự miêu tả

Số nổi một cách hiếm khi chính xác. 6.2flà một cái gì đó như 6.1999998.... Nếu bạn chuyển nó thành int, nó sẽ cắt nó và kết quả * 10 này là 61.

Kiểm tra DoubleConverterlớp Jon Skeets . Với lớp này, bạn thực sự có thể hình dung giá trị của một số trôi nổi dưới dạng chuỗi. Doublefloatlà cả hai số trôi nổi , số thập phân thì không (nó là số điểm cố định).

Mẫu vật

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

Thêm thông tin


5

Nhìn vào 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

Trình biên dịch làm giảm các biểu thức hằng số thời gian biên dịch thành giá trị không đổi của chúng và tôi nghĩ rằng nó làm cho một xấp xỉ sai ở một số điểm khi nó chuyển đổi hằng số thành int. Trong trường hợp speed2, chuyển đổi này được thực hiện không phải bởi trình biên dịch, mà bởi CLR và dường như chúng áp dụng các quy tắc khác nhau ...


1

Tôi đoán là 6.2fđại diện thực sự với độ chính xác nổi là 6.1999999trong khi 62fcó lẽ là một cái gì đó tương tự 62.00000001. (int)đúc luôn cắt ngắn giá trị thập phân để đó là lý do tại sao bạn có hành vi đó.

EDIT : Theo ý kiến ​​tôi đã đánh giá lại hành vi của intviệc chuyển sang một định nghĩa chính xác hơn nhiều.


Đúc tới một intgiá trị thập phân, nó không tròn.
Jim D'Angelo

@James D'Angelo: Xin lỗi tiếng Anh không phải là ngôn ngữ chính của tôi. Không biết từ chính xác nên tôi đã định nghĩa hành vi là "làm tròn số xuống khi xử lý số dương", về cơ bản mô tả hành vi tương tự. Nhưng vâng, điểm lấy, cắt ngắn là từ chính xác cho nó.
Trong khoảng giữa

không có vấn đề gì, nó chỉ là giao cảm nhưng có thể gây rắc rối nếu ai đó bắt đầu suy nghĩ float-> intliên quan đến làm tròn. = D
Jim D'Angelo

1

Tôi đã biên dịch và phân tách mã này (trên Win7 / .NET 4.0). Tôi đoán rằng trình biên dịch đánh giá biểu thức hằng nổi là gấp đôi.

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

Singlechỉ có 7 chữ số và khi truyền nó tới Int32trình biên dịch sẽ cắt tất cả các chữ số dấu phẩy động. Trong quá trình chuyển đổi, một hoặc nhiều chữ số có nghĩa có thể bị mất.

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

cho kết quả 619999980 vì vậy (Int32) (6.2f * 10) cho 61.

Nó khác nhau khi hai Đơn được nhân lên, trong trường hợp đó không có hoạt động cắt ngắn mà chỉ là xấp xỉ.

Xem http://msdn.microsoft.com/en-us/l Library / system.single.aspx


-4

Có một lý do mà bạn đang nhập vào intthay vì phân tích cú pháp?

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

sau đó sẽ đọc

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

Sự khác biệt có lẽ là để làm tròn: nếu bạn chọn double bạn có thể sẽ nhận được một cái gì đó như 61.78426.

Xin lưu ý đầu ra sau đây

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

Đó là lý do tại sao bạn đang nhận được các giá trị khác nhau!


1
Int.Parselấy một chuỗi làm tham số.
ken2k

Bạn chỉ có thể phân tích các chuỗi, tôi đoán bạn có nghĩa là tại sao bạn không sử dụng System.Convert
vc 74
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.