ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) Fast Span と Slow Span の挙動の不一致の罠

System.Span<T> および System.ReadOnlySpan<T>はメモリの連続領域を表すための配列ライクなオブジェクトです。 配列T[]およびそのスライス、スタックメモリstackalloc T[]、ポインタ (マネージドとアンマネージドの両方)を統一的に扱える便利なものです。

これらは .NET Core の時代に後から導入されたもので、これによって得られるパフォーマンス効果を100%使うためにはランタイムに修正が必要でした (Fast Span)。しかし、ランタイムに修正を加える前の古いランタイム (.NET Framework) およびランタイムを指定せず API が一致していれば使えるというライブラリ用の API 仕様である .NET Standard ではこれが使えず、本来のパフォーマンスを得られないが挙動だけ合わせたものとして提供されます (Slow Span)。

System.Span<T> および System.ReadOnlySpan<T>は Fast Span 環境では BCL に始めから入っており、Slow Span 環境では System.Memory の Nuget package で提供されています。本来ライブラリなのでパフォーマンスが劣る以外どちらも同じ挙動をすることが望まれるのですが、実は微妙に違いまして、Slow Span で使うと例外が発生するものがあります。

public unsafe struct Data
{
    public double* Pointer;
}

// -------------------------------------
// Create span (error !!!)
Span<Data> span = stackalloc Data[1] { new Data() };

実は上記のコードは Fast Span では実行できますが、Slow Span では例外を吐きます。 Span<T>に入れる型がポインタのフィールドを持っているとSpan<T>を作れません。ポインタの型は関係ありません。 これは再帰的に適用されるため、例えば上記のData構造体が別の構造体のフィールドを中に持っていて、それがポインタを持っている場合でも例外が出ます。

なら、Slow Span の環境ではポインタを持つ構造体をSpan<T>に入れられないのかというとIntPtrを使うと回避できます。

public unsafe struct Data2
{
    private IntPtr _pointer;
    public double* Pointer
    {
        get => (double*)_pointer;
        set => _pointer = (IntPtr)value;
    }
}

// -------------------------------------
// Create span (OK)
Span<Data2> span = stackalloc Data2[1] { new Data2() };

フィールドとしてポインタを直接持っていなければいいので上記のようにIntPtrに置き換えると例外が出ません。 ここでIntPtrの内部を知っている人からすれば、逆に混乱します。IntPtrは内部にvoid*のフィールドを持っているだけのラッパー構造体にすぎないため、先ほどの再帰的に適用されるという説明に矛盾します。

これは Slow Span のソースコードを確認したわけじゃないため推測ですが、おそらくIntPtrだけを特別扱いしていると思います。

個人的には Slow Span のレガシー環境を使わないので知ったこっちゃない!むしろ早く世の中全員ランタイム移行してくれ~って感じなんですが、まあそういうわけにもいかないので。

ランタイムを指定しない場合のライブラリなんかを作る場合は注意が必要ですね。