C#:バイナリ操作でよく使うAPIたち
C# でバイナリ操作を行う際によく使う API をまとめています。
確保/解放
スタック領域を確保
stackalloc でスタック領域にバッファを確保します。この確保・解放コストは非常に低く抑えられます。
Span<byte> buffer = stackalloc byte[1024];
スタック領域の大きさは実行環境や実装によりますが、例えば Windows ではスレッドあたり1MBとかになります。stackalloc での確保量が大きいと、メソッド呼び出しの深さなどによってはスタックオーバーフローを起こす可能性があるので注意が必要です。
配列プールからの確保
ArrayPool<T> はマネージド配列のプールです。stackalloc と比べると確保コストは高めですが、容量やライフタイムの制限なく使えるため、長時間使用するバッファや大きなバッファを確保するのに適しています。
using System;
using System.Buffers;
var buffer = ArrayPool<byte>.Shared.Rent(1024);
try
{
DoSomething(buffer);
}
finally
{
pool.Return(buffer);
}
アンマネージドメモリの確保
Marshal.AllocHGlobal() はアンマネージドメモリを確保します。確保したメモリは Marshal.FreeHGlobal() で解放する必要があります。
using System;
using System.Runtime.InteropServices;
IntPtr ptr = Marshal.AllocHGlobal(1024);
Span<byte> buffer = new Span<byte>((void*)ptr, 1024);
try
{
DoSomething(buffer);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
値の読み書き
エンディアンを指定した読み書き
Span<byte> 等から値を読み書きする際、通常は実行環境のエンディアンが使用されます(x86, ARM系など多くの環境ではリトルエンディアン)。
BinaryPrimitives クラスを使用すると実行環境によらず指定したエンディアンで値を読み書きすることができます。
using System.Buffers.Binary;
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
int value = BinaryPrimitives.ReadInt32LittleEndian(buffer);
Span<T> から ref T を取得
MemoryMarshal.GetReference() を使うと Span<T> が指す先頭の要素への ref T を取得できます。
using System.Runtime.InteropServices;
ref var element = ref MemoryMarshal.GetReference(buffer);
ref T から Span<T> を作成
MemoryMarshal.CreateSpan() は ref T を先頭とする Span<T> を作成します。
この方法で作成した Span<T> は静的なスコープのチェックを受けず、もとの ref T のスコープが終了しても有効なままとなるため注意が必要です。
using System.Runtime.InteropServices;
var span = MemoryMarshal.CreateSpan(ref value, length);
Span<T>の型変換
MemoryMarshal.Cast() は Span<T> の型変換を行います。内容はコピーされず、同じメモリを参照する Span<T> が返されます。
using System;
using System.Runtime.InteropServices;
Span<byte> buffer = GetBytes();
Span<int> intBuffer = MemoryMarshal.Cast<byte, int>(buffer);
int value = intBuffer[0];
アラインメントを無視した値の読み書き
MemoryMarshal.Read() / MemoryMarshal.Write() は Span<byte> に対して T 型の値を読み書きします。
アドレスが T のアラインメントに合わない場合でも正しく読み書きできます。
using System;
using System.Runtime.CompilerServices;
Span<byte> buffer = GetBytes();
int value = MemoryMarshal.Read<int>(buffer);
MemoryMarshal.Write(buffer, ref value);
近い機能を持つ Unsafe.ReadUnaligned() / Unsafe.WriteUnaligned() もあり、こちらは ref byte に対して T 型の値を読み書きします。
入力する型が異なる以外は基本的に MemoryMarshal.Read() / MemoryMarshal.Write() と同じです。
構造体レイアウトの指定
Span<byte> から既知のレイアウトで値を読み取りたい場合は構造体のレイアウトとして定義してしまうのが便利です。
using System;
using System.Runtime.InteropServices;
Span<byte> buffer = GetBytes();
S value = MemoryMarshal.Read<S>(buffer);
[StructLayout(LayoutKind.Explicit, Size = 32)]
public struct S
{
[FieldOffset(0)]
public byte a;
[FieldOffset(2)]
public int b;
}
連続領域のコピー
Buffer.BlockCopy() / Buffer.MemoryCopy() は配列やポインタの連続領域を高速にコピーします。内部的には memmove が使用されています。
using System;
Buffer.BlockCopy(source, 0, destination, 0, length);
ReadOnlyMemory<T> から配列を取得
MemoryMarshal.TryGetArray() は ReadOnlyMemory<T> が指している配列とインデックス、長さを ArraySegment<T> として取得します。
using System;
using System.Runtime.InteropServices;
Memory<byte> memory = GetMemory();
if (MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment))
{
byte[] array = segment.Array;
int offset = segment.Offset;
int length = segment.Count;
}
I/O
I/O についてはこちらの資料が詳しいです。
System.IO.Pipelines: パイプラインによるハイパフォーマンスI/O
ストリーム処理を効率よく行うためのパッケージです。Stream のモダンな代替ともいえます。
Span<byte> や Memory<byte> をベースとした API、アロケーションやコピーの削減など、パフォーマンスを意識した設計になっています。
ReadOnlySequence<T>: 断片化したバッファからの読み込み
System.IO.Pipelines におけるパイプからのデータの読み取りに使用されます。
ストリーム処理では、データが断片化した状態で逐次的に入力されることがよくあり、それらを常に一つの配列 byte[] に結合して処理するのは非効率的です。 ReadOnlySequence<byte> は断片化した状態のままデータ列を表現し、内容を読みとることができます。
IBufferWriter<T>: 対象バッファへの直接書き込み
System.IO.Pipelines におけるパイプへのデータの書き込みに使用されます。
ストリーム処理におけるデータの書き込みについても、毎度 byte[] などにデータの断片を詰めて受け渡すよりも、書き込み先のバッファを Span<byte> として取得し直接書き込むほうが効率的です。IBufferWriter<byte> はこのような書き込み操作を実現するためのインターフェースです。
Discussion