Почему поведение кода отличается в режиме выпуска и отладки?

84

Рассмотрим следующий код:

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;
}

Результат в режиме отладки: 100,100,100,100,100. Но в режиме Release это: 100,100,100,100,0.

Что происходит?

Он был протестирован с использованием .NET framework 4.7.1 и .NET Core 2.0.0.

Ашкан Нурзаде
источник
Какую версию Visual Studio (или компилятора) вы используете?
Styxxy 01
9
Репро; добавление Console.WriteLine(i);в последний loop ( dd[i] = d;) «исправляет» это, что предполагает ошибку компилятора или ошибку JIT; заглядывая в ИЛ ...
Марк Грейвелл
@Styxxy, протестировано на vs2015, 2017 и нацелено на все .net framework> = 4.5
Ашкан Нурзаде, 01
Однозначно ошибка. Он также исчезает, если вы удалите if (dd.Length >= N) return;, что может быть более простым воспроизведением.
Jeroen Mostert
1
Неудивительно, что если сравнивать яблоки с яблоками, кодогенерация x64 для .Net Framework и .Net Core имеет схожую производительность, поскольку (по умолчанию) это, по сути, один и тот же код, генерирующий jit. Было бы интересно сравнить производительность кодогенератора .Net Framework x86 с кодогенератором .Net Core x86 (который использует RyuJit с версии 2.0). По-прежнему существуют случаи, когда старый jit (он же Jit32) знает несколько уловок, а RyuJit - нет. И если вы обнаружите такие случаи, не забудьте открыть для них проблемы в репозитории CoreCLR.
Энди Эйерс

Ответы:

70

Похоже, это ошибка JIT; Я тестировал:

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

и добавляем Console.WriteLine(i)исправления. Единственное изменение IL:

// ...
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 

против

// ...
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 

что выглядит совершенно правильно (единственная разница заключается в дополнительном ldloc.3и call void [System.Console]System.Console::WriteLine(int32), а также в другой, но эквивалентной цели для br.s).

Я подозреваю, что это потребует исправления JIT.

Окружающая обстановка:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Предварительная версия 5.0
  • dotnet --version: 2.1.1
Марк Гравелл
источник
Тогда где сообщить об ошибке?
Ашкан Нурзаде 01
1
Я также вижу это в .NET full 4.7.1, поэтому, если это не ошибка RyuJIT, я съем свою шляпу.
Йерун Мостерт
2
Я не смог воспроизвести, установил .NET 4.7.1 и теперь могу воспроизвести.
user3057557 01
3
@MarcGravell .Net framework 4.7.1 и .net Core 2.0.0
Ашкан Нурзаде
4
@AshkanNourzadeh Честно говоря, я бы, наверное, записал это здесь , подчеркнув, что люди считают, что это ошибка RyuJIT
Марк Грейвелл
6

Это действительно ошибка сборки. x64, .net 4.7.1, сборка выпуска.

разборка:

            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  

Проблема находится по адресу 00007FF942690AFD, jg 00007FF942690AE7. Он возвращается, если ebx (который содержит 4, конечное значение цикла) больше (jg), чем eax, значение i. Конечно, это не удается, когда оно равно 4, поэтому он не записывает последний элемент в массиве.

Он терпит неудачу, потому что он включает значение регистра i (eax, по адресу 0x00007FF942690AF9), а затем проверяет его с 4, но ему все равно нужно записать это значение. Немного сложно определить, где именно находится проблема, поскольку похоже, что это может быть результатом оптимизации (N-Old.Length), поскольку сборка отладки содержит этот код, но сборка выпуска предварительно рассчитывает это. Так что это должны исправлять jit-люди;)

Франс Баума
источник
2
На днях мне нужно выкроить время, чтобы изучить коды операций сборки / процессора. Возможно, я наивно продолжаю думать: «Ммм, я умею читать и писать IL - я должен уметь это грокнуть» - но я просто никогда не
Марк Гравелл
x64 / x86 - не лучший язык ассемблера для начала;) В нем так много кодов операций, что я однажды прочитал, что в мире нет никого, кто знал бы их все. Не уверен, что это правда, но сначала не так-то легко ее прочитать. Хотя он использует несколько простых соглашений, таких как [], место назначения перед исходной частью и то, что все эти регистры означают (al - 8-битная часть rax, eax - 32-битная часть rax и т. Д.). Вы можете пройти через это в vs tho, что должно научить вас основам. Я уверен, что вы быстро поймете это, поскольку вы уже знаете коды операций IL;)
Frans