Чому поведінка коду відрізняється у режимі випуску та налагодження?


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. Але в режимі випуску це: 100 100 100 100 100.

Що відбувається?

Він був протестований за допомогою .NET framework 4.7.1 та .NET Core 2.0.0.


Яку версію Visual Studio (або компілятор) ви використовуєте?
Styxxy

9
Репро; додавання a Console.WriteLine(i);у заключний цикл ( dd[i] = d;) "виправляє" це, що передбачає помилку компілятора або помилку JIT; дивлячись в Іллінойс ...
Марк Гравелл

@Styxxy, протестований на vs2015, 2017 та націлений на кожну .net фреймворк> = 4,5
Ашкан Нурзаде

Безумовно, помилка. Він також зникає, якщо видалити if (dd.Length >= N) return;, що може бути більш простим докором.
Jeroen Mostert

1
Не дивно, що після порівняння "яблука з яблуками", x64-код для .Net Framework та .Net Core має схожу продуктивність, оскільки (за замовчуванням) це, по суті, той самий код генерації jit. Було б цікаво порівняти продуктивність кодегену .Net Framework x86 з кодогеном x86 .Net Core (який використовує 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)виправлення. Єдина зміна ІЛ:

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

Тоді де повідомити про помилку?
Ашкан Нурзаде

1
Я бачу це і в .NET full 4.7.1, тому, якщо це не помилка RyuJIT, я з'їм капелюх.
Jeroen Mostert

2
Я не зміг відтворити, встановив .NET 4.7.1 і можу відтворити зараз.
user3057557

3
@MarcGravell .Net framework 4.7.1 та .net Core 2.0.0
Ашкан Нурзаде 01

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), оскільки збірка налагодження містить цей код, але збірка випуску попередньо обчислює це. Отже, це потрібно для виправлення джитів;)


2
Одного дня мені потрібно викласти трохи часу, щоб вивчити операційні коди збірки / процесора. Можливо, наївно я продовжую думати "я, я вмію читати та писати ІЛ - я повинен мати змогу це промацати" - але я просто ніколи не
обіходжу

x64 / x86 - не найкраща асемблерська мова, яка починається з tho;) У ньому стільки операційних кодів, я одного разу прочитав, що ніхто не знає, хто їх усіх знає. Не впевнений, що це правда, але прочитати її спочатку не так просто. Хоча він використовує кілька простих домовленостей, таких як [], пункт призначення перед вихідною частиною і що всі ці регістри означають (al - це 8-бітна частина rax, eax - це 32-бітна частина rax тощо). Ви можете пройти через це проти того, що повинно навчити вас основним. Я впевнений, що ви швидко піднімаєте його, оскільки ви вже знаєте коди операцій IL;)
Франс Бума,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.