はじめに
連続したメモリ領域を表す型である配列(T[]
)にMemory<T>
(ReadOnlyMemory<T>
),Span<T>
(ReadOnlySpan<T>
),ポインタ(T*
),参照変数(ref T
)についてまとめ,その使い分け及び相互変換について記載します。
間違い等あればご指摘いただけますと幸いです。
本ドキュメントの対象バージョン
項目名 | バージョン |
---|---|
.NET | .NET7.0 |
C# | 11 |
各型の概説
T[]
どの言語でもお馴染みの配列。
連続したメモリ領域を表し,インデックスを用いることで要素へのランダムアクセスが可能。
Array
クラスに検索やコピーなどの各種メソッドが揃っているため,ここで紹介しているもののなかでは最も手軽に扱うことができます。
取り回しに関しては List<T>
とは「固定長か否か」という違いがあります。
Memory<T> & ReadOnlyMemory<T>
Span<T>
・ ReadOnlySpan<T>
と同時に実装された構造体で,内部には配列とその開始インデックス,そして領域のサイズを格納しており,昔から実装されていた ArraySegment<T>
のように配列の全体若しくは一部分を表します。
ArraySegment<T>
と異なる点としては,コピーなどの処理を後述の Span<T>
経由で行っており,処理が高速であるということが挙げられます。
Span<T>
に変換する前提であるためか, Span<T>
と比べて検索やコピーといったメソッドはあまり用意されていません。
内部の要素を書き換えられるかの可否で Memory<T>
と ReadOnlyMemory<T>
がそれぞれ用意されています。
Span<T> & ReadOnlySpan<T>
メモリを意識したプログラミングをunsafeでない(=safeな)コンテキストで記述する際に用いられます。
配列だけを用いる処理と比べてかなり動作が高速で,うまく使えばメモリの節約も可能です。
しかし強力な分だけ用途は制限されており,ref struct
を除きフィールドに入れられません。
この制約を守らせるため,フィールドとなる可能性の生じるジェネリックへの指定(List<Span<int>>
など)やイテレーターメソッド内(コンパイル時に独自の型が定義されるため),非同期メソッド内などでは用いることができません。
こちらも内部の要素を書き換えられるかの可否で Span<T>
と ReadOnlySpan<T>
がそれぞれ用意されています。
T*
皆大好き[要出典]ポインタです。メモリ上の位置を表します。
加算や減算ができるためメモリ上の位置を示す数値として見た方が扱いやすいかもしれません。
メモリを直接いじることができ,やりたい放題できる反面,使い方をひとたび誤れば災厄が待ち構えているという代物でもあります。
そのためC#では unsafe
コンテキスト内でのみ用いることができ,この unsafe
コンテキスト自体もコンパイラかCSPROJファイルの設定をいじらないと使うことができません。
ref T
正確には参照変数自体は型ではありませんが,良い括り方が思いつかなかったのでここでは型と括ってしまっています。
こちらは T*
に制約をかけることで unsafe
じゃない場所でも使うことができるようにしたものです。
云わば合法ポインタ。Span<T>
の内部でも用いられている他,StringBuilder
や Dictionary<TKey, TValue>
といった速度の求められる場面で用いられています。
制約に関しては Span<T>
のものと概ね同じです。
まとめの表
上記の項をざっくりまとめたのが以下の表です。
型 | 型の種類 | サイズ情報 | ランダムアクセス | foreach | 制約 | 説明 |
---|---|---|---|---|---|---|
T[] |
class |
持つ | ○ | ○ | なし | 汎用的なもの。一番使われる |
Memory<T> |
struct |
持つ | × | × | なし |
Span<T> よりもパフォーマンスが落ちるものの制約がない |
Span<T> |
ref struct |
持つ | ○ | ○ | フィールド・型引数に使用不可 | パフォーマンス改善手段として重宝 |
T* |
ポインタ | 持たない | ○ | × |
unsafe コンテキスト内unmanagedな型のみ |
メモリ上の位置を表す。CやC++で当然の如く現れるアイツ |
ref T |
参照変数 | 持たない | × | × | ローカル定義の参照変数はreturn不可 イテレーター・非同期メソッドで使用不可 |
制約を設けて安全性を高めたポインタ |
※1 サイズ情報: T[].Length
のようにインスタンスがメモリ領域のサイズを表す情報を持つかどうか
※2 ランダムアクセス: array[1]
のようにインデックスから要素が取得可能かどうか
使い分け
これらのメモリ領域の取り回しは主に以下の3種類に大分できます。
- 基本:
T[]
- 部分配列をメモリアロケーションなく持ちたい:
Memory<T>
,Span<T>
- メモリの操作を前提のコードを書きたい:
T*
,ref T
基本的には T[]
を使用し,配列の一部分(或いは全体)を一時的に使いたい場合に Memory<T>
や Span<T>
として切り出します。
Memory<T>
や Span<T>
は配列の内容をコピーするのではなく,配列のメモリ上のデータを直接参照するためメモリアロケーションが起こらず,結果としてパフォーマンス向上が見込めます。
Memory<T>
と Span<T>
では,高速で処理を行える Span<T>
の方を可能な限り使用します。
Span<T>
を使いたいけれど,制約に引っかかってしまう(イテレーターメソッドの変数として定義する等)場合に,Memory<T>
の利用を考えましょう。
T*
と ref T
は,Span<T>
を使うよりも高速な処理を記述したい場合や,C/C++とのデータの受け渡しに使用する認識です(私自身ポインタ類を使う機会が殆どないので「認識」程度です)。
T*
vs ref T
では,基本的に ref T
を使用し,ref T
の制約に触れる場合に T*
を使うのが良いと思っています。
変換
T[]
→ Span<T>
や Memory<T>
→ Span<T>
などは簡単に変換できるようなメソッドorプロパティが用意されています。
一方,ref T
→ Span<T>
などのように System.Runtime.CompilerServices.Unsafe
クラスや System.Runtime.InteropServices.MemoryMarshal
クラスといったプログラマが良く考えて使わなければならない奥の手のような変換もあります。
以下の表に変換の可否をまとめました。
その先には変換のサンプルコードがあります。
変換前\変換先 | T[] | Memory<T> | Span<T> | T* | ref T |
---|---|---|---|---|---|
T[] | - | ◎ | ◎ | ● | ● |
Memory<T> | ○ | - | ◎ | × | × |
Span<T> | ○ | × | - | × | ● |
T* | × | × | × | - | ● |
ref T | × | × | ● | ● | - |
凡例
- ◎:簡単に変換できるメソッドorプロパティが用意されている
- ○:簡単に変換できるが,メモリアロケーションが起こる
- ●:
Unsafe
クラスやMemoryMarshal
クラス,unsafe
コンテキストといった使う際に注意の要る変換 - ×:直接の変換が用意されていないもの
T[]
から Memory<T>
var array = new int[3];
// コンストラクタで変換
Memory<int> memory1 = new Memory<int>(array);
ReadOnlyMemory<int> romemory1 = new ReadOnlyMemory<int>(array);
// 暗黙的な変換も可能
Memory<int> memory2 = array;
ReadOnlyMemory<int> romemory2 = array;
T[]
から Span<T>
var array = new int[3];
// コンストラクタで変換
Span<int> span1 = new Span<int>(array);
ReadOnlySpan<int> rmspan1 = new ReadOnlySpan<int>(array);
// 暗黙的な変換も可能
Span<int> span2 = array;
ReadOnlySpan<int> rospan2 = array;
T[]
から T*
var array = new int[3];
unsafe
{
// fixedでarrayのメモリ上位置を固定する
fixed (int* ptr = array)
{
// 処理
}
}
T[]
から ref T
using System.Runtime.InteropServices;
var array = new int[3];
// メソッドの返り値をrefで受け取るのを忘れないように
ref int reference = ref MemoryMarshal.GetArrayDataReference(array);
Memory<T>
から T[]
var memory = new Memory<int>(new[] { 1, 2, 3 });
var romemory = new ReadOnlyMemory<int>(new[] { 1, 2, 3 });
// メモリアロケーションあり
int[] array1 = memory.ToArray();
int[] array2 = romemory.ToArray();
Memory<T>
から Span<T>
var memory = new Memory<int>(new[] { 1, 2, 3 });
var romemory = new ReadOnlyMemory<int>(new[] { 1, 2, 3 });
Span<int> span = memory.Span;
ReadOnlySpan<int> rospan = romemory.Span;
Span<T>
から T[]
var span = new Span<int>(new[] { 1, 2, 3 });
var rospan = new ReadOnlySpan<int>(new[] { 1, 2, 3 });
// メモリアロケーションあり
int[] array1 = span.ToArray();
int[] array2 = rospan.ToArray();
Span<T>
から ref T
using System.Runtime.InteropServices;
var span = new Span<int>(new[] { 1, 2, 3 });
var rospan = new ReadOnlySpan<int>(new[] { 1, 2, 3 });
ref int reference1 = ref MemoryMarshal.GetReference(span);
ref int reference2 = ref MemoryMarshal.GetReference(rospan);
T*
から ref T
using System.Runtime.CompilerServices;
int val = 1;
unsafe
{
int* ptr = &val;
ref int reference = ref Unsafe.AsRef<int>(ptr);
}
ref T
から Span<T>
using System.Runtime.InteropServices;
int val = 1;
ref int reference = ref val;
// 要素数の指定が必要
Span<int> span = MemoryMarshal.CreateSpan(ref reference, 1);
ReadOnlySpan<int> rospan = MemoryMarshal.CreateReadOnlySpan(ref reference, 1);
ref T
から T*
using System.Runtime.CompilerServices;
int val = 1;
ref int reference = ref val;
unsafe
{
// メソッド返り値はvoid*につき,要キャスト
int* ptr = (int*)Unsafe.AsPointer(ref reference);
}
おまけ:List<T>
から Span<T>
内部の配列を Span<T>
としてぶっこ抜けます。
但し,要素数が変わっても Span<T>.Length
には反映されません。
また,リサイズに伴って List<T>
の持つ配列のインスタンスが変わった際には当該 Span<T>
は使い物にならなくなります。
とどのつまり使い捨てです。
using System.Runtime.InteropServices;
var list = new List<int>() { 1, 2, 3 };
Span<int> span = CollectionsMarshal.AsSpan(list);
あとがき
思い立ってQiitaを初めてみました。
C#関連の記事がメインになると思いますが,どうぞよろしゅうに。
最近メモリ消費量に超絶気を配ってソフトウェアを作る機会があったので,その過程で学んだ内容のアウトプットとしてこの記事を書きました。
unsafe黒魔術で最適化したい諸兄の役に立てば幸いです。