2010-06-21 36 views
7

Tôi đã nhận thấy một hành vi thú vị với việc làm tròn/cắt phao bằng trình biên dịch C#. Cụ thể, khi một float float nằm ngoài phạm vi biểu diễn được bảo đảm (7 chữ số thập phân), thì a) đúc một kết quả float cho float (một hoạt động không cần thiết ngữ nghĩa) và b) lưu trữ kết quả tính toán trung gian trong biến cục bộ thay đổi đầu ra. Một ví dụ:Hành vi biên dịch lạ với các ký tự float và các biến float

using System; 

class Program 
{ 
    static void Main() 
    { 
     float f = 2.0499999f; 
     var a = f * 100f; 
     var b = (int) (f * 100f); 
     var c = (int) (float) (f * 100f); 
     var d = (int) a; 
     var e = (int) (float) a; 
     Console.WriteLine(a); 
     Console.WriteLine(b); 
     Console.WriteLine(c); 
     Console.WriteLine(d); 
     Console.WriteLine(e); 
    } 
} 

Đầu ra là:

205 
204 
205 
205 
205 

Trong JITted debug xây dựng trên máy tính của tôi, b được tính như sau:

  var b = (int) (f * 100f); 
0000005a fld   dword ptr [ebp-3Ch] 
0000005d fmul  dword ptr ds:[035E1648h] 
00000063 fstp  qword ptr [ebp-5Ch] 
00000066 movsd  xmm0,mmword ptr [ebp-5Ch] 
0000006b cvttsd2si eax,xmm0 
0000006f mov   dword ptr [ebp-44h],eax 

trong khi d được tính như

  var d = (int) a; 
00000096 fld   dword ptr [ebp-40h] 
00000099 fstp  qword ptr [ebp-5Ch] 
0000009c movsd  xmm0,mmword ptr [ebp-5Ch] 
000000a1 cvttsd2si eax,xmm0 
000000a5 mov   dword ptr [ebp-4Ch],eax 

Cuối cùng, nhiệm vụ của tôi trên: tại sao dòng thứ hai của đầu ra khác với thứ tư? Liệu fmul đó có tạo ra sự khác biệt như vậy không? Cũng lưu ý rằng nếu chữ số cuối cùng (không thể đại diện) từ phao f bị loại bỏ hoặc thậm chí bị giảm, mọi thứ "rơi vào vị trí".

+0

tôi thấy câu trả lời cho câu hỏi này ở đây nhưng không thể tìm thấy nó – Andrey

Trả lời

5

Câu hỏi của bạn có thể được đơn giản hóa để hỏi lý do tại sao hai kết quả này là khác nhau:

float f = 2.0499999f; 
var a = f * 100f; 
var b = (int)(f * 100f); 
var d = (int)a; 
Console.WriteLine(b); 
Console.WriteLine(d); 

Nếu bạn nhìn vào đoạn code trong .NET Reflector bạn có thể thấy rằng các mã trên là thực sự biên soạn như thể đó là mã sau:

float f = 2.05f; 
float a = f * 100f; 
int b = (int) (f * 100f); 
int d = (int) a; 
Console.WriteLine(b); 
Console.WriteLine(d); 

Tính toán điểm nổi không phải lúc nào cũng được thực hiện chính xác. Kết quả của 2.05 * 100f là không chính xác bằng 205, nhưng chỉ là một chút ít do làm tròn lỗi. Khi kết quả trung gian này được chuyển thành một số nguyên bị cắt ngắn. Khi được lưu trữ dưới dạng phao, nó được làm tròn thành dạng biểu thị gần nhất. Hai phương pháp làm tròn này cho kết quả khác nhau.


Về nhận xét của bạn vào câu trả lời của tôi khi bạn viết này:

Console.WriteLine((int) (2.0499999f * 100f)); 
Console.WriteLine((int)(float)(2.0499999f * 100f)); 

Các tính toán được thực hiện hoàn toàn trong trình biên dịch. Mã trên tương đương với điều này:

Console.WriteLine(204); 
Console.WriteLine(205); 
+0

Vì vậy, bạn nói lý do là (int) được thực hiện bằng cách cắt ngắn và (phao) có nghĩa là làm tròn. Nếu đó là trường hợp, thì tại sao đầu ra khác nhau cho Console.WriteLine ((int) (2.0499999f * 100f)) và Console.WriteLine ((int) (float) (2.0499999f * 100f))? – Alan

+0

@Alan, kiểm tra câu trả lời của tôi. lý do là float chỉ có thể chứa 7 chữ số. log (2^23) = 6.9 – Andrey

+0

@Alan: Khi bạn sử dụng hằng số được mã hóa cứng, các phép tính được thực hiện hoàn toàn trong trình biên dịch và sử dụng các quy tắc của trình biên dịch, chứ không phải trong thời gian chạy .NET. –

2

Đánh dấu là đúng về trình biên dịch. Bây giờ hãy đánh lừa trình biên dịch:

float f = (Math.Sin(0.5) < 5) ? 2.0499999f : -1; 
    var a = f * 100f; 
    var b = (int) (f * 100f); 
    var c = (int) (float) (f * 100f); 
    var d = (int) a; 
    var e = (int) (float) a; 
    Console.WriteLine(a); 
    Console.WriteLine(b); 
    Console.WriteLine(c); 
    Console.WriteLine(d); 
    Console.WriteLine(e); 

biểu thức đầu tiên là vô nghĩa nhưng ngăn trình biên dịch tối ưu hóa. Kết quả là:

205 
204 
205 
204 
205 

ok, tôi đã tìm thấy giải thích.

2.0499999f không thể được lưu trữ dưới dạng float, bởi vì nó chỉ có thể chứa 7 chữ số 10 dựa trên. và chữ này là 8 chữ số, do đó trình biên dịch làm tròn nó vì không thể lưu trữ. (nên cảnh báo IMO)

nếu bạn thay đổi thành 2.049999f kết quả sẽ được mong đợi.

+0

Cảm ơn Andrey, tôi đã chọn trả lời của Mark trên cơ sở trình biên dịch so với thông tin thời gian chạy, nhưng bạn cũng có liên quan. – Alan

4

Trong một bình luận bạn hỏi

Là những quy tắc khác nhau?

Có. Hay đúng hơn, các quy tắc cho phép hành vi khác nhau.

Và nếu có, tôi phải biết điều này, hoặc từ C# tham khảo ngôn ngữ doc hoặc MSDN, hay là này chỉ là một sự khác biệt thỉnh thoảng giữa biên dịch và thời gian chạy

Nó ngụ ý bởi đặc điểm kỹ thuật. Các hoạt động điểm nổi có mức độ chính xác tối thiểu nhất định phải được đáp ứng, nhưng trình biên dịch hoặc thời gian chạy được phép sử dụng chính xác hơn nếu nó phù hợp. Điều đó có thể gây ra những thay đổi lớn, có thể quan sát được khi bạn thực hiện các thao tác để phóng to những thay đổi nhỏ. Làm tròn, ví dụ, có thể biến một thay đổi cực nhỏ thành một cái rất lớn.

Thực tế này dẫn đến các câu hỏi thường gặp ở đây. Đối với một số nền tảng về tình huống này và tình huống khác có thể tạo ra sự khác biệt tương tự, thấy như sau:

Why does this floating-point calculation give different results on different machines?

C# XNA Visual Studio: Difference between "release" and "debug" modes?

CLR JIT optimizations violates causality?

https://stackoverflow.com/questions/2494724

+1

Eric, cảm ơn rất nhiều. Liên kết cuối cùng của bạn đặc biệt là khai sáng. Tôi thực sự đã tìm kiếm các kịch bản tương tự trước khi đăng câu hỏi, nhưng dường như phạm vi của tôi quá hẹp. – Alan

+0

@Alan: Bạn được chào đón! –