この安全でないコードは.NET Core 3でも機能しますか?


42

Span<T>可能な場合はヒープ割り当てを回避するために使用するライブラリをリファクタリングしていますが、古いフレームワークも対象としているため、いくつかの一般的なフォールバックソリューションも実装しています。しかし今、私は奇妙な問題を発見しました。.NETCore 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でうまく機能します(結局、満足できるかもしれません)が、それでも少し気になります。

ところで 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>する2つのバージョンは、異なるILにコンパイルします。あなたが無効なことをしているとは思いません。JITのバグだと思います。
canton7

あなたが見ているゴミは何ですか?locals-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 
}

私が考えて、望ましくない副作用と行動することを与えられた:私は、これはコンパイラのバグである、または少なくともと言うことは幸せになるだろう、前の決定は「.localsはinitを発する」と言うための場所に置かれている特に試すとしますstackalloc正気を保つ-しかし、コンパイラの人々が同意するかどうかは彼ら次第です。

回避策は次のとおりです。stackallocスペースを未定義として扱います(公平にするために、これはあなたが意図していることです)。ゼロになることが予想される場合:手動でゼロにします。


2
これにはオープンチケットがあるようです。これに新しいコメントを追加します。
ジェルジKőszeg

ええと、私のすべての仕事と私は最初のものが欠けていることに気づかなかったlocals init。良いですね。
canton7

1
@ canton7あなたが私のような人なら、あなたは自動的に過去.maxstackとをスキップ.localsし、それがそこにある/ないことに気づかないことを特に簡単にします:)
マークグラベル

1
The content of the newly allocated memory is undefined.MSDNによると。仕様には、メモリをゼロにする必要があることも記載されていません。したがって、それは偶然に、または契約外の行動の結果として、古いフレームワークでのみ機能するように見えます。
Luaan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.