Esse código não seguro também deve funcionar no .NET Core 3?

42

Estou refatorando minhas bibliotecas Span<T>para evitar alocações de heap, se possível, mas como eu viso também estruturas mais antigas, também estou implementando algumas soluções gerais de fallback. Mas agora encontrei um problema estranho e não tenho certeza se encontrei um bug no .NET Core 3 ou estou fazendo algo ilegal.

O problema:

// 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 bem no .NET Framework e no .NET Core 2.0 (para que eu possa ficar feliz com isso, afinal), ainda assim, isso me incomoda um pouco.

Btw. ReinterpretOldtambém pode ser corrigido no .NET Core 3.0 com uma pequena modificação:

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

Minha pergunta:

Isso é um bug ou ReinterpretOldfunciona em estruturas mais antigas apenas por acidente e devo aplicar a correção também a elas?

Observações:

  • A compilação de depuração também funciona no .NET Core 3.0
  • Eu tentei aplicar [MethodImpl(MethodImplOptions.NoInlining)]para ReinterpretOld, mas não teve nenhum efeito.
György Kőszeg
fonte
2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);ou return MemoryMarshal.Cast<byte, uint>(bytes)[0];- não há necessidade de usar GetPinnableReference(); olhando para a outra parte, porém
Marc Gravell
SharpLab , caso ajude mais alguém. As duas versões que evitam Span<T>são compiladas para diferentes IL. Não acho que você esteja fazendo nada inválido: suspeito de um bug do JIT.
canton7
qual é o lixo que você está vendo? você está usando o hack para desativar o locals-init? Este hack significativamente impactos stackalloc(ou seja, não limpe o espaço alocado)
Marc Gravell
@ canton7 se eles compilarem para o mesmo IL, não podemos inferir que é um bug do JIT ... se o IL for o mesmo, etc ... soa mais como um bug do compilador, se houver, talvez com um compilador mais antigo? György: você pode indicar exatamente como está compilando isso? qual SDK, por exemplo? Eu não posso repro o lixo
Marc Gravell
11
Parece que stackalloc nem sempre zero, na verdade: ligação
canton7

Respostas:

35

Ooh, este é um achado divertido; o que está acontecendo aqui é que o seu local está sendo otimizado - não há moradores restantes, o que significa que não existe .locals init, o que significa que stackallocse comporta de maneira diferente e não limpa o espaço;

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

torna-se:

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

Eu acho que eu ficaria feliz em dizer que este é um erro do compilador, ou pelo menos: um efeito colateral e comportamento indesejável, dado que as decisões anteriores foram postas em prática para dizer "emitem as .locals init" , especificamente para tentar mantenha a stackallocsanidade - mas se o pessoal do compilador concorda, é com eles.

A solução alternativa é: trate o stackallocespaço como indefinido (que, para ser justo, é o que você deve fazer); se você espera que seja zeros: zere-o manualmente.

Marc Gravell
fonte
2
Parece que há um ticket aberto para isso. Vou adicionar um novo comentário a isso.
György Kőszeg
Huh, todo o meu trabalho e eu não percebi o primeiro estava faltando locals init. Agradável.
canton7
11
@ canton7 se você for como eu, você pular automaticamente passado .maxstacke .locals, tornando-se especialmente fácil não notar que que é / não é lá :)
Marc Gravell
11
The content of the newly allocated memory is undefined.de acordo com o MSDN. A especificação também não diz que a memória deve ser zerada. Portanto, parece que só funciona na estrutura antiga por acidente ou como resultado de um comportamento não contratual.
Luaan 27/11/19