2010-02-08 30 views
14

Tôi đã viết một ví dụ hướng dẫn cho một đồng nghiệp để cho anh ta thấy lý do tại sao thử nghiệm phao cho bình đẳng thường là một ý tưởng tồi. Ví dụ tôi đã đi với đã được thêm 0,1 lần mười, và so sánh với 1.0 (một trong những tôi đã được hiển thị trong lớp số giới thiệu của tôi). Tôi đã rất ngạc nhiên khi thấy rằng hai kết quả bằng (code + output).CLR JIT tối ưu hóa vi phạm nhân quả?

float @float = 0.0f; 
for(int @int = 0; @int < 10; @int += 1) 
{ 
    @float += 0.1f; 
} 
Console.WriteLine(@float == 1.0f); 

Một số điều tra cho thấy kết quả này không thể dựa vào (giống như bình đẳng nổi). Điều tôi ngạc nhiên nhất là việc thêm mã sau mã khác có thể thay đổi kết quả của phép tính (code + output). Lưu ý rằng ví dụ này có cùng mã và IL, với thêm một dòng C# được nối thêm.

float @float = 0.0f; 
for(int @int = 0; @int < 10; @int += 1) 
{ 
    @float += 0.1f; 
} 
Console.WriteLine(@float == 1.0f); 
Console.WriteLine(@float.ToString("G9")); 

Tôi biết tôi không được sử dụng bình đẳng trên phao và do đó không nên quan tâm quá nhiều về điều này, nhưng tôi thấy nó khá ngạc nhiên, như mọi người đã thể hiện điều này. Làm công cụ sau bạn đã thực hiện một phép tính thay đổi giá trị của phép tính trước? Tôi không nghĩ đó là mô hình của những người tính toán thường có trong đầu họ.

Tôi không hoàn toàn bối rối, có vẻ như an toàn để cho rằng có một số loại tối ưu xảy ra trong trường hợp "bằng" thay đổi kết quả tính toán (xây dựng trong chế độ gỡ lỗi ngăn trường hợp "bằng"). Rõ ràng, tối ưu hóa bị bỏ qua khi CLR phát hiện ra rằng sau này nó sẽ cần hộp nổi.

Tôi đã tìm kiếm một chút nhưng không thể tìm thấy lý do cho hành vi này. Ai có thể dẫn tôi vào?

Trả lời

18

Đây là một tác dụng phụ của cách trình tối ưu hóa JIT hoạt động. Nó làm việc nhiều hơn nếu có ít mã để tạo ra. Vòng lặp trong đoạn ban đầu của bạn được biên dịch như sau:

   @float += 0.1f; 
0000000f fld   dword ptr ds:[0025156Ch]   ; push(intermediate), st0 = 0.1 
00000015 faddp  st(1),st       ; st0 = st0 + st1 
      for (int @int = 0; @int < 10; @int += 1) { 
00000017 inc   eax 
00000018 cmp   eax,0Ah 
0000001b jl   0000000F 

Khi bạn thêm Console.WriteLine() tuyên bố thêm, nó biên dịch nó như thế này:

   @float += 0.1f; 
00000011 fld   dword ptr ds:[00961594h]   ; st0 = 0.1 
00000017 fadd  dword ptr [ebp-8]     ; st0 = st0 + @float 
0000001a fstp  dword ptr [ebp-8]     ; @float = st0 
      for (int @int = 0; @int < 10; @int += 1) { 
0000001d inc   eax 
0000001e cmp   eax,0Ah 
00000021 jl   00000011 

Lưu ý sự khác biệt tại địa chỉ 15 vs địa chỉ 17 + 1a, vòng lặp đầu tiên giữ kết quả trung gian trong FPU. Vòng lặp thứ hai lưu nó trở lại biến địa phương @float. Trong khi nó nằm trong FPU, kết quả được tính toán với độ chính xác đầy đủ. Lưu trữ nó trở lại tuy nhiên cắt ngắn kết quả trung gian trở lại một phao, mất rất nhiều bit chính xác trong quá trình này.

Trong khi khó chịu, tôi không tin đây là lỗi. Trình biên dịch JIT x64 hoạt động khác nhau. Bạn có thể tạo trường hợp của mình tại connect.microsoft.com

+0

Câu trả lời hay. Tôi cũng sẽ không gọi nó là lỗi. Thêm một hiệu ứng phụ bất thường (và chắc chắn không ngờ). – Gobiner

4

Bạn có chạy bộ xử lý này trên bộ xử lý Intel không?

Một giả thuyết là JIT cho phép @float được tích lũy hoàn toàn trong thanh ghi điểm động, độ chính xác 80 bit đầy đủ. Bằng cách này, tính toán có thể đủ chính xác.

Phiên bản thứ hai của mã không phù hợp với sổ đăng ký và do đó @float phải được "tràn" vào bộ nhớ, làm cho giá trị bit 80 được làm tròn xuống một độ chính xác duy nhất. .

Nhưng đó chỉ là một dự đoán rất ngẫu nhiên. Người ta sẽ phải kiểm tra mã máy thực tế được tạo ra bởi trình biên dịch JIT (gỡ lỗi với khung nhìn tháo gỡ mở).

Edit:

Hm ... Tôi đã thử nghiệm mã của bạn tại địa phương (Intel Core 2, Windows 7 x64, 64-bit CLR) và tôi luôn luôn có những "dự kiến" làm tròn lỗi. Cả trong cấu hình phát hành và gỡ lỗi.

Sau đây là Visual Studio màn tháo gỡ cho đoạn mã đầu tiên trên máy tính của tôi:

xorps  xmm0,xmm0 
movss  dword ptr [rsp+20h],xmm0 
     for (int @int = 0; @int < 10; @int += 1) 
mov   dword ptr [rsp+24h],0 
jmp   0000000000000061 
     { 
      @float += 0.1f; 
movss  xmm0,dword ptr [000000A0h] 
addss  xmm0,dword ptr [rsp+20h] 
movss  dword ptr [rsp+20h],xmm0 // <-- @float gets stored in memory 
     for (int @int = 0; @int < 10; @int += 1) 
mov   eax,dword ptr [rsp+24h] 
add   eax,1 
mov   dword ptr [rsp+24h],eax 
cmp   dword ptr [rsp+24h],0Ah 
jl   0000000000000042 
     } 
     Console.WriteLine(@float == 1.0f); 
etc. 

khác biệt giữa x64 và x86 trình biên dịch JIT, nhưng tôi không có quyền truy cập vào một máy 32 bit.

+0

Tôi chắc chắn * đã * chạy nó trên một bộ xử lý Intel. Tôi không biết liệu dotnetpad có chạy trên bộ xử lý Intel hay không. – Gobiner

2

Lý thuyết của tôi mà không có dòng ToString, trình biên dịch có thể tối ưu hóa tĩnh chức năng xuống thành một giá trị duy nhất và bằng cách nào đó nó bù đắp cho lỗi dấu phẩy động.Nhưng khi dòng ToString được thêm vào, trình tối ưu hóa phải xử lý phao khác nhau bởi vì nó được yêu cầu bởi lời gọi phương thức. Đó chỉ là một dự đoán.