Haruskah kode yang tidak aman ini berfungsi juga di .NET Core 3?

42

Saya refactoring perpustakaan saya untuk digunakan Span<T>untuk menghindari alokasi tumpukan jika mungkin tetapi karena saya juga menargetkan kerangka kerja yang lebih tua saya menerapkan beberapa solusi cadangan umum juga. Tapi sekarang saya menemukan masalah aneh dan saya tidak yakin apakah saya menemukan bug di .NET Core 3 atau saya melakukan sesuatu yang ilegal.

Masalah:

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

Cukup menarik, ReinterpretOldbekerja dengan baik di .NET Framework dan .NET Core 2.0 (jadi saya bisa senang dengan itu semua), masih, itu sedikit mengganggu saya.

Btw. ReinterpretOlddapat diperbaiki juga di .NET Core 3.0 dengan modifikasi kecil:

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

Pertanyaan saya:

Apakah ini bug atau ReinterpretOldbekerja dalam kerangka kerja lama hanya secara tidak sengaja dan haruskah saya menerapkan perbaikannya juga untuk mereka?

Catatan:

  • Membangun debug bekerja juga di .NET Core 3.0
  • Saya mencoba menerapkan [MethodImpl(MethodImplOptions.NoInlining)]untuk ReinterpretOldtetapi tidak berpengaruh.
György Kőszeg
sumber
2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);atau return MemoryMarshal.Cast<byte, uint>(bytes)[0];- tidak perlu digunakan GetPinnableReference(); melihat ke dalam bit lainnya,
Marc Gravell
SharpLab seandainya itu membantu orang lain. Dua versi yang menghindari Span<T>melakukan kompilasi ke IL berbeda. Saya tidak berpikir Anda melakukan sesuatu yang tidak valid: Saya menduga ada bug JIT.
canton7
apa sampah yang kamu lihat? apakah Anda menggunakan peretasan untuk menonaktifkan lokal-init? peretasan ini secara signifikan berdampak stackalloc(yaitu tidak menghapus ruang yang dialokasikan)
Marc Gravell
@ canton7 jika mereka mengkompilasi ke IL yang sama, kita tidak dapat menyimpulkan itu adalah bug JIT ... jika IL adalah sama, dll ... terdengar lebih seperti bug kompiler, jika ada, mungkin dengan kompiler yang lebih tua? György: dapatkah Anda menunjukkan dengan tepat bagaimana Anda menyusun ini? SDK apa, misalnya? Saya tidak dapat melaporkan ulang sampah
Marc Gravell
1
Sepertinya stackalloc tidak selalu nol, sebenarnya: tautan
canton7

Jawaban:

35

Ooh, ini adalah penemuan yang menyenangkan; apa yang terjadi di sini adalah bahwa lokal Anda semakin dioptimalkan - tidak ada penduduk setempat yang tersisa, yang berarti tidak ada .locals init, yang berarti stackallocberperilaku berbeda , dan tidak menghapus ruang;

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

menjadi:

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

Saya pikir saya akan senang mengatakan bahwa ini adalah bug penyusun, atau setidaknya: efek samping dan perilaku yang tidak diinginkan mengingat bahwa keputusan sebelumnya telah dibuat untuk mengatakan "emit the .locals init" , khusus untuk mencoba dan tetap stackallocwaras - tetapi apakah orang-orang kompilator setuju terserah mereka.

Solusinya adalah: perlakukan stackallocruang sebagai tidak terdefinisi (yang, adil, adalah apa yang Anda maksudkan untuk dilakukan); jika Anda mengharapkannya nol: secara manual nol.

Marc Gravell
sumber
2
Sepertinya ada tiket terbuka untuk ini. Saya akan menambahkan komentar baru untuk itu.
György Kőszeg
Huh, semua pekerjaan saya dan saya tidak melihat yang pertama hilang locals init. Bagus
canton7
1
@ canton7 jika Anda seperti saya, Anda secara otomatis melewati masa lalu .maxstackdan .locals, membuatnya sangat mudah untuk tidak menyadari bahwa itu ada / tidak ada :)
Marc Gravell
1
The content of the newly allocated memory is undefined.menurut MSDN. Spesifikasi tidak mengatakan bahwa memori juga harus di-zeroed. Jadi sepertinya itu hanya bekerja pada kerangka kerja lama secara tidak sengaja, atau sebagai akibat dari perilaku non-kontrak.
Luaan