.NET Frameworkにおいてバッファなどのバイト列を扱うクラスにはMemoryStreamが存在しますが、Bufferクラスのメソッドを用いることによってプリミティブ型の配列を直接バイト列として操作することができます。 例えば、int型の配列に対してバイト単位で値の取得・設定をしたり、memcpyのように配列の一部をバイト列としてコピーすることができます。

こういった操作はシフト演算やポインタを介して行うこともできますが、Bufferクラスを用いればポインタ、つまりunsafeコンテキストによるアンセーフコードを一切用いずに記述することができます。

Bufferクラス

Bufferクラスは配列をバッファとして扱い、バイト単位での操作を行うためのメソッドを持ったクラスです。 例えば、int型配列に対して、1バイト単位で読み取り・書き込みをしたり、memcpymemcpy_sのようにメモリブロックのコピーを行うためのメソッドなどが提供されています。

Bufferクラスは、リングバッファなどのバッファ機能を実装するクラスではありません。 バッファとなるメモリ領域を確保し、確保した領域に読み書きする目的にはSafeBufferクラス、ストリームへの読み書きに対してバッファリングを行う目的にはBufferedStreamクラスなど、バッファ機能を扱うクラスは別に存在します。

Bufferクラスで扱える配列型

Bufferクラスはプリミティブ型の配列に対してのみ用いることができます。 その他の型の配列、構造体やクラスの配列に対しては用いることができません。 .NET Frameworkではbyte, int, char, double, IntPtrなどの型がプリミティブ型として扱われます。 ここで言うプリミティブ型とは言語の組み込み型とは異なります。 例えばdecimalint?は組み込み型ですがプリミティブ型ではありません。

Bufferクラスで扱えるプリミティブ型
数値型 整数型 sbyte, short, int, long
byte, ushort, uint, ulong
実数型 single, double
ポインタ型 IntPtr, UIntPtr
文字型 char
ブール型 bool

プリミティブ型についてや型の分類等について詳しくは型の種類・サイズ・精度・値域を参照してください。

配列のバイト列操作

バイト単位での値の設定・取得 (GetByteメソッド・SetByteメソッド)

Buffer.GetByteメソッドおよびBuffer.SetByteメソッドを用いると、プリミティブ型配列に対してバイト単位での値の取得・設定を行うことができます。 これは、任意のプリミティブ型配列をバイト配列と同様に扱うことができることを意味します。

例えば配列int[]をポインタbyte*にキャストしてバイト列として扱うような操作は、このメソッドによって代用することができます。 したがって、GetByteメソッド・SetByteメソッドを用いれば、unsafeなコードの記述を避けることができます。

Buffer.GetByteメソッドを使って配列をバイト列として扱い、値を取得する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    int[] arr = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f};

    // (配列の内容を16進数で表示)
    Console.WriteLine(string.Join(" ", arr.Select(e => e.ToString("X8"))));

    // 配列arrの先頭から4バイト目(=1番目の要素の0バイト目)の値を取得する
    // ( "*((*byte)arr + 4)" に相当する操作)
    Console.WriteLine("GetByte(arr, 4) = {0:X2}", Buffer.GetByte(arr, 4));

    // 配列arrの先頭から7バイト目(=1番目の要素の3バイト目)の値を取得する
    // ( "*((*byte)arr + 7)" に相当する操作)
    Console.WriteLine("GetByte(arr, 7) = {0:X2}", Buffer.GetByte(arr, 7));
  }
}
実行結果
00010203 04050607 08090A0B 0C0D0E0F
GetByte(arr, 4) = 07
GetByte(arr, 7) = 04

上記の例において、配列内の各値はリトルエンディアンで格納されている点に注意してください。 配列arrのバイト表現を図式化すると次にようになります。

配列arrの内容
オフセット(バイト) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
要素値 0x00010203 0x04050607 0x08090A0B 0x0C0D0E0F
バイト値 0x03 0x02 0x01 0x00 0x07 0x06 0x05 0x04 0x0B 0x0A 0x09 0x08 0x0F 0x0E 0x0D 0x0C

Buffer.SetByteメソッドを使って配列をバイト列として扱い、値を設定する
using System;

class Sample {
  static void Main()
  {
    int[] arr = {0, 1, 2, 3};

    Console.WriteLine(string.Join(", ", arr));

    // 配列arrの1バイト目に値0x01を設定する
    // ( "*((*byte)arr + 1) = 0x01" に相当する操作)
    Buffer.SetByte(arr, 1, 0x01);

    Console.WriteLine(string.Join(", ", arr));
  }
}
実行結果
0, 1, 2, 3
256, 1, 2, 3

配列のバイト数の取得 (ByteLengthメソッド)

GetByteメソッド・SetByteメソッドに加えて、Buffer.ByteLengthメソッドを使うと配列のバイト数を取得することができます。 配列のバイト数は、sizeof演算子で型のサイズを求め、配列の長さとの積をとることでも求めることができますが、このメソッドでは配列の型が何であるかを意識しなくてもメソッド呼び出しのみで配列のバイト数を求めることができます。

Buffer.ByteLengthメソッドを使って配列のバイト数を求める
using System;

class Sample {
  static void Main()
  {
    byte[] bytes = {0, 1, 2, 3};
    short[] shorts = {0, 1, 2, 3};
    int[] ints = {0, 1, 2, 3};

    // Buffer.ByteLengthメソッドで各配列のバイト数を求める
    // (sizeof(T) * arr.Lengthとしても求められるが、このメソッドでは配列の型を意識する必要がなく
    // 配列を単なるバイト列とみなしてそのサイズを取得できる)
    Console.WriteLine(Buffer.ByteLength(bytes));
    Console.WriteLine(Buffer.ByteLength(shorts));
    Console.WriteLine(Buffer.ByteLength(ints));
  }
}
実行結果
4
8
16

GetByteメソッド・SetByteメソッドと同様、ByteLengthメソッドもプリミティブ型配列に対してのみ用いることができます。

C#におけるsizeof演算子については構造体のサイズ §.sizeof演算子 (C#)を参照してください。

Bufferクラスによるバイト列操作の例

以下ではGetByteメソッド・SetByteメソッドを使った例を掲載します。 いずれもパフォーマンス上の観点からはポインタ演算やシフト演算を用いた方がよいものですが、GetByteメソッド・SetByteメソッドを用いることでunsafeコンテキストを使った記述を排除できる=マネージドコードのみを用いても記述できることを示しています。

Buffer.GetByteメソッドを使って配列のバイト表現を表示する
using System;

class Sample {
  static void Main()
  {
    short[] arr = new short[] {0, 1, 2, 3};

    // 配列のバイト数を取得する
    var sizeOfArray = Buffer.ByteLength(arr);
    // sizeof演算子を使って次のように求めることもできる
    // (ただし、配列の型は既知である必要がある)
    //var sizeOfArray = sizeof(short) * arr.Length

    // Buffer.GetByteメソッドを使って配列のバイト表現を出力する
    for (var offset = 0; offset < sizeOfArray; offset++) {
      Console.Write("0x{0:x2} ", Buffer.GetByte(arr, offset));
    }
    Console.WriteLine();

    // 上記の処理をポインタを使って記述すると次のようになる
    // (Buffer.GetByteメソッドを使えば、unsafeコンテキストやポインタを
    // 用いずにこのような処理を記述することができる)
    unsafe {
      fixed (void* ptr = arr) {
        for (var offset = 0; offset < sizeOfArray; offset++) {
          Console.Write("0x{0:x2} ", *((byte*)ptr + offset));
        }
        Console.WriteLine();
      }
    }
  }
}
実行結果
0x00 0x00 0x01 0x00 0x02 0x00 0x03 0x00 
0x00 0x00 0x01 0x00 0x02 0x00 0x03 0x00 

Buffer.SetByteメソッドを使って32bit整数値のエンディアンネスを逆転する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    uint[] arr = new uint[] {0x01234567, 0xff00ff00, 0xdeadbeef};

    Console.WriteLine(string.Join(" ", arr.Select(e => e.ToString("X8"))));

    for (var offset = 0; offset < Buffer.ByteLength(arr); offset += 4) {
      byte b0 = Buffer.GetByte(arr, offset + 0);
      byte b1 = Buffer.GetByte(arr, offset + 1);
      byte b2 = Buffer.GetByte(arr, offset + 2);
      byte b3 = Buffer.GetByte(arr, offset + 3);

      Buffer.SetByte(arr, offset + 0, b3);
      Buffer.SetByte(arr, offset + 1, b2);
      Buffer.SetByte(arr, offset + 2, b1);
      Buffer.SetByte(arr, offset + 3, b0);
    }

    Console.WriteLine(string.Join(" ", arr.Select(e => e.ToString("X8"))));

    // エンディアンネスの変換にはSystem.Net.IPAddressクラスのNetworkToHostOrderメソッド
    // またはHostToNetworkOrderメソッドを使うことができる
    for (var index = 0; index < arr.Length; index++) {
      arr[index] = (uint)System.Net.IPAddress.NetworkToHostOrder((int)arr[index]);
    }
  }
}
実行結果
01234567 FF00FF00 DEADBEEF
67452301 00FF00FF EFBEADDE

Buffer.SetByteメソッドを使って32bitRGBカラーの各ピクセルを飽和加算する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    int[] pixels1 = new int[] {0x00808080, 0x0060a0c0, 0x00c09060, 0x00206040}; // 32bit RGB 4pixels
    int[] pixels2 = new int[] {0x00997788, 0x00444444, 0x00666666, 0x00333333}; // 32bit RGB 4pixels

    Console.WriteLine(string.Join(" ", pixels1.Select(pixel => pixel.ToString("X8"))));
    Console.WriteLine(string.Join(" ", pixels2.Select(pixel => pixel.ToString("X8"))));

    int[] pixels = new int[4];

    for (var offset = 0; offset < Buffer.ByteLength(pixels); offset += 4) {
      // R, G, Bの各色素を飽和加算する
      byte b = (byte)Math.Min((int)Buffer.GetByte(pixels1, offset + 0) + (int)Buffer.GetByte(pixels2, offset + 0), 0xff); // 各intの0バイト目
      byte g = (byte)Math.Min((int)Buffer.GetByte(pixels1, offset + 1) + (int)Buffer.GetByte(pixels2, offset + 1), 0xff); // 各intの1バイト目
      byte r = (byte)Math.Min((int)Buffer.GetByte(pixels1, offset + 2) + (int)Buffer.GetByte(pixels2, offset + 2), 0xff); // 各intの2バイト目

      // 結果を配列へ格納する
      Buffer.SetByte(pixels, offset + 0, b); // 各intの0バイト目
      Buffer.SetByte(pixels, offset + 1, g); // 各intの1バイト目
      Buffer.SetByte(pixels, offset + 2, r); // 各intの2バイト目
      Buffer.SetByte(pixels, offset + 3, 0); // 各intの3バイト目
    }

    Console.WriteLine(string.Join(" ", pixels.Select(pixel => pixel.ToString("X8"))));
  }
}
実行結果
00808080 0060A0C0 00C09060 00206040
00997788 00444444 00666666 00333333
00FFF7FF 00A4E4FF 00FFF6C6 00539373

バイト列のコピー

Bufferクラスでは、配列に対してmemcpymemmoveに相当する操作を行うためのBuffer.BlockCopyBuffer.MemoryCopyといったメソッドが用意されています。 これらのメソッドを使うと、配列を単なるバイト列やメモリブロックとして扱い、配列の型によらずバイト単位でのコピーを行うことができます。

BlockCopyメソッド

Buffer.BlockCopyメソッドは、配列をバイト列とみなしてその内容を別の配列へとコピーします。 memcpymemmoveに相当する操作は、このBlockCopyメソッドか、後述のMemoryCopyメソッドを使うことで実現できます。

配列をコピーするという点ではBuffer.BlockCopyメソッドとArray.Copyメソッドは似ていますが、BlockCopyメソッドは要素単位ではなくバイト単位でのコピーを行います。 引数の指定順序も同じですが、各引数の意味は以下のように異なるため、Array.CopyメソッドとBuffer.BlockCopyメソッドを単純に置き換えて使うことはできません。

Buffer.BlockCopyとArray.Copyにおける引数の意味の違い
メソッド呼び出し メソッドの動作
Buffer.BlockCopy(source, 0, dest, 2, 6) 配列sourceオフセット0から6バイト分を配列destオフセット2以降にコピーする
Array.Copy(source, 0, dest, 2, 6) 配列sourceインデックス0から6要素分を配列destインデックス2以降にコピーする
Buffer.BlockCopyメソッドを使ってuint配列の一部をバイト列として他の配列にコピーする
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    uint[] source = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f};
    uint[] dest   = {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};

    // (配列の内容を16進数で表示)
    Console.WriteLine(string.Join(" ", source.Select(e => e.ToString("X8"))));
    Console.WriteLine(string.Join(" ", dest.Select(e => e.ToString("X8"))));
    Console.WriteLine();

    // 配列sourceのオフセット0から6バイト分を配列destのオフセット2以降にコピーする
    Buffer.BlockCopy(source, 0, dest, 2, 6);

    Console.WriteLine(string.Join(" ", dest.Select(e => e.ToString("X8"))));
  }
}
実行結果
00010203 04050607 08090A0B 0C0D0E0F
FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF

0203FFFF 06070001 FFFFFFFF FFFFFFFF

上記の例において、配列内の各値はリトルエンディアンで格納されている点に注意してください。 配列sourcedestの内容を図式化すると次にようになります。

コピー元配列sourceの内容
オフセット(バイト) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
要素値 0x00010203 0x04050607 0x08090A0B 0x0C0D0E0F
バイト値 0x03 0x02 0x01 0x00 0x07 0x06 0x05 0x04 0x0B 0x0A 0x09 0x08 0x0F 0x0E 0x0D 0x0C
コピー後の配列destの内容
オフセット(バイト) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
要素値 0x0203FFFF 0x06070001 0xFFFFFFFF 0xFFFFFFFF
バイト値 0xFF 0xFF 0x03 0x02 0x01 0x00 0x07 0x06 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF

このように、BlockCopyメソッドではコピー元・コピー先をバイト単位で指定することができるため、要素をまたがる位置からの/へのコピーをすることもできます。


さらにBlockCopyメソッドでは、コピー先がプリミティブ型配列であれば異なる型の配列へのコピーをすることもできます。 そのため、BlockCopyメソッドを使うことによって配列のバイト単位での内容を維持したまま別の型の配列へと変換することができます。

BlockCopyメソッドを使ってバイト列を維持したままuint[]からbyte[]へ変換する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    uint[] source = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f};
    byte[] dest = new byte[16];

    // (配列の内容を16進数で表示)
    Console.WriteLine(string.Join(" ", source.Select(e => e.ToString("X8"))));
    Console.WriteLine(string.Join(" ", dest.Select(e => e.ToString("X2"))));
    Console.WriteLine();

    // 配列sourceのオフセット0から16バイト分を配列destのオフセット0以降にコピーする
    // (バイト単位でコピーしてuint[]からbyte[]へ型変換する)
    Buffer.BlockCopy(source, 0, dest, 0, 16);

    Console.WriteLine(string.Join(" ", dest.Select(e => e.ToString("X2"))));
  }
}
実行結果
00010203 04050607 08090A0B 0C0D0E0F
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

03 02 01 00 07 06 05 04 0B 0A 09 08 0F 0E 0D 0C

この例のように、BlockCopyメソッドではuintbyteのように型としては互換性のない変換(縮小変換となる変換)でもコピーを行うことができます。 Array.Copyメソッドでは、uint[]byte[]へのコピーのように縮小変換を伴うコピーを行おうとすると例外ArrayTypeMismatchExceptionがスローされます。

型の変換の種類、縮小変換については基本型の型変換を参照してください。


BlockCopyメソッドでは、コピー元とコピー先の領域がオーバーラップしていても(重なった状態でも)、適切にコピーが行われます。 オーバーラップした部分のコピー中の状態がコピー先に反映されることはありません。 (この動作は、Array.Copyメソッドでも同様です)

Buffer.BlockCopyメソッドでコピー範囲がオーバーラップするコピーを行う
using System;
using System.Linq;
using System.Text;

class Sample {
  static void Main()
  {
    byte[] bytes = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};

    Console.WriteLine(string.Join(" ", bytes.Select(e => e.ToString("X2"))));

    // bytesのオフセット0以降にbytesのオフセット2以降から4バイト分をコピーする
    // (コピー元とコピー先の範囲がオーバーラップしていても、コピー途中の状態が反映されることはない)
    Buffer.BlockCopy(bytes, 0, bytes, 2, 4);

    // Array.Copyメソッドでも同じ動作となる
    //Array.Copy(bytes, 0, bytes, 2, 4);

    Console.WriteLine(string.Join(" ", bytes.Select(e => e.ToString("X2"))));
  }
}
実行結果
00 01 02 03 04 05 06 07
00 01 00 01 02 03 06 07

Buffer.BlockCopyメソッドとArray.Copyメソッドの相違点を整理すると次のようになります。

Buffer.BlockCopyとArray.Copyの相違点
操作 Buffer.BlockCopy Array.Copy
コピーの単位 バイト単位 要素単位
要素をまたがる位置からの/へのコピー 不可
異なる型の配列へのコピー プリミティブ型であれば可 (byteint,intdoubleなど)
それ以外の型の場合はArgumentException
プリミティブ型かつ型に互換性がある場合のみ可 (byteint,intdoubleなど)
それ以外の型の場合はArrayTypeMismatchException
(詳細:配列操作 §.異なる型の配列への複写)
オーバーラップする範囲のコピー 適切にコピーされる 適切にコピーされる

BlockCopyメソッド以外でメモリブロックのコピーを行う方法、またそのパフォーマンスについては§.メモリブロックのコピー手法とパフォーマンスで別途解説します。

MemoryCopyメソッド

Buffer.MemoryCopyは.NET Framework 4.6より利用可能になったメソッドで、名前が示すとおりmemcpyに相当するメソッドです(厳密には、よりセキュアな関数memcpy_sに近い)。

MemoryCopyメソッドはBlockCopyメソッドとは異なり、コピー元・コピー先の配列をポインタで指定します。 MemoryCopyメソッドでは、バッファオーバーランを起こす可能性を低減するために、まず引数destinationSizeInBytesにコピー先の使用可能なサイズを指定します。 その上で、引数sourceBytesToCopyにコピーするバイト数を指定します。

MemoryCopyメソッドではコピー元・コピー先の配列とオフセットをポインタとして指定するため、unsafeコンテキストで用いる必要があります。

MemoryCopyメソッドを使ってバイト列を維持したままuint[]からbyte[]へ変換する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    uint[] source = {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f};
    byte[] dest = new byte[16];

    // (配列の内容を16進数で表示)
    Console.WriteLine(string.Join(" ", source.Select(e => e.ToString("X8"))));
    Console.WriteLine(string.Join(" ", dest.Select(e => e.ToString("X2"))));
    Console.WriteLine();

    unsafe {
      // 配列のポインタを取得する
      fixed (void* ptrSource = source, ptrDest = dest) {
        // 配列sourceの先頭から16バイト分を配列destの先頭にコピーする
        // (バイト単位でコピーしてuint[]からbyte[]へ型変換する)
        Buffer.MemoryCopy(ptrSource, ptrDest, Buffer.ByteLength(dest), 16);
      }
    }

    Console.WriteLine(string.Join(" ", dest.Select(e => e.ToString("X2"))));
  }
}
実行結果
00010203 04050607 08090A0B 0C0D0E0F
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

03 02 01 00 07 06 05 04 0B 0A 09 08 0F 0E 0D 0C

配列⇄アンマネージポインタ間でのコピーを行う場合は、Marshal.Copyメソッドを使うことができます。

MemoryBlockメソッド以外でメモリブロックのコピーを行う方法、またそのパフォーマンスについては§.メモリブロックのコピー手法とパフォーマンスで別途解説します。

バイト列操作に関するクラス

ここではBufferクラス以外でバイト列操作に関連するクラスにどのようなものがあるか紹介します。

プリミティブ型とバイト列の相互変換 (BitConverter)

Bufferクラスのメソッドではプリミティブ型配列の操作を行うことができますが、プリミティブ型の値そのものをバイト列として扱うことはできません。 プリミティブ型の値のバイト列を取得する、また逆にバイト列からプリミティブ型の値に変換する操作を行うにはBitConverterクラスを使います。

バイト列への変換にはBitConverter.GetBytesメソッド、バイト列からの変換にはBitConverter.ToInt32などのメソッドを使います。

BitConverterクラスを使ってプリミティブ型の値とバイト列を相互に変換する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    // int型の値をバイト列に変換する
    int val = 0x01234567;
    byte[] bytes = BitConverter.GetBytes(val);

    Console.WriteLine("{0:X8} -> {1}", val, string.Join(" ", bytes.Select(e => e.ToString("X2"))));

    // バイト列のオフセット0から4バイト分をint型(Int32)の値に変換する
    bytes = new byte[] {0xde, 0xad, 0xbe, 0xef};
    val = BitConverter.ToInt32(bytes, 0);

    Console.WriteLine("{0} -> {1:X8}", string.Join(" ", bytes.Select(e => e.ToString("X2"))), val);
  }
}
実行結果
01234567 -> 67 45 23 01
DE AD BE EF -> EFBEADDE

プリミティブ型⇄バイト列の相互変換、BitConverterクラスについては基本型の型変換 §.基本型とバイト配列への/からの変換を参照してください。

文字列とバイト列の相互変換 (Encoding)

文字列をバイト列として扱うにはEncodingクラスを使います。 Encodingクラスは特定の文字コードにおける変換規則を表すクラスです。

バイト列への変換にはEncoding.GetBytesメソッド、バイト列からの変換にはEncoding.GetStringメソッドを使います。

BitConverterクラスを使って文字列とバイト列を相互に変換する
using System;
using System.Linq;
using System.Text;

class Sample {
  static void Main()
  {
    // 文字列をUTF-8のバイト列に変換する
    string str = "日本語";
    byte[] bytes = Encoding.UTF8.GetBytes(str);

    Console.WriteLine("{0} -> {1}", str, string.Join(" ", bytes.Select(e => e.ToString("X2"))));

    // バイト列をShift_JISの文字列とみなして変換する
    bytes = new byte[] {0x8A, 0xBF, 0x8E, 0x9A};
    str = Encoding.GetEncoding("Shift_JIS").GetString(bytes);

    Console.WriteLine("{0} -> {1}", string.Join(" ", bytes.Select(e => e.ToString("X2"))), str);
  }
}
実行結果
日本語 -> E6 97 A5 E6 9C AC E8 AA 9E
8A BF 8E 9A -> 漢字

構造体とバイト列の相互変換

.NET Frameworkでは任意の構造体とバイト列を直接相互に変換するクラスやメソッドが用意されていません。 独自に実装する方法についてはBinaryReader・BinaryWriterでの構造体の読み書きを参照してください。

バイト列のハッシュ化・暗号化・フォーマット変換

バイト列をMD5, SHA-1, SHA-512などのハッシュ関数でハッシュ化する方法、暗号化する方法、BASE64などにフォーマット変換する方法についてはテキスト変換・フォーマット変換を参照してください。

MemoryStreamとBinaryReader/BinaryWriterを使ったバイト列の相互変換

MemoryStreamとBinaryReader/BinaryWriterを組み合わせて使うことによっても、プリミティブ型とバイト列の相互変換をすることができます。

MemoryStreamとBinaryWriterを使ってuint型の値をバイト列に変換する
using System;
using System.IO;
using System.Linq;

class Sample {
  static void Main()
  {
    // 可変長のバッファを確保するMemoryStreamを作成
    using (var stream = new MemoryStream()) {
      // MemoryStreamに書き込むBinaryWriterを作成
      using (var writer = new BinaryWriter(stream)) {
        // uint型の値を書き込む
        writer.Write((uint)0x01234567);
        writer.Write((uint)0x89abcdef);
      }

      // MemoryStreamに書き込んだ内容をbyte型の配列に変換
      var bytes = stream.ToArray();

      Console.WriteLine(string.Join(" ", bytes.Select(e => e.ToString("X2"))));
    }
  }
}
実行結果
67 45 23 01 EF CD AB 89

MemoryStreamとBinaryReaderを使ってバイト列をuint型の値に変換する
using System;
using System.IO;
using System.Linq;

class Sample {
  static void Main()
  {
    byte[] bytes = new byte[] {0x67, 0x45, 0x23, 0x01, 0xef, 0xcd, 0xab, 0x89};

    // 既存の固定長バッファから読み込むMemoryStreamを作成
    using (var stream = new MemoryStream(bytes, false)) {
      // MemoryStreamから読み込むBinaryReaderを作成
      using (var reader = new BinaryReader(stream)) {
        // uint型の値を読み込む
        Console.WriteLine("{0:X8}", reader.ReadUInt32());
        Console.WriteLine("{0:X8}", reader.ReadUInt32());
      }
    }
  }
}
実行結果
01234567
89ABCDEF

MemoryStreamについての詳細はMemoryStreamクラス、BinaryReader/BinaryWriterについてはBinaryReaderクラス・BinaryWriterクラスを参照してください。

メモリブロックのコピー手法とパフォーマンス

.NET Frameworkのクラスライブラリで用いることができるメソッドを使って、memcpyのようなメモリブロックのコピーを行う方法と、そのパフォーマンスについて比較します。 ここでは下記メソッドを検証の対象としています。

以下は1kBのバッファを10,000,000回コピーした場合と、1MBのバッファを10,000回コピーした場合における各メソッドの所要時間です。 ほぼ同等のスペックではあるものの、それぞれ異なる構成の環境での結果になります。(ランタイムと構成の詳細は後に掲載)

Mono master on Ubuntu 14.04
$mcs /unsafe test.cs && mono test.exe
Unix 3.13.0.24
4.6.57.0
repeat: 10,000,000
bufferSize: 1,024
[trial0]
Array.Copy          : 00:00:01.6362623
Buffer.BlockCopy    : 00:00:00.9508335
Buffer.MemoryCopy   : 00:00:04.8978061
Marshal.Copy        : 00:00:01.0364196
[trial1]
Array.Copy          : 00:00:01.5339539
Buffer.BlockCopy    : 00:00:00.9387640
Buffer.MemoryCopy   : 00:00:04.7937147
Marshal.Copy        : 00:00:01.0056053
[trial2]
Array.Copy          : 00:00:01.5417680
Buffer.BlockCopy    : 00:00:00.9607770
Buffer.MemoryCopy   : 00:00:04.8224396
Marshal.Copy        : 00:00:01.0089125

$mcs /unsafe test.cs && mono test.exe
Unix 3.13.0.24
4.6.57.0
repeat: 10,000
bufferSize: 1,048,576
[trial0]
Array.Copy          : 00:00:01.2085023
Buffer.BlockCopy    : 00:00:01.7494556
Buffer.MemoryCopy   : 00:00:05.4265505
Marshal.Copy        : 00:00:01.7614601
[trial1]
Array.Copy          : 00:00:01.2344488
Buffer.BlockCopy    : 00:00:01.7310019
Buffer.MemoryCopy   : 00:00:05.2115303
Marshal.Copy        : 00:00:01.7434765
[trial2]
Array.Copy          : 00:00:01.2652210
Buffer.BlockCopy    : 00:00:01.7775290
Buffer.MemoryCopy   : 00:00:05.3989120
Marshal.Copy        : 00:00:01.7415971
.NET Framework 4.6 on Windows 7
>csc /unsafe test.cs && test.exe
Microsoft Windows NT 6.1.7601 Service Pack 1
4.0.30319.42000
repeat: 10,000,000
bufferSize: 1,024
[trial0]
Array.Copy          : 00:00:01.0222218
Buffer.BlockCopy    : 00:00:01.0212767
Buffer.MemoryCopy   : 00:00:00.9842077
Marshal.Copy        : 00:00:01.0883042
[trial1]
Array.Copy          : 00:00:01.0779283
Buffer.BlockCopy    : 00:00:01.0214242
Buffer.MemoryCopy   : 00:00:00.9880428
Marshal.Copy        : 00:00:01.1224393
[trial2]
Array.Copy          : 00:00:01.0340761
Buffer.BlockCopy    : 00:00:01.0217731
Buffer.MemoryCopy   : 00:00:00.9855210
Marshal.Copy        : 00:00:01.0926419

>csc /unsafe test.cs && test.exe
Microsoft Windows NT 6.1.7601 Service Pack 1
4.0.30319.42000
repeat: 10,000
bufferSize: 1,048,576
[trial0]
Array.Copy          : 00:00:11.1120259
Buffer.BlockCopy    : 00:00:11.1659976
Buffer.MemoryCopy   : 00:00:10.9877895
Marshal.Copy        : 00:00:11.2638044
[trial1]
Array.Copy          : 00:00:11.0481049
Buffer.BlockCopy    : 00:00:11.0484399
Buffer.MemoryCopy   : 00:00:10.9999365
Marshal.Copy        : 00:00:10.9938771
[trial2]
Array.Copy          : 00:00:10.9910499
Buffer.BlockCopy    : 00:00:11.0081194
Buffer.MemoryCopy   : 00:00:10.9891173
Marshal.Copy        : 00:00:10.9937553
  • 結果
    • .NET Frameworkでは、どのメソッドでもほぼ同等の速度
    • Monoでは、(1)Array.Copy、(2)Buffer.BlockCopyとMarshal.Copy、(3)Buffer.MemoryCopyで速度が異なる(=それぞれ異なる実装になっている?)
    • Monoでは、Buffer.MemoryCopyは際立ってパフォーマンスが悪い
    • Monoではバッファのサイズが大きくなってもさほど変わらない、.NET Frameworkではバッファのサイズが大きくなると遅くなる

この結果を見る限りでは、Buffer.MemoryCopyやMarshal.Copyによるコピーに大きな優位はないようです。 ただ、マルチプラットフォームでの動作を想定したコードでBuffer.MemoryCopyを使用する場合は、ほかのメソッドを使ったほうがよいか検討する余地がありそうです。

検証に使ったコード
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Diagnostics;

class Sample {
  static void Main()
  {
    Console.WriteLine(Environment.OSVersion);
    Console.WriteLine(Environment.Version);

    const int trial = 3; // 各テストを3回試行
    const int repeat = 10 * 1000; // 各テストにおけるコピーの繰り返し回数
    const int bufferSize = 1024 * 1024; // コピーするバッファのサイズ

    var bufferSource = new byte[bufferSize];
    var bufferDest   = new byte[bufferSize];

    Console.WriteLine("repeat: {0:N0}", repeat);
    Console.WriteLine("bufferSize: {0:N0}", bufferSize);

    for (var t = 0; t < trial; t++) {
      Console.WriteLine("[trial{0}]", t);

      // Array.Copyメソッドによるコピー
      {
        var lengthToCopy = bufferSource.Length;
        var sw = Stopwatch.StartNew();

        for (var r = 0; r < repeat; r++) {
          Array.Copy(bufferSource, 0,
                     bufferDest, 0, lengthToCopy);
        }

        Console.WriteLine("{0,-20}: {1}", "Array.Copy", sw.Elapsed);
      }

      // Buffer.BlockCopyメソッドによるコピー
      {
        var bytesToCopy = Buffer.ByteLength(bufferSource);
        var sw = Stopwatch.StartNew();

        for (var r = 0; r < repeat; r++) {
          Buffer.BlockCopy(bufferSource, 0,
                           bufferDest, 0, bytesToCopy);
        }

        Console.WriteLine("{0,-20}: {1}", "Buffer.BlockCopy", sw.Elapsed);
      }

      // Buffer.MemoryCopyメソッドによるコピー
      {
        unsafe {
          fixed (void* source = bufferSource, dest = bufferDest) {
            var bytesToCopy = Buffer.ByteLength(bufferSource);
            var destSizeInBytes = sizeof(byte) * bufferDest.Length;
            var sw = Stopwatch.StartNew();

            for (var r = 0; r < repeat; r++) {
              Buffer.MemoryCopy(source, dest, destSizeInBytes, bytesToCopy);
            }

            Console.WriteLine("{0,-20}: {1}", "Buffer.MemoryCopy", sw.Elapsed);
          }
        }
      }

      // Marshal.Copyメソッドによるコピー
      {
        unsafe {
          fixed (void* dest = bufferDest) {
            var lengthToCopy = bufferSource.Length;
            var ptrDest = new IntPtr(dest);
            var sw = Stopwatch.StartNew();

            for (var r = 0; r < repeat; r++) {
              Marshal.Copy(bufferSource, 0, ptrDest, lengthToCopy);
            }

            Console.WriteLine("{0,-20}: {1}", "Marshal.Copy", sw.Elapsed);
          }
        }
      }

#if false
      // Stream.CopyToメソッドによるコピー
      {
        var sw = Stopwatch.StartNew();

        for (var r = 0; r < repeat; r++) {
          using (var sourceStream = new MemoryStream(bufferSource, false))
          using (var destStream   = new MemoryStream(bufferDest,   true)) {
            sourceStream.CopyTo(destStream, 1024);
          }
        }

        Console.WriteLine("{0,-20}: {1}", "Stream.CopyTo", sw.Elapsed);
      }
#endif
    }
  }
}

このコードにはSystem.IO.Stream.CopyToによるコピーを掲載しています。 ただ、引数に指定するバッファサイズ次第でパフォーマンスが大きく変わるため、検証結果からは除外しています。

実行環境(Mono master on Ubuntu 14.04)
$uname -a
Linux smdn.jp 3.13.0-24-generic #47-Ubuntu SMP Fri May 2 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$mono -V
Mono JIT compiler version 4.3.2 (master/81e1e07 2016年  1月  2日 土曜日 14:47:13 JST)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
	TLS:           __thread
	SIGSEGV:       altstack
	Notifications: epoll
	Architecture:  amd64
	Disabled:      none
	Misc:          softdebug 
	LLVM:          yes(3.6.0svn-mono-master/a173357)
	GC:            sgen

$cat /proc/cpuinfo | grep name
model name	: Intel(R) Core(TM)2 CPU          6600  @ 2.40GHz
model name	: Intel(R) Core(TM)2 CPU          6600  @ 2.40GHz
実行環境(.NET Framework 4.6 on Windows 7)
>systeminfo
OS 名:                  Microsoft Windows 7 Ultimate 
OS バージョン:          6.1.7601 Service Pack 1 ビルド 7601
OS 製造元:              Microsoft Corporation
OS 構成:                スタンドアロン ワークステーション
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: AMD64 Family 15 Model 43 Stepping 1 AuthenticAMD ~2200 Mhz