目次

概要

Ver. 12

.NET 8 で、 InlineArray 属性 (System.Runtime.CompilerServices 名前空間) というものが入りました。

基本的には .NET ランタイム側の機能ですが、 いくつか、C# 側にもこの InlineArray 向けの特殊対応が入っています。

ちなみに、この機能は現状、 コレクション式の内部実装にこそ使っていますが、 本稿で書いているようなコードを直接書く必要はほぼありません。 (実質、本稿はコレクション式の内部実装(の一部)の説明みたいなものです。)

InlineArray 属性

.NET 8 から、 以下のように、構造体に属性を付けると構造体のサイズが変わります。

using System.Runtime.CompilerServices;

// この属性を付けると、 .NET ランタイムが特別扱いして、構造体のサイズを拡大する。
// (コンストラクター引数で Length 指定。)
[InlineArray(3)]
struct FixedBuffer<T>
{
    // フィールドを1個だけ書く。
    // (2個以上書くとコンパイル エラーになる。)
    // 構造体のサイズが sizeof(T) × Length になる。
    private T _value;
}

inline array という名前通り、「埋め込み配列」として使います。 (長さ N の配列代わりに、長さ N 個分のサイズを持った構造体を作ります。 C# の配列はヒープに割り当てられるのに対して、この inline array であればスタック上に値を持てます。)

要は、以下のような「N 個のフィールドを並べる」みたいな構造体を、ランタイム側で自動的に作ってくれる機能です。

using System.Runtime.InteropServices;

// これまでの .NET/C# で同じことをやろうとすると…
// 長さごとに専用の構造体を書いて、
struct FixedBuffer3<T>
{
    // 所望の個数フィールドを書く。
    // (3要素くらいならいいけども、数十とか数百になるときつい。)
    private T _value0;
    private T _value1;
    private T _value2;

    // 変換とかも自前で書く。
    public static implicit operator Span<T>(FixedBuffer3<T> x)
        => MemoryMarshal.CreateSpan(ref x._value0, 3);

    public ref T this[int index] => ref ((Span<T>)this)[index];
}

ちなみに、InlineArrayAttribte クラスには [EditorBrowsable(Never)] 属性がついています (この属性が付いていると、Visual Studio などのコード補完の候補から外れます)。 要するに、開発者が InlineArray 属性を直接使うことは想定していなくて、隠してあります。

stackalloc との違い

これまでも stackalloc という機能を使えば、 一応、スタック上に配列上のデータを置くことはできました。 ただ、stackalloc には結構強い制限があって使いづらいです。

一番きつい制限は、参照型、もしくは、参照を含む型に対して使えないことです (これを認めようとするとガベコレの負担が上がって、パフォーマンス的にかえって不利になるそうです)。 例えば以下のコードでは、string 以下の型に対してコンパイル エラーになります。

// 構造体に対しては使える。
Span<int> i = stackalloc int[100];
Span<DateTimeOffset> d = stackalloc DateTimeOffset[100];

// クラスに対しては使えない。
// (コンパイル エラーになる。)
Span<string> s = stackalloc string[100];

// クラスや参照を含む構造体に対しても使えない。
// (コンパイル エラーになる。)
Span<ContainsRefType> r1 = stackalloc ContainsRefType[100];
Span<ContainsRefField> r2 = stackalloc ContainsRefField[100];

struct ContainsRefType
{
    public string String;
}

ref struct ContainsRefField
{
    public ref int Ref;
}

また、stackalloc で確保したスタック領域は、メソッドを抜けるまで解放されません。 このせいで、ループの内側で間違って stackalloc を使ってしまうと簡単にスタック オーバーフロー(要はメモリ不足)を引き起こします (一般に、スタックはヒープよりもだいぶサイズが小さいです。Windows の場合は 1MB 程度)。 例えば以下のコードを Windows で実行するとスタック オーバーフローします (1000 とか 200 とか、そこまで大きくない数字ですら簡単にスタック オーバーフローになります)。

for (int i = 0; i < 1000; i++)
{
    _ = stackalloc long[200];
}

C# 側特殊対応

一応、C# 側にもこの InlineArray に対する特殊対応が入っています。 (一応、C# 12 の新機能。)

まず、属性を付けた型に対するチェックが働いています。 すでに前述の例でも書いていますが、 InlineArray 属性を付けた型にフィールドが2つ以上あるとコンパイル エラーになります。

using System.Runtime.CompilerServices;

[InlineArray(3)]
struct FixedBuffer<T>
{
    // フィールドを2個以上書くとコンパイル エラーになるのは一応「C# の新機能」。
    private T _value;
}

また、この型を使う側に、以下のような特殊対応が入っています。

  • インデクサーを直接書ける
  • Span<T>/ReadOnlySpan<T> に暗黙的に変換できる
  • foreach で列挙できる
FixedBuffer<string> buffer = new();

// InlineArray に対して直接インデクサーを書ける。
buffer[0] = "zero";
buffer[1] = "one";

// Span/ReadOnlySpan に暗黙的に変換できる。
Span<string> span = buffer;
span[2] = "two";

// foreach で列挙できる。
foreach (var x in buffer)
{
    Console.WriteLine(x);
}

コレクション式と InlineArray

前述の通り、 InlineArray 属性には [EditorBrowsable(Never)] が付いていて、 開発者が直接使う想定はあまりありません。

ただ、この機能は C# 12 時点で、コレクション式の最適化のために使われています。 Span<T>ReadOnlySpan<T> 型に対してコレクション式を使うと、 InlineArray に展開されます。 例えば以下のようなコードの場合、

Span<int> i = [1, 2, 3, 4, 5];

ReadOnlySpan<string> s = ["a", "abc", ""];

以下のようなコードとほぼ同じ挙動になります。

using System.Runtime.CompilerServices;

var i0 = new FixedArray5<int>();
i0[0] = 1;
i0[1] = 2;
i0[2] = 3;
i0[3] = 4;
i0[4] = 5;
Span<int> i = i0;

var s0 = new FixedArray3<string>();
s0[0] = "a";
s0[1] = "abc";
s0[2] = "";
ReadOnlySpan<string> s = s0;

[InlineArray(3)]
struct FixedArray3<T>
{
    private T _value;
}

[InlineArray(5)]
struct FixedArray5<T>
{
    private T _value;
}

将来展望

現状では、先ほどの例でいうと FixedArray3<T>FixedArray5<T> があるように、 長さごとに別の型を用意せざるを得ない状態です。 「N 個のフィールドを並べる」コードを手書きするよりはマシですが、 まだ一時しのぎ的な実装になっていることは否めません。

根本的に大工事して型システムを改善するなら、 例えば、以下のように「整数型引数」を導入して、これを使って InlineArray を作りたいという話もなくはないです。

// ※仮定の文法
namespace System;

public struct InlineArray<T, int N>;

こういう「public にできる(一時しのぎではないちゃんとした) InlineArray 型」があるのなら、 C# 側でももう少し踏み込んだ文法を導入したかったみたいです。 候補として挙がっていたのは、int[N] という書き方で「長さ N の InlineArray」を書けるようにするというものです。

// ※仮定の文法
var c = new C();

int[3] values = c.Values;

class C
{
    private int[3] _values;
    public int[3] Values => _values;
}

前述の InlineArray<T, int N> みたいな書き方をできるようにするのは結構大変で、 短期的には実現しそうになく、 それに依存しそうな int[N] という書き方も残念ながらしばらく実現の見込みはありません。

更新履歴

ブログ