¿Debería funcionar este código inseguro también en .NET Core 3?

42

Estoy refactorizando mis bibliotecas para usarlas Span<T>para evitar las asignaciones de almacenamiento dinámico, si es posible, pero al apuntar también a marcos más antiguos, también estoy implementando algunas soluciones de respaldo generales. Pero ahora encontré un problema extraño y no estoy muy seguro de si encontré un error en .NET Core 3 o si estoy haciendo algo ilegal.

La cuestión:

// This returns 1 as expected but cannot be used in older frameworks:
private static uint ReinterpretNew()
{
    Span<byte> bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return Unsafe.As<byte, uint>(ref bytes.GetPinnableReference());
}

// This returns garbage in .NET Core 3.0 with release build:
private static unsafe uint ReinterpretOld()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return *(uint*)bytes;
}

Curiosamente, ReinterpretOldfunciona bien en .NET Framework y en .NET Core 2.0 (por lo que podría estar contento con él después de todo), aún así, me molesta un poco.

Por cierto. ReinterpretOldse puede arreglar también en .NET Core 3.0 mediante una pequeña modificación:

//return *(uint*)bytes;
uint* asUint = (uint*)bytes;
return *asUint;

Mi pregunta:

¿Es esto un error o ReinterpretOldfunciona en marcos anteriores solo por accidente y debo aplicar la solución también para ellos?

Observaciones:

  • La compilación de depuración también funciona en .NET Core 3.0
  • I intentado aplicar [MethodImpl(MethodImplOptions.NoInlining)]a ReinterpretOldpero no tuvo ningún efecto.
György Kőszeg
fuente
2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);o return MemoryMarshal.Cast<byte, uint>(bytes)[0];- no es necesario usar GetPinnableReference(); sin embargo, mirando a la otra parte
Marc Gravell
SharpLab en caso de que ayude a alguien más. Las dos versiones que evitan Span<T>compilan en diferentes IL. No creo que estés haciendo nada inválido: sospecho que hay un error JIT.
canton7
¿Cuál es la basura que estás viendo? ¿Estás usando el hack para deshabilitar locals-init? este truco impacta significativamentestackalloc (es decir, no borra el espacio asignado)
Marc Gravell
@ canton7 si compilan al mismo IL, no podemos inferir que es un error JIT ... si el IL es el mismo, etc., ¿suena más como un error del compilador, si acaso, quizás con un compilador más antiguo? György: ¿puedes indicar exactamente cómo estás compilando esto? ¿Qué SDK, por ejemplo? No puedo reprochar la basura
Marc Gravell
1
Parece que stackalloc no siempre es cero, en realidad: enlace
canton7

Respuestas:

35

Ooh, este es un hallazgo divertido; Lo que está sucediendo aquí es que su local se está optimizando: no quedan locales, lo que significa que no existe .locals init, lo que significa que se stackalloccomporta de manera diferente y no borra el espacio;

private static unsafe uint Reinterpret1()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    return *(uint*)bytes;
}

private static unsafe uint Reinterpret2()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    uint* asUint = (uint*)bytes;
    return *asUint;
}

se convierte en:

.method private hidebysig static uint32 Reinterpret1() cil managed
{
    .maxstack 8
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: ldind.u4 
    L_0008: ret 
}

.method private hidebysig static uint32 Reinterpret2() cil managed
{
    .maxstack 3
    .locals init (
        [0] uint32* numPtr)
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldind.u4 
    L_000a: ret 
}

Creo que me alegraría decir que se trata de un error del compilador, o al menos: un efecto secundario y un comportamiento indeseables dado que se han tomado decisiones previas para decir "emitir el .locals init" , específicamente para intentar manténgase stackalloccuerdo, pero si la gente del compilador está de acuerdo depende de ellos.

La solución es: tratar el stackallocespacio como indefinido (que, para ser justos, es lo que debe hacer); si espera que sean ceros: ponga a cero manualmente.

Marc Gravell
fuente
2
Parece que hay un boleto abierto para esto. Voy a agregar un nuevo comentario a eso.
György Kőszeg
Huh, todo mi trabajo y no me di cuenta de que faltaba el primero locals init. Buena esa.
canton7
1
@ canton7 si eres como yo, automáticamente pasas de largo .maxstacky .locals, haciendo que sea especialmente fácil no notar que está / no está allí :)
Marc Gravell
1
The content of the newly allocated memory is undefined.De acuerdo con MSDN. La especificación tampoco dice que la memoria deba ponerse a cero. Por lo tanto, parece que solo funciona en un marco antiguo por accidente o como resultado de un comportamiento no contractual.
Luaan