memcpyをC#で

自分がC# (.NET Framework) を使っていて一番の不満かもしれないことの一つに、memcpy (CopyMemory) が無いことが挙げられます。普通はCopyMemoryをDllImportすれば解決ですが、Linux等への移植性まで考えるとWin32APIを使うわけにはいきません。以下では、DllImportを使わない代替案をいくつか考えてみます。

Marshal.Copy

一番オーソドックスかと思われる方法です。System.Runtime.InteropServices.Marshal.Copyを使い、一度マネージ配列を経由させてコピーします。メモリを余計に食うのが気になりますが、今はあまり気にしなくてもいい時代かもしれません。

static void CopyMemory(IntPtr dst, IntPtr src, int size)
{
    byte[] temp = new byte[size];
    Marshal.Copy(src, temp, 0, size);
    Marshal.Copy(temp, 0, dst, size);
}

UnmanagedMemoryStream

System.IO.UnmanagedMemoryStreamを使いメモリを読み書きします。やっていることはMarshal.Copyと同じで、間にマネージ配列を挟みます。

static unsafe void CopyMemory(IntPtr dst, IntPtr src, int size)
{
    using (UnmanagedMemoryStream streamSrc = new UnmanagedMemoryStream((byte*)src, size)
    using (UnmanagedMemoryStream streamDst = new UnmanagedMemoryStream((byte*)dst, size)
    {
        byte[] temp = new byte[size];
        streamSrc.Read(temp, 0, size);
        streamDst.Write(temp, 0, size);
    }
}

UnmanagedMemoryStreamのコンストラクタでは、メモリ領域の長さlengthはlong値で指定できます(Marshal.Copyはintです)。64ビット環境を考えるとこちらの方がいいのかもしれません。

少しずつコピー

大きいメモリ領域をコピーする際が気がかりという場合は、一気にではなく何バイトずつかに区切ってコピーするのが良いです。StreamからStreamへデータをコピーする以下のようなメソッドを作っておけば、CopyMemoryはそれを呼び出すことで実装できます。

Marshal.Copyでももちろん同様の手法を取ることはできますが、最後に残ったテンポラリ配列の長さに満たないメモリ領域についての処理が、Streamの方が簡単に書けます。

static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[32768];
    input.Position = 0;
    while (true)    
    {
        int read = input.Read (buffer, 0, buffer.Length);
        if (read <= 0)
            break;
        output.Write (buffer, 0, read);
    }
    input.Position = 0;
}
static unsafe void CopyMemory(IntPtr dst, IntPtr src, int size)
{
    using (UnmanagedMemoryStream streamSrc = new UnmanagedMemoryStream((byte*)src, size)
    using (UnmanagedMemoryStream streamDst = new UnmanagedMemoryStream((byte*)dst, size)
    {
        CopyStream(streamSrc, streamDst);
    }
}

.NET Framework 4では

Stream.CopyToという便利なメソッドが.NET Framework 4では増えました。これを使えばお手軽に書けてしまいます。

static unsafe void CopyMemory(IntPtr dst, IntPtr src, int size)
{
    using (UnmanagedMemoryStream streamSrc = new UnmanagedMemoryStream((byte*)src, size)
    using (UnmanagedMemoryStream streamDst = new UnmanagedMemoryStream((byte*)dst, size)
    {
        streamSrc.CopyTo(streamDst);
    }
}

ちまちまコピー

まず原理的にはこうなります。この方法の利点は余計なメモリを食わないことです。ただし以下の例では遅すぎて使い物にはなりません。

static void CopyMemory(IntPtr dst, IntPtr src, int size)
{
    for(int i = 0; i < size; i++)
    {
        Marshal.WriteByte(dst, i, Marshal.ReadByte(src, i));
    }
}

int単位で転送

byte単位ではなくint単位にすれば一気に4バイトコピーできるので速くなります (CPUにとっても4バイト(32ビット)単位はうれしいです)。その場合は、アドレスが4バイト単位になっていない場合を考慮する必要があります。Yanesdk.NETにはunsafeコードでuint*としてコピーするCopyMemoryの実装があり、参考になります。以下そのコードを引用させて頂きます。充分実用レベルの速度が出るそうです。

static unsafe void CopyMemory(void* outDest, void* inSrc, uint inNumOfBytes)
{
    // 転送先をuint幅にalignする
    const uint align = sizeof(uint) - 1;
    uint offset = (uint)outDest & align;
    // ↑ポインタは32bitとは限らないので本来このキャストはuintではダメだが、
    // 今は下位2bitだけあればいいのでこれでOK。
    if (offset != 0)
        offset = align - offset;
    offset = global::System.Math.Min(offset, inNumOfBytes);

    // 先頭の余り部分をbyteでちまちまコピー
    byte* srcBytes = (byte*)inSrc;
    byte* dstBytes = (byte*)outDest;
    for (uint i = 0; i < offset; i++)
        dstBytes[i] = srcBytes[i];

    // uintで一気に転送
    uint* dst = (uint*)((byte*)outDest + offset);
    uint* src = (uint*)((byte*)inSrc + offset);
    uint numOfUInt = (inNumOfBytes - offset) / sizeof(uint);
    for (uint i = 0; i < numOfUInt; i++)
        dst[i] = src[i];

    // 末尾の余り部分をbyteでちまちまコピー
    for (uint i = offset + numOfUInt * sizeof(uint); i < inNumOfBytes; i++)
        dstBytes[i] = srcBytes[i];
}

その他のCの関数は.NETでは?

こちらのページに一覧になっています。

http://support.microsoft.com/kb/313836/ja

上から2番目にあるmemcpyでは、Buffer.BlockCopyString.Copyが代わりとして示されています。まあ確かに、マネージ配列同士のコピーとしては正しいですし、memcpyはstring.hにある関数だからString.Copyもその通りではあります。でもアンマネージなメモリに対しては使えません。