はじめに
今回はUnsafe.WriteUnaligned
とMemoryMarshal.GetReference
とSpan
を組み合わせてメモリのコピーを行ってみようという記事になります。
// サンプルコード public static void Main(string[] args) { int value = 999; // 231, 3, 0, 0 Console.WriteLine(string.Join(",", BitConverter.GetBytes(value))); // ------------ // Managed Heap // ------------ Span<byte> managedArray = new byte[sizeof(int)].AsSpan(); Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(managedArray), value); // 231, 3, 0, 0 Console.WriteLine(string.Join(",", managedArray.ToArray())); // ------------ // Unmanaged Heap // ------------ unsafe { IntPtr p = Marshal.AllocHGlobal(sizeof(int)); Span<byte> unmanagedArray = new Span<byte>((byte*)p, sizeof(int)); Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(unmanagedArray), value); // 231, 3, 0, 0 Console.WriteLine(string.Join(",", unmanagedArray.ToArray())); Marshal.FreeHGlobal(p); } // ------------ // stack // ------------ Span<byte> stackArray = stackalloc byte[sizeof(int)]; Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(stackArray), value); // 231, 3, 0, 0 Console.WriteLine(string.Join(",", stackArray.ToArray())); }
概要
まずはそれぞれのAPIの概要について説明します。
Unsafe.WriteUnaligned
public static void WriteUnaligned<T> (ref byte destination, T value);
宛先アドレスのアーキテクチャに依存する配置を想定せずに、指定された場所に型 T の値を書き込みます。
Unsafe.WriteUnaligned
は.NET Core 3.0
からの対応なのでC# 8.0
からという認識で大丈夫なはずです。(ちょっと自信がない...)
C# 言語のバージョン管理 - C# ガイド | Microsoft Learn
またUnsafe
というクラス名ですが、unsafe
で囲ってあげなくても利用可になります。
MemoryMarshal.GetReference
public static ref T GetReference<T> (ReadOnlySpan<T> span); public static ref T GetReference<T> (Span<T> span);
インデックス 0 にあるスパンの要素の参照を返します。
Span
の先頭ポインタを返すと言うことですね。分かりやすい。
メモリのコピーを行う
Span
の先頭要素からint
のbyte[]
を書き込むサンプルコードを載せておきます。
public static void Main(string[] args) { int value = 999; // 231, 3, 0, 0 Console.WriteLine(string.Join(",", BitConverter.GetBytes(value))); // ------------ // Managed Heap // ------------ Span<byte> managedArray = new byte[sizeof(int)].AsSpan(); Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(managedArray), value); // 231, 3, 0, 0 Console.WriteLine(string.Join(",", managedArray.ToArray())); // ------------ // Unmanaged Heap // ------------ unsafe { IntPtr p = Marshal.AllocHGlobal(sizeof(int)); Span<byte> unmanagedArray = new Span<byte>((byte*)p, sizeof(int)); Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(unmanagedArray), value); // 231, 3, 0, 0 Console.WriteLine(string.Join(",", unmanagedArray.ToArray())); Marshal.FreeHGlobal(p); } // ------------ // stack // ------------ Span<byte> stackArray = stackalloc byte[sizeof(int)]; Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(stackArray), value); // 231, 3, 0, 0 Console.WriteLine(string.Join(",", stackArray.ToArray())); }
雑にコードを書こうとすると遅かったり、無駄なAllocation
を発生させてしまったりするので、このコードの書き方はかなり良いコードだと思います。
MessagePackでの実用例
CEDEC2023での講演にてMessagePack
というC#のシリアライザーでの活用例が紹介されていました。
github.com
// uint16 msgpack code Unsafe.WriteUnaligned(ref dest[0], (byte)0xcd); // 先頭に型識別子 // Write value as BidEndian var temp = BinaryPrimitives.ReverseEndianness((ushort)value); Unsafe.WriteUnaligned(ref dest[1], temp); // 3, e7 Console.WriteLine(string.Join(",", BitConverter.GetBytes(temp).Select(x => x.ToString("x")))); // cd, 3, e7 Console.WriteLine(string.Join(",", dest.Select(x => x.ToString("x"))));
一部分かりやすいようにログ出力を足しています。
dest
というSpan
に対して、最初にcd
という型識別子を書き込み、2要素目からデータを書き込んでいます。