てくメモ

trivial な notes

【C#】配列のシャローコピー

いまさら配列のシャローコピー!? な感じがあるけれど整理。
きっかけはClone()を用いてコピーを取っているのを見たこと。


結果から言えば無知にもとづく認識だったのだけれど、Clone()は使わない方がよさそうと思っていた。
Clone と銘打ちながらジェネリックでないためキャストが必要になるし、ICloneableインターフェースは実装しないことが推奨されているし……。
ICloneable インターフェイス (System) | Microsoft Learn


ところがふとArray.Cloneを見てみると、Intrinsic属性が付いていた。
Source Browser (Array.Clone)
参考:JIT Intrinsics | ++C++; // 未確認飛行 C ブログ(Intrinsic)

使わない方がよいと思っていたAPIにパフォーマンスのための属性がついているとは――、ということで確認してみることにした。


2次元配列の複製において、Clone()Array.CopyBuffer.BlockCopy、ループを BenchmarkDotNet にて計測。

private readonly int[,] source = new int[256, 256];

[GlobalSetup]
public void Setup()
{
    int xl = source.GetLength(0);
    int yl = source.GetLength(1);
    int i = 0;
    for (int x = 0; x < xl; x++)
    {
        for (int y = 0; y < yl; y++)
        {
            source[x, y] = i++;
        }
    }
}

[Benchmark]
public int[,] ByClone() => (int[,])source.Clone();

[Benchmark]
public int[,] ByArrayCopy()
{
    int[,] dest = new int[source.GetLength(0),source.GetLength(1)];
    Array.Copy(source, dest, source.Length);
    return dest;
}

[Benchmark]
public int[,] ByBlockCopy()
{
    int[,] dest = new int[source.GetLength(0), source.GetLength(1)];
    Buffer.BlockCopy(source, 0, dest, 0, sizeof(int) * source.Length);
    return dest;
}

[Benchmark]
public int[,] ByLoop()
{
    int xl = source.GetLength(0);
    int yl = source.GetLength(1);
    int[,] dest = new int[xl, yl];
    for (int x = 0; x < xl; x++)
    {
        for (int y = 0; y < yl; y++)
        {
            dest[x, y] = source[x, y];
        }
    }

    return dest;
}
      Method |     Mean |     Error |   StdDev |    Gen0 |    Gen1 |    Gen2 | Allocated |
------------ |---------:|----------:|---------:|--------:|--------:|--------:|----------:|
     ByClone | 135.4 μs |  56.59 μs |  3.10 μs | 83.2520 | 83.2520 | 83.2520 | 256.07 KB |
 ByArrayCopy | 134.8 μs |  81.71 μs |  4.48 μs | 83.2520 | 83.2520 | 83.2520 | 256.07 KB |
 ByBlockCopy | 135.7 μs |  20.82 μs |  1.14 μs | 83.2520 | 83.2520 | 83.2520 | 256.07 KB |
      ByLoop | 246.7 μs | 225.55 μs | 12.36 μs | 83.0078 | 83.0078 | 83.0078 | 256.07 KB |


今回については前3者は実質同速とみなせそうに見える。
これならば、単純なシャローコピーなら簡潔なClone()でいい、と言える気がする。

もちろんケースによっては積極的に選ばない(例えばバッファをArrayPoolから用意、となれば使えない)ものの、少なくとも、使わないほうがよさそうという曖昧な忌避感は誤認だった。