Czy ten niebezpieczny kod powinien działać również w .NET Core 3?

42

Refaktoryzuję swoje biblioteki, aby Span<T>w miarę możliwości unikały alokacji sterty, ale ponieważ celuję również w starsze platformy, wdrażam również ogólne rozwiązania awaryjne. Ale teraz znalazłem dziwny problem i nie jestem do końca pewien, czy znalazłem błąd w .NET Core 3, czy robię coś nielegalnego.

Problem:

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

Co ciekawe, ReinterpretOlddziała dobrze w .NET Framework i .NET Core 2.0 (więc w końcu mógłbym być z tego zadowolony), ale trochę mnie to niepokoi.

Btw. ReinterpretOldmożna naprawić również w .NET Core 3.0 przez niewielką modyfikację:

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

Moje pytanie:

Czy to błąd, czy ReinterpretOlddziała w starszych frameworkach tylko przypadkowo i czy powinienem zastosować poprawkę również dla nich?

Uwagi:

  • Kompilacja debugowania działa również w .NET Core 3.0
  • Starałem się stosować [MethodImpl(MethodImplOptions.NoInlining)]do ReinterpretOldale to nie miało żadnego wpływu.
György Kőszeg
źródło
2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);lub return MemoryMarshal.Cast<byte, uint>(bytes)[0];- nie ma potrzeby korzystania GetPinnableReference(); spoglądając w drugą stronę
Marc Gravell
SharpLab na wypadek, gdyby pomógł komukolwiek innemu. Dwie wersje, które unikają Span<T>, kompilują się do różnych IL. Nie sądzę, że robisz coś nie tak: Podejrzewam błąd JIT.
kanton7,
jakie śmieci widzisz? czy używasz hacka, aby wyłączyć locals-init? ten hack znacząco wpływa stackalloc(tzn. nie usuwa przydzielonego miejsca)
Marc Gravell
@ kanton7, jeśli kompilują się do tej samej IL, nie możemy wywnioskować, że to błąd JIT ... jeśli IL jest taki sam itp. ... brzmi bardziej jak błąd kompilatora, jeśli w ogóle, może ze starszym kompilatorem? György: czy możesz dokładnie wskazać, jak to skompilujesz? jaki na przykład SDK? Nie mogę zarzucić śmieci
Marc Gravell
1
Wygląda na to, że stackalloc nie zawsze
wynosi

Odpowiedzi:

35

Och, to zabawne znalezisko; dzieje się tutaj to, że twój lokal się optymalizuje - nie ma już miejscowych, co oznacza, że ​​nie ma .locals init, co oznacza, że stackalloczachowuje się inaczej i nie czyści przestrzeni;

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

staje się:

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

I pomyśleć , że będę szczęśliwy mogąc powiedzieć, że jest to błąd kompilatora, lub co najmniej: niepożądanym efektem ubocznym i zachowanie zważywszy, że wcześniejsze decyzje zostały wprowadzone, aby powiedzieć „emitują .locals init” , specjalnie , aby spróbować zachowaj stackallocrozsądek - ale to, czy ludzie kompilatora się zgodzą, zależy od nich.

Obejście polega na: traktowaniu stackallocprzestrzeni jako niezdefiniowanej (co, uczciwie mówiąc, to jest to, co masz zrobić); jeśli spodziewasz się, że będzie to zero: wyzeruj go ręcznie.

Marc Gravell
źródło
2
Wydaje się, że istnieje na to otwarty bilet . Dodam do tego nowy komentarz.
György Kőszeg
Huh, cała moja praca i nie zauważyłem, że pierwsza zaginęła locals init. Niezłe.
kanton7,
1
@ canton7, jeśli jesteś podobny do mnie, automatycznie pomijasz przeszłość .maxstacki .locals, dzięki czemu szczególnie łatwo nie zauważyć, że tam jest / nie ma :)
Marc Gravell
1
The content of the newly allocated memory is undefined.zgodnie z MSDN. Specyfikacja nie mówi też, że pamięć powinna być zerowana. Wygląda więc na to, że działa tylko na starych ramach przez przypadek lub w wyniku działań pozaumownych.
Luaan