Questo codice non sicuro dovrebbe funzionare anche in .NET Core 3?


42

Sto eseguendo il refactoring delle mie librerie da utilizzare Span<T>per evitare allocazioni di heap, se possibile, ma poiché mi rivolgo anche a framework più vecchi, sto implementando anche alcune soluzioni generali di fallback. Ma ora ho riscontrato uno strano problema e non sono sicuro se ho trovato un bug in .NET Core 3 o sto facendo qualcosa di illegale.

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

È interessante notare che ReinterpretOldfunziona bene in .NET Framework e .NET Core 2.0 (quindi potrei esserne contento dopo tutto), tuttavia mi disturba un po '.

Btw. ReinterpretOldpuò essere risolto anche in .NET Core 3.0 con una piccola modifica:

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

La mia domanda:

Si tratta di un bug o ReinterpretOldfunziona nei framework più vecchi solo per caso e dovrei applicare la correzione anche per loro?

Osservazioni:

  • La build di debug funziona anche in .NET Core 3.0
  • Ho cercato di applicare [MethodImpl(MethodImplOptions.NoInlining)]a ReinterpretOld, ma non ha avuto alcun effetto.

2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);oppure return MemoryMarshal.Cast<byte, uint>(bytes)[0];- non è necessario utilizzare GetPinnableReference(); guardando dall'altra parte, però
Marc Gravell

SharpLab nel caso in cui aiuti qualcun altro. Le due versioni che evitano Span<T>si compilano in IL diversi. Non credo che tu stia facendo nulla di invalido: sospetto un bug JIT.
canton7,

qual è la spazzatura che stai vedendo? stai usando l'hack per disabilitare i locali-init? questo hack ha un impatto significativostackalloc (cioè non cancella lo spazio assegnato)
Marc Gravell

@ canton7 se si compilano nello stesso IL, non possiamo dedurre che si tratta di un bug JIT ... se l'IL è lo stesso, ecc ... suona più come un bug del compilatore, semmai, forse con un compilatore più vecchio? György: puoi indicare esattamente come lo stai compilando? quale SDK, per esempio? Non posso
riproporre

1
Sembra che stackalloc non sia sempre zero, in realtà: link
canton7,

Risposte:


35

Ooh, questa è una scoperta divertente; quello che sta succedendo qui è che il tuo locale si sta ottimizzando - non ci sono residenti locali, il che significa che non c'è .locals init, il che significa che stackallocsi comporta in modo diverso e non cancella lo spazio;

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

diventa:

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

Io penso che sarei felice di dire che questo è un bug del compilatore, o almeno: un effetto collaterale e un comportamento indesiderato dato che le decisioni precedenti sono stati messi in atto per dire "emettono le .locals init" , in particolare per cercare di mantenersi stackallocsani di mente, ma se la gente del compilatore è d'accordo dipende da loro.

La soluzione è: trattare lo stackallocspazio come indefinito (che, per essere onesti, è ciò che si intende fare); se ti aspetti che sia zero: azzeralo manualmente.


2
Sembra che ci sia un biglietto aperto per questo. Ho intenzione di aggiungere un nuovo commento a questo.
György Kőszeg,

Eh, tutto il mio lavoro e non ho notato che mancava il primo locals init. Ben fatto.
canton7,

1
@ canton7 se sei qualcosa come me, salti automaticamente .maxstacke .locals, rendendo particolarmente facile non notare che è / non c'è :)
Marc Gravell

1
The content of the newly allocated memory is undefined.secondo MSDN. La specifica non dice che neanche la memoria dovrebbe essere azzerata. Quindi sembra che funzioni solo su un vecchio framework per caso o come risultato di un comportamento non contrattuale.
Luaan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.