はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】Unsafe.WriteUnalignedとMemoryMarshal.GetReferenceとSpanを組み合わせてメモリのコピーを行う

はじめに

今回はUnsafe.WriteUnalignedMemoryMarshal.GetReferenceSpanを組み合わせてメモリのコピーを行ってみようという記事になります。

// サンプルコード
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 の値を書き込みます。

learn.microsoft.com

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 にあるスパンの要素の参照を返します。

learn.microsoft.com

Spanの先頭ポインタを返すと言うことですね。分かりやすい。

メモリのコピーを行う

Spanの先頭要素からintbyte[]を書き込むサンプルコードを載せておきます。

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要素目からデータを書き込んでいます。