Tại sao hành vi mã lại khác nhau trong chế độ phát hành và gỡ lỗi?


84

Hãy xem xét đoạn mã sau:

private static void Main(string[] args)
{
    var ar = new double[]
    {
        100
    };

    FillTo(ref ar, 5);
    Console.WriteLine(string.Join(",", ar.Select(a => a.ToString()).ToArray()));
}

public static void FillTo(ref double[] dd, int N)
{
    if (dd.Length >= N)
        return;

    double[] Old = dd;
    double d = double.NaN;
    if (Old.Length > 0)
        d = Old[0];

    dd = new double[N];

    for (int i = 0; i < Old.Length; i++)
    {
        dd[N - Old.Length + i] = Old[i];
    }
    for (int i = 0; i < N - Old.Length; i++)
        dd[i] = d;
}

Kết quả ở chế độ Gỡ lỗi là: 100,100,100,100,100. Nhưng ở chế độ Phát hành thì nó là: 100,100,100,100,0.

Điều gì đang xảy ra?

Nó đã được thử nghiệm bằng cách sử dụng .NET framework 4.7.1 và .NET Core 2.0.0.


Bạn sử dụng phiên bản Visual Studio (hoặc trình biên dịch) nào?
Styxxy

9
Repro; thêm một Console.WriteLine(i);vào vòng lặp cuối cùng ( dd[i] = d;) sẽ "sửa" nó, điều này cho thấy lỗi trình biên dịch hoặc lỗi JIT; nhìn vào IL ...
Marc Gravell

@Styxxy, được thử nghiệm vào năm 2015, 2017 và nhắm mục tiêu mọi khung .net> = 4.5
Ashkan Nourzadeh

Chắc chắn là một lỗi. Nó cũng biến mất nếu bạn xóa if (dd.Length >= N) return;, có thể là một bản repro đơn giản hơn.
Jeroen Mostert

1
Không có gì ngạc nhiên khi so sánh là táo-to-táo, codegen x64 cho .Net Framework và .Net Core có hiệu suất tương tự, vì (theo mặc định) về cơ bản nó là cùng một mã tạo jit. Sẽ rất thú vị nếu so sánh hiệu suất của .Net Framework x86 codegen với codegen x86 của .Net Core (đang sử dụng RyuJit kể từ 2.0). Vẫn có những trường hợp mà jit cũ hơn (hay còn gọi là Jit32) biết một vài thủ thuật mà RyuJit thì không. Và nếu bạn tìm thấy bất kỳ trường hợp nào như vậy, hãy đảm bảo mở các vấn đề cho chúng trên repo CoreCLR.
Andy Ayers

Câu trả lời:


70

Đây dường như là một lỗi JIT; Tôi đã thử nghiệm với:

// ... existing code unchanged
for (int i = 0; i < N - Old.Length; i++)
{
    // Console.WriteLine(i); // <== comment/uncomment this line
    dd[i] = d;
}

và thêm các Console.WriteLine(i)bản sửa lỗi. Thay đổi IL duy nhất là:

// ...
L_0040: ldc.i4.0 
L_0041: stloc.3 
L_0042: br.s L_004d
L_0044: ldarg.0 
L_0045: ldind.ref 
L_0046: ldloc.3 
L_0047: ldloc.1 
L_0048: stelem.r8 
L_0049: ldloc.3 
L_004a: ldc.i4.1 
L_004b: add 
L_004c: stloc.3 
L_004d: ldloc.3 
L_004e: ldarg.1 
L_004f: ldloc.0 
L_0050: ldlen 
L_0051: conv.i4 
L_0052: sub 
L_0053: blt.s L_0044
L_0055: ret 

vs

// ...
L_0040: ldc.i4.0 
L_0041: stloc.3 
L_0042: br.s L_0053
L_0044: ldloc.3 
L_0045: call void [System.Console]System.Console::WriteLine(int32)
L_004a: ldarg.0 
L_004b: ldind.ref 
L_004c: ldloc.3 
L_004d: ldloc.1 
L_004e: stelem.r8 
L_004f: ldloc.3 
L_0050: ldc.i4.1 
L_0051: add 
L_0052: stloc.3 
L_0053: ldloc.3 
L_0054: ldarg.1 
L_0055: ldloc.0 
L_0056: ldlen 
L_0057: conv.i4 
L_0058: sub 
L_0059: blt.s L_0044
L_005b: ret 

mà có vẻ chính xác (sự khác biệt duy nhất là bổ sung ldloc.3call void [System.Console]System.Console::WriteLine(int32)và một mục tiêu khác nhưng tương đương cho br.s).

Tôi nghi ngờ là nó sẽ cần một bản sửa lỗi JIT.

Môi trường:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Xem trước 5.0
  • dotnet --version: 2.1.1

Sau đó, nơi để báo cáo lỗi?
Ashkan Nourzadeh

1
Tôi cũng thấy nó trên .NET full 4.7.1, vì vậy nếu đây không phải là lỗi RyuJIT, tôi sẽ ngả mũ.
Jeroen Mostert

2
Tôi không thể tái tạo, đã cài đặt .NET 4.7.1 và có thể tạo lại ngay bây giờ.
dùng3057557

3
@MarcGravell .Net framework 4.7.1 và .net Core 2.0.0
Ashkan Nourzadeh

4
@AshkanNourzadeh Tôi có lẽ sẽ ghi lại nó ở đây một cách trung thực, nhấn mạnh rằng mọi người tin rằng đó là một lỗi RyuJIT
Marc Gravell

6

Đó thực sự là một lỗi lắp ráp. x64, .net 4.7.1, phát hành bản dựng.

tháo rời:

            for(int i = 0; i < N - Old.Length; i++)
00007FF942690ADD  xor         eax,eax  
            for(int i = 0; i < N - Old.Length; i++)
00007FF942690ADF  mov         ebx,esi  
00007FF942690AE1  sub         ebx,ebp  
00007FF942690AE3  test        ebx,ebx  
00007FF942690AE5  jle         00007FF942690AFF  
                dd[i] = d;
00007FF942690AE7  mov         rdx,qword ptr [rdi]  
00007FF942690AEA  cmp         eax,dword ptr [rdx+8]  
00007FF942690AED  jae         00007FF942690B11  
00007FF942690AEF  movsxd      rcx,eax  
00007FF942690AF2  vmovsd      qword ptr [rdx+rcx*8+10h],xmm6  
            for(int i = 0; i < N - Old.Length; i++)
00007FF942690AF9  inc         eax  
00007FF942690AFB  cmp         ebx,eax  
00007FF942690AFD  jg          00007FF942690AE7  
00007FF942690AFF  vmovaps     xmm6,xmmword ptr [rsp+20h]  
00007FF942690B06  add         rsp,30h  
00007FF942690B0A  pop         rbx  
00007FF942690B0B  pop         rbp  
00007FF942690B0C  pop         rsi  
00007FF942690B0D  pop         rdi  
00007FF942690B0E  pop         r14  
00007FF942690B10  ret  

Sự cố xảy ra tại địa chỉ 00007FF942690AFD, mã jg 00007FF942690AE7. Nó nhảy trở lại nếu ebx (chứa 4, giá trị kết thúc vòng lặp) lớn hơn (jg) so với eax, giá trị i. Điều này không thành công khi tất nhiên là 4, vì vậy nó không viết phần tử cuối cùng trong mảng.

Nó không thành công, vì nó bao gồm giá trị đăng ký của tôi (eax, tại 0x00007FF942690AF9), và sau đó kiểm tra nó với 4, nhưng nó vẫn phải ghi giá trị đó. Hơi khó để xác định chính xác vấn đề nằm ở đâu, vì có vẻ như nó có thể là kết quả của việc tối ưu hóa (N-Old.Length), vì bản dựng gỡ lỗi chứa mã đó, nhưng bản phát hành đã tính toán trước điều đó. Vì vậy, đó là để những người jit sửa chữa;)


2
Một trong những ngày này, tôi cần dành chút thời gian để học lắp ráp / opcodes CPU. Có lẽ ngây thơ tôi cứ nghĩ "meh, tôi có thể đọc và viết IL - tôi nên có thể mò mẫm nó" - nhưng tôi không bao giờ hiểu được nó :)
Marc Gravell

x64 / x86 không phải là hợp ngữ tuyệt vời nhất để bắt đầu với tho;) Nó có rất nhiều opcode, tôi đã từng đọc rằng không ai còn sống biết tất cả. Không chắc điều đó có đúng không, nhưng đọc nó lúc đầu không dễ đâu. Mặc dù nó sử dụng một vài quy ước đơn giản, như [], đích trước phần nguồn và ý nghĩa của các thanh ghi này (al là 8bit của rax, eax là 32bit của rax, v.v.). Bạn có thể bước qua nó trong vs tho, điều này sẽ dạy cho bạn những điều cần thiết. Tôi chắc chắn rằng bạn nhặt nó lên một cách nhanh chóng như bạn đã biết opcodes IL;)
Frans Bouma
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.