構造体からポインタ(バイト配列)への変換

色々方法はあります。これから、思いついたものを列挙していきます。
なお、今回変換する構造体とそのインスタンスは以下のようなものとします。

[StructLayout(LayoutKind.Sequential)]
struct Hoge
{
    public int A;
    public int B;
}
Hoge obj = new Hoge{ A = 12345, B = 67890 };

Test1 : Marshal.StructureToPtr

そのものズバリなメソッドです。予め確保しておいたメモリに構造体のデータをコピーします。

int size = Marshal.SizeOf(obj);
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(obj, ptr, false);


また、ポインタではなくbyte配列に対しコピーしたい場合はこのようになります。

int size = Marshal.SizeOf(obj);
IntPtr ptr = Marshal.AllocHGlobal(size);
byte[] bytes = new byte[size];
Marshal.StructureToPtr(obj, ptr, false);
Marshal.Copy(ptr, bytes, 0, size);

但しこれではバッファリングのためにメモリを倍食っています。それが嫌ならGCHandleでbyte配列のポインタを取得し、直接そこに書き込むこともできます。

int size = Marshal.SizeOf(obj);
byte[] bytes = new byte[size];
GCHandle gch = GCHandle.Alloc(bytes, GCHandleType.Pinned);
Marshal.StructureToPtr(obj, gch.AddrOfPinnedObject(), false);
gch.Free();

これ以下の例でも、Marshal.AllocHGlobalで確保したメモリへ出力する例と、byte配列に出力する例を併記していきます。

Test2 : CopyMemory

Win32APIのCopyMemoryDllImportで定義した上で、それを使ってメモリのコピーを行います。

[DllImport("kernel32.dll")]
static extern void CopyMemory(IntPtr dst, IntPtr src, int size);
int size = Marshal.SizeOf(obj);
IntPtr ptr = Marshal.AllocHGlobal(size);
GCHandle gch = GCHandle.Alloc(obj, GCHandleType.Pinned);
CopyMemory(ptr, gch.AddrOfPinnedObject(), size);
gch.Free();


byte配列での例はこちら。

int size = Marshal.SizeOf(obj);
byte[] bytes = new byte[size];
GCHandle gchObj = GCHandle.Alloc(obj, GCHandleType.Pinned);
GCHandle gchBytes = GCHandle.Alloc(bytes, GCHandleType.Pinned);
CopyMemory(gchBytes.AddrOfPinnedObject(), gchObj.AddrOfPinnedObject(), size);
gchObj.Free();
gchBytes.Free();

ここで、GCHandleではなくunsafeコードでfixedを使った場合はこのようになります。

[DllImport("kernel32.dll")]
static extern unsafe void CopyMemory(void* dst, void* src, int size);
unsafe
{
    int size = sizeof(Hoge);
    byte[] bytes = new byte[size];
    fixed (Hoge* src = &obj)
    fixed (byte* dst = bytes)
    {
        CopyMemory(dst, src, size);
    }
}

Test3 : ポインタ

ポインタ演算で直接コピーします。

unsafe
{
    int size = sizeof(Hoge);
    IntPtr ptr = Marshal.AllocHGlobal(size);
    *(Hoge*)ptr = obj;
}


byte配列の場合はこちら。

unsafe
{
    int size = sizeof(Hoge);
    byte[] bytes = new byte[size];
    fixed (byte* pbytes = bytes)
    {
        *(Hoge*)pbytes = obj;
    }
}

処理速度

上記の方法それぞれをループで777777回実行したときの処理速度を測ってみました。

手法 出力先 処理時間(ms)
Marshal.StructureToPtr IntPtr 197
Marshal.StructureToPtr byte[] 307
CopyMemory IntPtr 395
CopyMemory byte[] (GCHandle) 510
CopyMemory byte[] (fixed) 68
ポインタ IntPtr 90
ポインタ byte[] 27

感想

  • やっぱりunsafeは速い
    • 但し、特定の型固有のコードしか書けない。ジェネリックのポインタは無理。
    • Marshal.SizeOf より sizeof の方が速いというのも効いてそう
  • GCHandleが結構重いらしい

ということで、とことん速さを追求したければポインタを使えばいいし、汎用的なコードを書きたいとか特に速さはこだわらないというのであれば、おとなしくMarshal.StructureToPtrで良いと思います。


他にも例えば、BitConverter.GetBytesメソッドを使って、構造体のメンバ1つ1つを地道にバイト列に変換する・・・なんて方法も考えられます。面倒ではありますが、変換前のint等の値と変換後のbyte[]を見比べたりしてるとエンディアンの勉強になったりしますので、一度やってみるとよいと思います。

最後に

今回のテストに使ったコードを載せておきます。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace CsTest
{
    delegate void Action();

    [StructLayout(LayoutKind.Sequential)]
    struct Hoge
    {
        public int A;
        public int B;
    }

    class Program
    {
        const int Count = 777777;
        static Hoge obj = new Hoge { A = 12345, B = 67890 };

        [DllImport("kernel32.dll")]
        static extern void CopyMemory(IntPtr dst, IntPtr src, int size);
        [DllImport("kernel32.dll")]
        static extern unsafe void CopyMemory(void* dst, void* src, int size);

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Console.WriteLine("Test1_Pointer     : {0}ms", MeasureTime(Test1_Pointer).Milliseconds);
            Console.WriteLine("Test2_Pointer     : {0}ms", MeasureTime(Test2_Pointer).Milliseconds);
            Console.WriteLine("Test3_Pointer     : {0}ms", MeasureTime(Test3_Pointer).Milliseconds);
            Console.WriteLine("Test1_ByteArray   : {0}ms", MeasureTime(Test1_ByteArray).Milliseconds);
            Console.WriteLine("Test2_ByteArray_1 : {0}ms", MeasureTime(Test2_ByteArray_1).Milliseconds);
            Console.WriteLine("Test2_ByteArray_2 : {0}ms", MeasureTime(Test2_ByteArray_2).Milliseconds);
            Console.WriteLine("Test3_ByteArray   : {0}ms", MeasureTime(Test3_ByteArray).Milliseconds);            
            Console.Read();
        }

        static void Test1_Pointer()
        {
            int size = Marshal.SizeOf(obj);
            IntPtr ptr = Marshal.AllocHGlobal(size);
            Marshal.StructureToPtr(obj, ptr, false);
            Marshal.FreeHGlobal(ptr);
        }
        static void Test2_Pointer()
        {
            int size = Marshal.SizeOf(obj);
            IntPtr ptr = Marshal.AllocHGlobal(size);
            GCHandle gch = GCHandle.Alloc(obj, GCHandleType.Pinned);
            CopyMemory(ptr, gch.AddrOfPinnedObject(), size);
            gch.Free();
            Marshal.FreeHGlobal(ptr);
        }
        static unsafe void Test3_Pointer()
        {
            int size = sizeof(Hoge);
            IntPtr ptr = Marshal.AllocHGlobal(size);
            *(Hoge*)ptr = obj;
            Marshal.FreeHGlobal(ptr);
        }

        static void Test1_ByteArray()
        {
            int size = Marshal.SizeOf(obj);
            byte[] bytes = new byte[size];
            GCHandle gch = GCHandle.Alloc(bytes, GCHandleType.Pinned);
            Marshal.StructureToPtr(obj, gch.AddrOfPinnedObject(), false);
            gch.Free();
        }
        static void Test2_ByteArray_1()
        {
            int size = Marshal.SizeOf(obj);
            byte[] bytes = new byte[size];
            GCHandle gchObj = GCHandle.Alloc(obj, GCHandleType.Pinned);
            GCHandle gchBytes = GCHandle.Alloc(bytes, GCHandleType.Pinned);
            CopyMemory(gchBytes.AddrOfPinnedObject(), gchObj.AddrOfPinnedObject(), size);
            gchObj.Free();
            gchBytes.Free();
        }
        static unsafe void Test2_ByteArray_2()
        {
            int size = sizeof(Hoge);
            byte[] bytes = new byte[size];
            fixed (Hoge* src = &obj)
            fixed (byte* dst = bytes)
            {
                CopyMemory(dst, src, size);
            }
        }
        static unsafe void Test3_ByteArray()
        {
            int size = sizeof(Hoge);
            byte[] bytes = new byte[size];
            fixed (byte* pbytes = bytes)
            {
                *(Hoge*)pbytes = obj;
            }
        }

        /// <summary>
        /// 指定したデリゲートの実行にかかった時間を計測
        /// </summary>
        /// <param name="action"></param>
        /// <returns></returns>
        static TimeSpan MeasureTime(Action action)
        {
            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < Count; i++)
            {
                action();
            }
            watch.Stop();
            return watch.Elapsed;
        }
    }
}