Почему поведение кода отличается в режиме выпуска и отладки?
Рассмотрим следующий код:
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.
Ответы
Ответ 1
Это похоже на ошибку 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 Preview 5.0
-
dotnet --version
: 2.1.1
Ответ 2
Это действительно ошибка сборки. 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, конечно, поэтому он не записывает последний элемент в массиве.
Он не работает, потому что он регистрирует значение (eax, 0x00007FF942690AF9), а затем проверяет его на 4, но он все равно должен записать это значение. Немного сложно определить, где именно находится проблема, поскольку это похоже на то, что это может быть результатом оптимизации (N-Old.Length), поскольку сборка отладки содержит этот код, но сборка релиза предсказывает это. Так что для jit людей исправить;)