概要
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;
struct FixedBuffer3<T>
{
// 所望の個数フィールドを書く。
// (3要素くらいならいいけども、数十とか数百になるときつい。)
private T _value0;
private T _value1;
private T _value2;
// 変換とかも自前で書く。
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _value0, 3);
public ref T this[int index] => ref AsSpan()[index];
}
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] という書き方も残念ながらしばらく実現の見込みはありません。
