.NET Frameworkにおいてバッファなどのバイト列を扱うクラスにはMemoryStreamが存在しますが、Bufferクラスのメソッドを用いることによってプリミティブ型の配列を直接バイト列として操作することができます。 例えば、int
型の配列に対してバイト単位で値の取得・設定をしたり、memcpy
のように配列の一部をバイト列としてコピーすることができます。
こういった操作はシフト演算やポインタを介して行うこともできますが、Bufferクラスを用いればポインタ、つまりunsafe
コンテキストによるアンセーフコードを一切用いずに記述することができます。
Bufferクラス
Bufferクラスは配列をバッファとして扱い、バイト単位での操作を行うためのメソッドを持ったクラスです。 例えば、int
型配列に対して、1バイト単位で読み取り・書き込みをしたり、memcpy
・memcpy_s
のようにメモリブロックのコピーを行うためのメソッドなどが提供されています。
Bufferクラスは、リングバッファなどのバッファ機能を実装するクラスではありません。 バッファとなるメモリ領域を確保し、確保した領域に読み書きする目的にはSafeBufferクラス、ストリームへの読み書きに対してバッファリングを行う目的にはBufferedStreamクラスなど、バッファ機能を扱うクラスは別に存在します。
Bufferクラスで扱える配列型
Bufferクラスはプリミティブ型の配列に対してのみ用いることができます。 その他の型の配列、構造体やクラスの配列に対しては用いることができません。 .NET Frameworkではbyte
, int
, char
, double
, IntPtrなどの型がプリミティブ型として扱われます。 ここで言うプリミティブ型とは言語の組み込み型とは異なります。 例えばdecimal
やint?
は組み込み型ですがプリミティブ型ではありません。
数値型 | 整数型 |
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
なコードの記述を避けることができます。
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のバイト表現を図式化すると次にようになります。
オフセット(バイト) | 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 |
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
演算子で型のサイズを求め、配列の長さとの積をとることでも求めることができますが、このメソッドでは配列の型が何であるかを意識しなくてもメソッド呼び出しのみで配列のバイト数を求めることができます。
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
コンテキストを使った記述を排除できる=マネージドコードのみを用いても記述できることを示しています。
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
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
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クラスでは、配列に対してmemcpy
やmemmove
に相当する操作を行うためのBuffer.BlockCopy・Buffer.MemoryCopyといったメソッドが用意されています。 これらのメソッドを使うと、配列を単なるバイト列やメモリブロックとして扱い、配列の型によらずバイト単位でのコピーを行うことができます。
BlockCopyメソッド
Buffer.BlockCopyメソッドは、配列をバイト列とみなしてその内容を別の配列へとコピーします。 memcpy
やmemmove
に相当する操作は、このBlockCopyメソッドか、後述のMemoryCopyメソッドを使うことで実現できます。
配列をコピーするという点ではBuffer.BlockCopyメソッドとArray.Copyメソッドは似ていますが、BlockCopyメソッドは要素単位ではなくバイト単位でのコピーを行います。 引数の指定順序も同じですが、各引数の意味は以下のように異なるため、Array.CopyメソッドとBuffer.BlockCopyメソッドを単純に置き換えて使うことはできません。
メソッド呼び出し | メソッドの動作 |
---|---|
Buffer.BlockCopy(source, 0, dest, 2, 6)
|
配列sourceのオフセット0から6バイト分を配列destのオフセット2以降にコピーする |
Array.Copy(source, 0, dest, 2, 6)
|
配列sourceのインデックス0から6要素分を配列destのインデックス2以降にコピーする |
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
上記の例において、配列内の各値はリトルエンディアンで格納されている点に注意してください。 配列sourceとdestの内容を図式化すると次にようになります。
オフセット(バイト) | 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 |
オフセット(バイト) | 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メソッドを使うことによって配列のバイト単位での内容を維持したまま別の型の配列へと変換することができます。
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メソッドではuint
→byte
のように型としては互換性のない変換(縮小変換となる変換)でもコピーを行うことができます。 Array.Copyメソッドでは、uint[]
→byte[]
へのコピーのように縮小変換を伴うコピーを行おうとすると例外ArrayTypeMismatchExceptionがスローされます。
型の変換の種類、縮小変換については基本型の型変換を参照してください。
BlockCopyメソッドでは、コピー元とコピー先の領域がオーバーラップしていても(重なった状態でも)、適切にコピーが行われます。 オーバーラップした部分のコピー中の状態がコピー先に反映されることはありません。 (この動作は、Array.Copyメソッドでも同様です)
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 |
---|---|---|
コピーの単位 | バイト単位 | 要素単位 |
要素をまたがる位置からの/へのコピー | 可 | 不可 |
異なる型の配列へのコピー | プリミティブ型であれば可 (byte ⇄int ,int ⇄double など)それ以外の型の場合はArgumentException |
プリミティブ型かつ型に互換性がある場合のみ可 (byte →int ,int →double など)それ以外の型の場合はArrayTypeMismatchException (詳細:配列操作 §.異なる型の配列への複写) |
オーバーラップする範囲のコピー | 適切にコピーされる | 適切にコピーされる |
BlockCopyメソッド以外でメモリブロックのコピーを行う方法、またそのパフォーマンスについては§.メモリブロックのコピー手法とパフォーマンスで別途解説します。
MemoryCopyメソッド
Buffer.MemoryCopyは.NET Framework 4.6より利用可能になったメソッドで、名前が示すとおりmemcpy
に相当するメソッドです(厳密には、よりセキュアな関数memcpy_s
に近い)。
MemoryCopyメソッドはBlockCopyメソッドとは異なり、コピー元・コピー先の配列をポインタで指定します。 MemoryCopyメソッドでは、バッファオーバーランを起こす可能性を低減するために、まず引数destinationSizeInBytesにコピー先の使用可能なサイズを指定します。 その上で、引数sourceBytesToCopyにコピーするバイト数を指定します。
MemoryCopyメソッドではコピー元・コピー先の配列とオフセットをポインタとして指定するため、unsafe
コンテキストで用いる必要があります。
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などのメソッドを使います。
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メソッドを使います。
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を組み合わせて使うことによっても、プリミティブ型とバイト列の相互変換をすることができます。
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
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
のようなメモリブロックのコピーを行う方法と、そのパフォーマンスについて比較します。 ここでは下記メソッドを検証の対象としています。
- System.Array.Copy
- System.Buffer.BlockCopy
- System.Buffer.MemoryCopy
- System.Runtime.InteropServices.Marshal.Copy
以下は1kBのバッファを10,000,000回コピーした場合と、1MBのバッファを10,000回コピーした場合における各メソッドの所要時間です。 ほぼ同等のスペックではあるものの、それぞれ異なる構成の環境での結果になります。(ランタイムと構成の詳細は後に掲載)
$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
>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によるコピーを掲載しています。 ただ、引数に指定するバッファサイズ次第でパフォーマンスが大きく変わるため、検証結果からは除外しています。
$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
>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