Skip to content
eclair's note
Go back

【C#】配列のDeepCopyを高速化するベンチマーク調査結果

Edit on GitHub

以下のような操作を最も高速で行える方法はなにかを調べたメモ。

// 文字列の配列(ないしList)があったとして
List<string> original = ["a", "b", "c", "d", /* ... */];
// これをDeepCopy(DeepClone)する。
List<string> copy = new(original.Count);
foreach (var item in original)
{
    copy.Add(item);
}
// これを最速で行う方法を調べる。

環境は以下の通り。

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7623/25H2/2025Update/HudsonValley2)
Intel Core i7-14700F 2.10GHz, 1 CPU, 28 logical and 20 physical cores
.NET SDK 10.0.101
  [Host]    : .NET 6.0.28 (6.0.28, 6.0.2824.12007), X64 RyuJIT x86-64-v3
  .NET 6.0  : .NET 6.0.28 (6.0.28, 6.0.2824.12007), X64 RyuJIT x86-64-v3
  .NET 8.0  : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v3
  .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3

配列(string)の場合

とりあえず思いつく方法を列挙。

  1. Array.Copy
  2. forループで1要素ずつコピー
  3. Clone
  4. Enumerable.ToArray(original)
  5. original.AsSpan().ToArray()
  6. [.. original]
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class ArrayCopy
{
    private string[] SampleArray { get; set; } = [];

    [Params(100)]
    public int ArraySize { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        SampleArray = new string[ArraySize];
        for (int i = 0; i < ArraySize; i++)
        {
            SampleArray[i] = "SampleString" + i;
        }
    }

    [Benchmark]
    public string[] CopyUsingArrayCopy()
    {
        var a = SampleArray.Length;
        var destinationArray = new string[a];
        Array.Copy(SampleArray, destinationArray, a);
        return destinationArray;
    }

    [Benchmark]
    public string[] CopyFor()
    {
        var a = SampleArray.Length;
        var destinationArray = new string[a];
        for (int i = 0; i < a; i++)
        {
            destinationArray[i] = SampleArray[i];
        }
        return destinationArray;
    }

    [Benchmark]
    public string[] CopyUsingClone()
    {
        return (string[])SampleArray.Clone();
    }

    [Benchmark]
    public string[] CopyWithLinq()
    {
        return System.Linq.Enumerable.ToArray(SampleArray);
    }

    [Benchmark]
    public string[] CopyUsingSpan()
    {
        return SampleArray.AsSpan().ToArray();
    }

    [Benchmark]
    public string[] CopyUsingCollectionExpr()
    {
        return [.. SampleArray];
    }
}

結果

MethodJobRuntimeArraySizeMeanErrorStdDevGen0Allocated
CopyUsingArrayCopy.NET 10.0.NET 10.010032.65 ns0.678 ns0.807 ns0.0477824 B
CopyWithLinq.NET 10.0.NET 10.010033.53 ns0.700 ns0.911 ns0.0477824 B
CopyUsingSpan.NET 6.0.NET 6.010034.25 ns0.649 ns0.694 ns0.0477824 B
CopyUsingSpan.NET 8.0.NET 8.010034.90 ns0.702 ns0.656 ns0.0477824 B
CopyUsingCollectionExpr.NET 6.0.NET 6.010040.33 ns0.761 ns0.712 ns0.0477824 B
CopyWithLinq.NET 6.0.NET 6.010041.40 ns0.849 ns0.978 ns0.0477824 B
CopyUsingSpan.NET 10.0.NET 10.010042.04 ns0.868 ns1.129 ns0.0477824 B
CopyUsingCollectionExpr.NET 10.0.NET 10.010043.03 ns0.897 ns1.033 ns0.0477824 B
CopyUsingCollectionExpr.NET 8.0.NET 8.010044.83 ns0.433 ns0.384 ns0.0477824 B
CopyUsingArrayCopy.NET 8.0.NET 8.010045.13 ns0.928 ns1.417 ns0.0477824 B
CopyUsingClone.NET 10.0.NET 10.010045.27 ns0.359 ns0.336 ns0.0477824 B
CopyUsingClone.NET 8.0.NET 8.010047.07 ns0.949 ns1.298 ns0.0477824 B
CopyUsingClone.NET 6.0.NET 6.010048.62 ns0.973 ns1.195 ns0.0477824 B
CopyWithLinq.NET 8.0.NET 8.010054.21 ns1.109 ns2.214 ns0.0477824 B
CopyUsingArrayCopy.NET 6.0.NET 6.010057.13 ns1.161 ns1.086 ns0.0477824 B
CopyFor.NET 10.0.NET 10.010085.55 ns0.843 ns0.788 ns0.0477824 B
CopyFor.NET 8.0.NET 8.010086.35 ns0.946 ns0.885 ns0.0477824 B
CopyFor.NET 6.0.NET 6.0100104.11 ns1.537 ns1.438 ns0.0477824 B

うーん、謎…。
とりあえずSpanを使えば悪くなさそう。

配列(クラス)の場合

以下のようなクラスをディープコピーすることを考える。

public class SampleSubClass
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public bool Flag { get; set; } = false;

    public SampleSubClass Clone()
    {
        return new SampleSubClass()
        {
            Id = this.Id,
            Name = this.Name,
            Flag = this.Flag,
        };
    }
}

方法としては

  1. forループで1要素ずつCloneしてコピー
  2. Enumerable.ToArray(original.Select(x => x.Clone()))
  3. original.AsSpan()+forループで1要素ずつCloneしてコピー
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class ArrayCopyWithClass
{
    private SampleSubClass[] SampleArray { get; set; } = [];

    [Params(5)]
    public int ArraySize { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        SampleArray = new SampleSubClass[ArraySize];
        for (int i = 0; i < ArraySize; i++)
        {
            SampleArray[i] = new()
            {
                Id = i,
                Name = "Sample_" + i.ToString(),
                Flag = (i % 2 == 0),
            };
        }
    }

    [Benchmark]
    public SampleSubClass[] CopyForAdd()
    {
        var a = SampleArray.Length;
        var c = new SampleSubClass[a];
        for (int i = 0; i < a; i++)
        {
            c[i] = SampleArray[i].Clone();
        }
        return c;
    }

    [Benchmark]
    public SampleSubClass[] CopyUsingLinq()
    {
        return System.Linq.Enumerable.ToArray(SampleArray.Select(item => item.Clone()));
    }

    [Benchmark]
    public SampleSubClass[] CopyUsingSpan()
    {
        var a = SampleArray.Length;
        var r = new SampleSubClass[a];
        var c = r.AsSpan();
        for (var i = 0; i < a; i++)
        {
            c[i] = SampleArray[i].Clone();
        }
        return r;
    }
}

結果

MethodJobRuntimeArraySizeMeanErrorStdDevGen0Allocated
CopyUsingSpan.NET 10.0.NET 10.0521.30 ns0.203 ns0.199 ns0.0130224 B
CopyForAdd.NET 8.0.NET 8.0522.82 ns0.326 ns0.305 ns0.0130224 B
CopyUsingSpan.NET 8.0.NET 8.0526.61 ns0.493 ns0.437 ns0.0130224 B
CopyUsingLinq.NET 10.0.NET 10.0533.22 ns0.227 ns0.438 ns0.0157272 B
CopyUsingSpan.NET 6.0.NET 6.0534.50 ns0.666 ns0.955 ns0.0129224 B
CopyForAdd.NET 6.0.NET 6.0535.63 ns0.483 ns0.452 ns0.0129224 B
CopyUsingLinq.NET 8.0.NET 8.0539.48 ns0.415 ns0.388 ns0.0157272 B
CopyForAdd.NET 10.0.NET 10.0540.50 ns0.604 ns0.565 ns0.0130224 B
CopyUsingLinq.NET 6.0.NET 6.0552.01 ns0.614 ns0.574 ns0.0157272 B

とりあえずSpan経由で良さげ。


List(string)の場合

思いつく範囲で。

  1. コンストラクタ経由でコピー
  2. foreachで1要素ずつコピー
  3. forループで1要素ずつコピー
  4. Enumerable.ToList(original)
  5. CollectionsMarshal.AsSpan(original).ToArray()
  6. CollectionsMarshal.AsSpan+一つずつコピー
  7. [.. original]
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class ListCopy
{
    private List<string> SampleList { get; set; } = [];

    [Params(100)]
    public int ArraySize { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        SampleList = new(ArraySize);
        for (int i = 0; i < ArraySize; i++)
        {
            SampleList.Add("SampleString" + i);
        }
    }

    [Benchmark]
    public List<string> CopyUsingConstructor()
    {
        return new List<string>(SampleList);
    }

    [Benchmark]
    public List<string> CopyForeachAdd()
    {
        var a = SampleList.Count;
        var c = new List<string>(a);
        foreach (var item in SampleList)
        {
            c.Add(item);
        }
        return c;
    }

    [Benchmark]
    public List<string> CopyForAdd()
    {
        var a = SampleList.Count;
        var c = new List<string>(a);
        for (int i = 0; i < SampleList.Count; i++)
        {
            c.Add(SampleList[i]);
        }
        return c;
    }

    [Benchmark]
    public List<string> CopyUsingLinq()
    {
        return System.Linq.Enumerable.ToList(SampleList);
    }

    [Benchmark]
    public List<string> CopyUsingSpan1()
    {
        var c = CollectionsMarshal.AsSpan(SampleList).ToArray();
        return new List<string>(c);
    }

    [Benchmark]
    public List<string> CopyUsingSpan2()
    {
#if NET7_0_OR_GREATER
        var a = SampleList.Count;
        var r = new List<string>(a);
        CollectionsMarshal.SetCount(r, a);
        var c = CollectionsMarshal.AsSpan(r);
        for (var i = 0; i < SampleList.Count; i++)
        {
            c[i] = SampleList[i];
        }
        return r;
#endif
        throw new NotSupportedException(
            "CollectionsMarshal.SetCount is not supported in this .NET version."
        );
    }

    [Benchmark]
    public List<string> CopyUsingCollectionExpr()
    {
        return [.. SampleList];
    }
}

結果

MethodJobRuntimeArraySizeMeanErrorStdDevGen0Gen1Allocated
CopyUsingLinq.NET 10.0.NET 10.010036.66 ns0.668 ns0.592 ns0.04960.0001856 B
CopyUsingConstructor.NET 10.0.NET 10.010036.73 ns0.551 ns0.460 ns0.04960.0001856 B
CopyUsingCollectionExpr.NET 10.0.NET 10.010036.93 ns0.498 ns0.466 ns0.04960.0001856 B
CopyUsingConstructor.NET 6.0.NET 6.010042.11 ns0.781 ns0.988 ns0.04960.0001856 B
CopyUsingConstructor.NET 8.0.NET 8.010042.21 ns0.303 ns0.237 ns0.04960.0001856 B
CopyUsingCollectionExpr.NET 6.0.NET 6.010044.89 ns0.412 ns0.386 ns0.04960.0001856 B
CopyUsingLinq.NET 6.0.NET 6.010045.04 ns0.729 ns0.748 ns0.04960.0001856 B
CopyUsingCollectionExpr.NET 8.0.NET 8.010045.20 ns0.725 ns0.678 ns0.04960.0001856 B
CopyUsingLinq.NET 8.0.NET 8.010046.67 ns0.964 ns1.184 ns0.04960.0001856 B
CopyUsingSpan1.NET 10.0.NET 10.010066.88 ns1.051 ns0.932 ns0.09740.00021680 B
CopyUsingSpan1.NET 6.0.NET 6.010082.20 ns0.669 ns0.626 ns0.09730.00021680 B
CopyUsingSpan1.NET 8.0.NET 8.010082.57 ns1.162 ns0.970 ns0.09740.00021680 B
CopyForeachAdd.NET 10.0.NET 10.0100133.95 ns1.563 ns1.385 ns0.0496-856 B
CopyUsingSpan2.NET 8.0.NET 8.0100137.5 ns1.51 ns1.41 ns0.0496-856 B
CopyUsingSpan2.NET 10.0.NET 10.0100140.1 ns2.07 ns2.54 ns0.0496-856 B
CopyForeachAdd.NET 8.0.NET 8.0100150.56 ns1.521 ns1.422 ns0.0496-856 B
CopyForAdd.NET 10.0.NET 10.0100169.19 ns2.459 ns2.179 ns0.0496-856 B
CopyForeachAdd.NET 6.0.NET 6.0100170.70 ns1.737 ns1.625 ns0.0496-856 B
CopyForAdd.NET 6.0.NET 6.0100173.45 ns1.378 ns1.222 ns0.0496-856 B
CopyForAdd.NET 8.0.NET 8.0100195.96 ns3.262 ns3.051 ns0.0496-856 B

どの環境でもnew List<string>(original)で良さそう。

List(クラス)の場合

Arrayと同様に、SampleSubClassをディープコピー(Clone関数の呼び出し)することを考える。
方法としては

  1. foreachで1要素ずつCloneしてAdd
  2. forループで1要素ずつCloneしてAdd
  3. SetCount+forループで1要素ずつCloneして代入
  4. Enumerable.ToList(original.Select(x => x.Clone()))
  5. CollectionsMarshal.AsSpan(original)+forループでCloneしてAdd
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class ListCopyWithClass
{
    private List<SampleSubClass> SampleList { get; set; } = [];

    [Params(5)]
    public int ArraySize { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        SampleList = new(ArraySize);
        for (int i = 0; i < ArraySize; i++)
        {
            SampleList.Add(
                new()
                {
                    Id = i,
                    Name = "Sample_" + i.ToString(),
                    Flag = (i % 2 == 0),
                }
            );
        }
    }

    [Benchmark]
    public List<SampleSubClass> CopyForeachAdd()
    {
        var a = SampleList.Count;
        var c = new List<SampleSubClass>(a);
        foreach (var i in SampleList)
        {
            c.Add(i.Clone());
        }
        return c;
    }

    [Benchmark]
    public List<SampleSubClass> CopyForAdd()
    {
        var a = SampleList.Count;
        var c = new List<SampleSubClass>(a);
        for (int i = 0; i < a; i++)
        {
            c.Add(SampleList[i].Clone());
        }
        return c;
    }

    [Benchmark]
    public List<SampleSubClass> CopyForIndex()
    {
#if NET7_0_OR_GREATER
        var a = SampleList.Count;
        var c = new List<SampleSubClass>(a);
        CollectionsMarshal.SetCount(c, a);
        for (int i = 0; i < a; i++)
        {
            c[i] = SampleList[i].Clone();
        }
        return c;
#endif
        throw new NotSupportedException(
            "CollectionsMarshal.SetCount is not supported in this .NET version."
        );
    }

    [Benchmark]
    public List<SampleSubClass> CopyUsingLinq()
    {
        return System.Linq.Enumerable.ToList(SampleList.Select(item => item.Clone()));
    }

    [Benchmark]
    public List<SampleSubClass> CopyUsingSpan()
    {
#if NET7_0_OR_GREATER
        var a = SampleList.Count;
        var r = new List<SampleSubClass>(a);
        CollectionsMarshal.SetCount(r, a);
        var c = CollectionsMarshal.AsSpan(r);
        for (var i = 0; i < a; i++)
        {
            c[i] = SampleList[i].Clone();
        }
        return r;
#endif
        throw new NotSupportedException(
            "CollectionsMarshal.SetCount is not supported in this .NET version."
        );
    }
}

結果

MethodJobRuntimeArraySizeMeanErrorStdDevGen0Allocated
CopyForIndex.NET 6.0.NET 6.05NANANANANA
CopyUsingSpan.NET 6.0.NET 6.05NANANANANA
CopyUsingSpan.NET 8.0.NET 8.0526.61 ns0.569 ns1.187 ns0.0148256 B
CopyForIndex.NET 10.0.NET 10.0527.13 ns0.578 ns1.085 ns0.0148256 B
CopyUsingSpan.NET 10.0.NET 10.0527.67 ns0.585 ns0.674 ns0.0148256 B
CopyForAdd.NET 10.0.NET 10.0530.59 ns0.476 ns0.422 ns0.0148256 B
CopyForeachAdd.NET 8.0.NET 8.0530.91 ns0.616 ns0.605 ns0.0148256 B
CopyForeachAdd.NET 10.0.NET 10.0531.03 ns0.657 ns0.922 ns0.0148256 B
CopyForIndex.NET 8.0.NET 8.0531.34 ns0.490 ns0.434 ns0.0148256 B
CopyForAdd.NET 8.0.NET 8.0533.21 ns0.579 ns0.541 ns0.0148256 B
CopyForAdd.NET 6.0.NET 6.0541.43 ns0.597 ns0.559 ns0.0148256 B
CopyUsingLinq.NET 10.0.NET 10.0543.21 ns0.894 ns1.339 ns0.0190328 B
CopyForeachAdd.NET 6.0.NET 6.0543.36 ns0.513 ns0.480 ns0.0148256 B
CopyUsingLinq.NET 8.0.NET 8.0556.47 ns1.057 ns1.175 ns0.0190328 B
CopyUsingLinq.NET 6.0.NET 6.0564.31 ns0.438 ns0.388 ns0.0190328 B

.NET7以降であればSetCountを使う(Span or Index指定追加)が最速。
.NET6の場合はforeachforで良さそう。


Edit on GitHub
Share this post on:

Previous Post
【C#】Decorator/Interceptorの自動実装を試みる方法メモ
Next Post
Source Generator Tips(#1): コード整形にIndentStringBuilderを使う