Должен ли этот небезопасный код работать и в .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
К вашему сведению: return Unsafe.As<byte, uint>(ref bytes[0]);или return MemoryMarshal.Cast<byte, uint>(bytes)[0];- нет необходимости использовать GetPinnableReference(); хотя заглядываем в другое
Марк Гравелл
SharpLab на случай, если он кому-нибудь поможет. Две версии, которые избегают Span<T>, компилируются в разные IL. Я не думаю, что вы делаете что-то недействительное: я подозреваю, что ошибка JIT.
canton7
что за фигня ты видишь? Вы используете взломать, чтобы отключить localals-init? этот хак существенно влияет stackalloc(то есть не
Марк Грэвелл
@ canton7, если они компилируются в один и тот же IL, мы не можем сделать вывод, что это ошибка JIT ... если IL такой же и т. д. ... это больше похоже на ошибку компилятора, если что-нибудь, возможно, со старым компилятором? György: можете ли вы указать, как именно вы это компилируете? какой SDK, например? Я не могу воспроизвести мусор
Марк Гравелл
1
Похоже, что stackalloc не всегда равен нулю, на самом деле: ссылка
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 
}

Я думаю, что я был бы рад сказать, что это ошибка компилятора, или, по крайней мере, нежелательный побочный эффект и поведение, учитывая, что были приняты предыдущие решения, чтобы сказать «испустить init .locals» , в частности, чтобы попытаться оставайтесь в stackallocздравом уме - но согласны ли люди компилятора, решать им.

Обходной путь: обрабатывать stackallocпространство как неопределенное (что, честно говоря, это то, что вы должны делать); если вы ожидаете, что это будут нули: вручную обнулите его.

Марк Гравелл
источник
2
Кажется, есть открытый билет для этого. Я собираюсь добавить новый комментарий к этому.
Дьёрдь Кёсег
Да, все мои работы, и я не заметил, что первая пропала locals init. Хороший.
canton7
1
@ canton7, если вы чем-то похожи на меня, вы автоматически пропускаете мимо .maxstackи .locals, особенно легко заметить, что он есть / не существует :)
Марк Гравелл
1
The content of the newly allocated memory is undefined.в соответствии с MSDN. В спецификации не сказано, что память также должна быть обнулена. Таким образом, похоже, что он работает на старой платформе только случайно или в результате недоговорного поведения.
Луаан