paramsは便利だけど
paramsは便利だけど、Unityが使用しているC#バージョンではヒープを使用します。それが積み重なるとGCが発生するので嬉しくないですね。
Unityでヒープを使わずにparamsのような書き方ができる方法を考えてみました。
ポイント
その1 paramsで書きたい関数の引数の数
paramsで引数を書く時に、私は4つ以上の引数を入れたことがありません。
paramsで10や20個以上の引数を入力するとき、それはもう普通に配列を作ってやったほうがいいんじゃないでしょうか。
この考えで行くと、paramsはせいぜい10個程度の引数入力に対応すればいいのです。
掲載するコードでは冗長になるため1~3の引数のみに対応しています。使いたい方は各々、自分の要望に合わせて追加で定義してください。
その2 暗黙的変換は実装しない
ReadOnlySpanへの暗黙的変換を実装すれば、利用時のコード量が少なくなります。しかしスパンの指す領域が少ない場合にメモリリークが発生します。
多分暗黙的変換のとき、小さなReadOnlySpanに最適化がかかって、変な領域をさしてしまうのだと思います。
よくわからないのですが、明示的にReadOnlySpanに変換する事でこのバグは発生しなくなったのでこういう方針にしました。
ChatGPTによる解説(多分正解)
💥 Buffer1 が壊れる理由:小さすぎる値型最適化(コピー省略)
JIT(特に RyuJIT)は小さな構造体(8バイト以下)をレジスタ上で扱う最適化を行います。
これが壊れる主原因です。(1) Buffer1 のサイズは 4 または 8 バイト(例:int なら 4B)
→ JIT はこの構造体を「単一レジスタ(例:RAX)」で受け渡します。
→ MemoryMarshal.CreateReadOnlySpan(ref t0, size) で参照した ref が レジスタ上の>一時領域を指してしまう。結果:
ref t0 はメモリ上に存在しない値を指してしまう(JITがRAX上に展開してるため)
直後に関数を抜けると無効領域参照になる
→ 壊れたスパンが生成される。
✅ Buffer2・Buffer3 が壊れない理由:構造体が大きくなり、スタック上に置かれる
Buffer2 は 8 バイト × 2 = 16 バイト
Buffer3 は 24 バイト
このサイズになると、JIT は「レジスタ受け渡し」をやめ、
構造体をスタック上に配置してポインタ渡しします。つまり ref buffer.t0 は「実際にスタック上にある構造体領域」を指すようになります。
そのため、メソッドを抜けるまで有効なメモリ参照として機能し、
壊れたスパンにならないのです。
使用例
using System;
using System.Runtime.InteropServices;
class Program
{
static void Main()
{
WriteStrings(Temp.Params("スタックで効率的").AsRSpan);
WriteStrings(Temp.Params("これは","未対応の長さなので", "ヒープで","ゴミが","できる"));
WriteSum(Temp.Params(99).AsRSpan);
WriteSum(Temp.Params(10, 20).AsRSpan);
WriteSum(Temp.Params(10, 20, 30).AsRSpan);
Console.ReadLine();
}
static void WriteStrings(ReadOnlySpan<string> str)
{
for (int i = 0; i < str.Length; i++) Console.WriteLine(str[i]);
}
static void WriteSum(ReadOnlySpan<int> nums)
{
int sum = 0;
foreach (var num in nums)
{
Console.Write($"{num}と");
sum += num;
}
Console.WriteLine($"の合計は、{sum}です。");
}
}
コード
public static class Temp
{
public static Buffer1<T> Params<T>(T t0) => new Buffer1<T>() { t0 = t0 };
public static Buffer2<T> Params<T>(T t0, T t1) => new Buffer2<T>() { t0 = t0, t1 = t1 };
public static Buffer3<T> Params<T>(T t0, T t1, T t2) => new Buffer3<T>() { t0 = t0, t1 = t1, t2 = t2 };
public static T[] Params<T>(params T[] ts)
{
Console.WriteLine($"独自最適化のTemp.Paramsでサイズ{ts.Length}は定義外です。追加で定義しないとパフォーマンスが悪化します");
return ts;
}
public struct Buffer1<T>
{
const int size = 1;
public T t0;
public ref T this[int index] => ref AsSpan[index];
public ReadOnlySpan<T> AsRSpan => MemoryMarshal.CreateReadOnlySpan(ref t0, size);
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref t0, size);
}
public struct Buffer2<T>
{
const int size = 2;
public T t0, t1;
public ref T this[int index] => ref AsSpan[index];
public ReadOnlySpan<T> AsRSpan => MemoryMarshal.CreateReadOnlySpan(ref t0, size);
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref t0, size);
}
public struct Buffer3<T>
{
const int size = 3;
public T t0, t1, t2;
public ref T this[int index] => ref AsSpan[index];
public ReadOnlySpan<T> AsRSpan => MemoryMarshal.CreateReadOnlySpan(ref t0, size);
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref t0, size);
}
}