今日は「low level hackathon」話2個目。

可変長引数

C# の可変長引数は、一時的にデータを詰めておく配列を作ってメソッドに渡す作りになっています。 例えば、void m(params int[] args) というメソッドがあったとして、 m(1, 2, 3); みたいに呼び出した場合、 m(new int[] { 1, 2, 3 }); みたいに展開されます。

ここで問題になるのが new int[] でヒープ アロケーションが発生する点。 あまりにも数が重なると無視できないコストになってきます。

params Span

C# 7.2 の頃に Span<T> 構造体が入ったことで、 当然 params T[]new T[] によるアロケーションも避けたいという話が出てきます。

つまるところ、

  • メソッド定義
    • 今あるもの: void m(params T[] args)
    • 欲しいもの: void m(params Span<T> args)
  • 呼び出し側の展開結果:
    • 今あるもの: m(new int[] { 1, 2, 3 }); とか
    • 欲しいもの: m(stackalloc int[] { 1, 2, 3 }); とか

みたいなものが欲しいと。

実際、案自体は結構昔からあります:

params Span<T>

参照型 stackalloc (没気味)

ただ、stackalloc の制限が結構きついので、素直に上記のような展開はできません。

わかりやすい原因は、参照型に対して stackalloc を使えない点。 以下のようなコードはコンパイル エラーになります。

Span<string> span = stackalloc string[4];

これは元々ある .NET ランタイムの制限です。

参照型に対する stackalloc を下手に認めてしまうとガベージコレクションの参照トラッキングの負担が上がって、GC 発生時のコストまで見た時トータルではかえって遅くなる可能性が高いとのこと。

この制限に対して、low level hachathon で1回、任意の型に対する stackalloc をやってみる実験をしたみたいです。

Experiment with Unsafe.StackAlloc<T>

pull request がそっ閉じされてるんで、 やっぱり上記のような stackalloc の問題が許容されなかったんですかね。

ValueArray

他に、params Span<T> に使いたいのであれば固定長配列の類でもいいわけでして。 例えば以下のようなコードで「長さ4固定の配列もどき」を作ることはできます。

using System.Runtime.InteropServices;

ValueArray4<string> buffer = default;
Span<string> span = MemoryMarshal.CreateSpan(ref buffer.X0, 4);

struct ValueArray4<T>
{
    public T X0, X1, X2, X3;
}

とはいえ、こんなコードを都度手書きはしたくないわけでして。 あと、できれば ValueArray<string, 4> みたいな感じで何らかの手段で「長さ」の情報はジェネリクス的に渡したかったりはします。

それに類するものをとりあえず実装してみたという pull request が low level hackathon で出てたりします。

[hackathon] ValueArray

「試しにやってみた」実装なのでなかなかにキモイです… 現状の .NET は「ジェネリクスに型引数代わりに整数を渡す」みたいなことができないので、 1 の代わりに object[]、2 の代わりに object[,]、3 の代わりに object[,,]、… みたいな、object 配列の次元を整数代わりに使うというすごい実装。 本来であれば ValueArray<string, 4> と書きたいところを ValueArray<string, object[,,,]> と書くことになります…

using System.Runtime.InteropServices;

ValueArray<string, object[,,,]> buffer = default;
Span<string> span = buffer.AsSpan();

これはさすがにあまりにもきもいので没気味。 代替案として、「いったん属性を付けて特殊処理しようか」みたいな話になっています。

こちらだと、いちいち構造体の定義が要るみたいです。

using System.Runtime.InteropServices;

ValueArray4<string> buffer = default;
Span<string> span = buffer.AsSpan();

// この属性を付けた構造体は T 4つ分のメモリを確保する。
[InlineArray(Length = 4)]
struct ValueArray4<T>
{
    private T _element0;
    public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _element0, 4);
}

やっぱ、根本的にはジェネリクスに整数を渡せるようにしてほしいところですけどね… それは結構型システムに手を入れないといけないみたいでちょっと大変みたいです。