¿Por qué el comportamiento del código es diferente en el modo de liberación y depuración?

84

Considere el siguiente código:

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

El resultado en el modo de depuración es: 100,100,100,100,100. Pero en el modo de lanzamiento es: 100,100,100,100,0.

¿Qué está pasando?

Se probó con .NET framework 4.7.1 y .NET Core 2.0.0.

Ashkan Nourzadeh
fuente
¿Qué versión de Visual Studio (o el compilador) usas?
Styxxy
9
Repro; agregar un Console.WriteLine(i);en el bucle final ( dd[i] = d;) lo "arregla", lo que sugiere un error del compilador o error JIT; mirando en IL ...
Marc Gravell
@Styxxy, probado en vs2015, 2017 y dirigido a todos los marcos .net> = 4.5
Ashkan Nourzadeh
Definitivamente un error. También desaparece si lo quitas if (dd.Length >= N) return;, que puede ser una reproducción más simple.
Jeroen Mostert
1
No es sorprendente que una vez que la comparación sea de manzanas a manzanas, el codegen x64 para .Net Framework y .Net Core tenga un rendimiento similar, ya que (de forma predeterminada) es esencialmente el mismo código de generación de jit. Sería interesante comparar el rendimiento del codegen .Net Framework x86 con el codegen x86 de .Net Core (que utiliza RyuJit desde 2.0). Todavía hay casos en los que el jit más antiguo (también conocido como Jit32) conoce algunos trucos que RyuJit no conoce. Y si encuentra alguno de estos casos, asegúrese de abrir los problemas para ellos en el repositorio de CoreCLR.
Andy Ayers

Respuestas:

70

Esto parece ser un error de JIT; He probado con:

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

y agregando los Console.WriteLine(i)arreglos. El único cambio de IL es:

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

que se ve exactamente bien (la única diferencia es el extra ldloc.3y call void [System.Console]System.Console::WriteLine(int32), y un objetivo diferente pero equivalente para br.s).

Necesitará una solución JIT, sospecho.

Ambiente:

  • Environment.Version: 4.0.30319.42000
  • <TargetFramework>netcoreapp2.0</TargetFramework>
  • VS: 15.5.0 Vista previa 5.0
  • dotnet --version: 2.1.1
Marc Gravell
fuente
Entonces, ¿dónde informar del error?
Ashkan Nourzadeh
1
También lo veo en .NET full 4.7.1, así que si esto no es un error de RyuJIT, me comeré mi sombrero.
Jeroen Mostert
2
No pude reproducir, instalé .NET 4.7.1 y puedo reproducir ahora.
user3057557
3
@MarcGravell .Net framework 4.7.1 y .net Core 2.0.0
Ashkan Nourzadeh
4
@AshkanNourzadeh Probablemente lo registraría aquí para ser honesto, enfatizando que la gente cree que es un error de RyuJIT
Marc Gravell
6

De hecho, es un error de montaje. x64, .net 4.7.1, versión de versión.

desmontaje:

            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  

El problema está en la dirección 00007FF942690AFD, el jg 00007FF942690AE7. Salta hacia atrás si ebx (que contiene 4, el valor final del ciclo) es mayor (jg) que eax, el valor i. Esto falla cuando es 4, por supuesto, por lo que no escribe el último elemento de la matriz.

Falla, porque incluye el valor de registro i (eax, en 0x00007FF942690AF9), y luego lo verifica con 4, pero aún tiene que escribir ese valor. Es un poco difícil precisar dónde se encuentra exactamente el problema, ya que parece que podría ser el resultado de la optimización de (N-Old.Length), ya que la compilación de depuración contiene ese código, pero la compilación de la versión lo calcula previamente. Entonces eso es para que la gente jit lo arregle

Frans Bouma
fuente
2
Uno de estos días necesito sacar algo de tiempo para aprender los códigos de operación de ensamblaje / CPU. Quizás ingenuamente sigo pensando "meh, puedo leer y escribir IL, debería ser capaz de asimilarlo", pero nunca lo consigo :)
Marc Gravell
x64 / x86 no es el mejor lenguaje ensamblador para empezar;) Tiene tantos códigos de operación, una vez leí que no hay nadie vivo que los conozca todos. No estoy seguro de si eso es cierto, pero no es tan fácil de leer al principio. Aunque usa algunas convenciones simples, como [], el destino antes de la parte de origen y lo que significan todos estos registros (al es parte de 8 bits de rax, eax es parte de 32 bits de rax, etc.). Puede recorrerlo en vs tho, que debería enseñarle lo esencial. Estoy seguro de que lo capta rápidamente, ya que ya conoce los códigos de operación de IL;)
Frans Bouma