Чи повинен цей небезпечний код працювати і в .NET Core 3?


42

Я рефакторинг своїх бібліотек, щоб використовувати, Span<T>щоб уникнути розподілу купи, якщо це можливо, але, оскільки я націлюю також на більш старі рамки, я також реалізую деякі загальні резервні рішення. Але зараз я виявив дивну проблему, і я не зовсім впевнений, знайшов помилку в .NET Core 3 чи роблю щось незаконне.

Питання:

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

Цікаво, що він ReinterpretOldдобре працює в .NET Framework і в .NET Core 2.0 (тому я міг би бути задоволений цим все-таки), все-таки це мене трохи турбує.

Btw. ReinterpretOldможна також виправити в .NET Core 3.0 невеликою модифікацією:

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

Моє запитання:

Це помилка чи ReinterpretOldпрацює у старих рамках лише випадково, і чи потрібно застосовувати виправлення також до них?

Зауваження:

  • Збірка налагодження працює також у .NET Core 3.0
  • Я намагався застосувати [MethodImpl(MethodImplOptions.NoInlining)]до , ReinterpretOldале це не мало ніякого ефекту.

2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);або return MemoryMarshal.Cast<byte, uint>(bytes)[0];- не потрібно використовувати GetPinnableReference(); Хоча, дивлячись на інший шматочок
Марк Гравелл

SharpLab у випадку, якщо він допомагає комусь іншому. Дві версії, які уникають Span<T>, компілюються в різні ІЛ. Я не думаю, що ти робиш щось недійсне: я підозрюю помилку JIT.
canton7

що таке сміття, яке ви бачите? ви використовуєте хак для відключення локальних інітатів? цей злом суттєво впливає stackalloc(тобто він не
стирає

@ canton7, якщо вони компілюються в один і той же IL, ми не можемо зробити висновок, що це помилка JIT ... якщо IL однакова і т. д. ... звучить більше як помилка компілятора, якщо що, можливо, зі старшим компілятором? Дьорджі: чи можете ви точно вказати, як ви це складаєте? який SDK, наприклад? Я не можу
докоряти

1
Схоже, stackalloc не завжди дорівнює нулю, насправді: link
canton7

Відповіді:


35

О, це весела знахідка; що тут відбувається, це те, що ваш місцевий оптимізується - місцевих жителів не залишається, а значить, таких немає .locals init, а це означає, що stackallocповодиться по- різному і не стирає простір;

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

стає:

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

Я думаю , що я був би радий сказати , що це помилка компілятора, або принаймні: небажаний побічний ефект і поведінку , враховуючи , що попередні рішення були поставлені на місце , щоб сказати «випускають .locals ініціалізації» , в зокрема , щоб спробувати тримати stackallocSANE - але чи люди компілятор згоден до них.

Вирішення цього питання: ставитися до stackallocпростору як до невизначеного (що, справедливо кажучи, саме те, що ти маєш намір робити); якщо ви очікуєте, що це буде нулі: вручну нуль.


2
Здається, для цього є відкритий квиток . Я додам новий коментар до цього.
György Kőszeg

Так, вся моя робота, і я не помітила, перша пропала locals init. Хороший.
canton7

1
@ canton7, якщо ти щось схоже на мене, ти автоматично пропускаєш повз .maxstackі .locals, особливо легко не помічаючи, що його немає / немає :)
Marc Gravell

1
The content of the newly allocated memory is undefined.за даними MSDN. У специфікації не сказано, що пам'ять також повинна бути нульовою. Таким чином, схоже, що це працює лише на старих рамках випадково або внаслідок позадоговірної поведінки.
Луань
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.