11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

C#のクラスを爆速でDeepCopyする一番ナウい方法

Posted at

はじめに

タイトル詐欺ですいません。。。
非同期処理を書いているときに、大きなListのスナップショットが欲しいと思い、DeepCopyの方法を調べてみました。
最近のC#はどんどん新しい機能が追加されているため、ナウい最新の方法を探します。

BinaryFormatter

みんな大好きBinaryFormatter!
ですが、最新のC#ではセキュリティ上、推奨されないようです。

コードはこんな感じです。

BinaryFormatter
public static T BinaryFomartterClone<T>(T src)
{
    using (var ms = new MemoryStream())
    {
        var binaryFormatter = new System.Runtime.Serialization
            .Formatters.Binary.BinaryFormatter();
        binaryFormatter.Serialize(ms, src);
        ms.Seek(0, SeekOrigin.Begin);
        return (T)binaryFormatter.Deserialize(ms);
    }
}

バイト列にシリアライズしたあと、デシリアライズしています。

JsonSerializer

どちらかといえば新顔のSystem.Text.Json.JsonSerializerです。
こちらはオブジェクトをJsonにシリアライズしたあと、デシリアライズしています。

JsonSerializer
public static T JsonSerializerClone<T>(T src)
{
    var json = System.Text.Json.JsonSerializer.Serialize(src);
    return System.Text.Json.JsonSerializer.Deserialize<T>(json);
}

Json関連の処理が入るため、ぱっと見た感じはBinaryFormatterのほうが早そうです。
上のコードをもうちょっと省エネにするためにstringを経由せずbyte[]System.Span<T>を使ってみます。

JsonSerializer(Use Span)
public static T JsonSerializerByteClone<T>(T src)
{
    ReadOnlySpan<byte> b = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes<T>(src);
    return System.Text.Json.JsonSerializer.Deserialize<T>(b);
}

ベンチマーク

以下のコードを使って、どちらが早いのかベンチマークを行います。
処理内容は、List<string>に1000アイテム追加したものと、List<T> where T : classに1000アイテム追加したものをそれぞれディープコピーしていきます。

Benchmark
// See https://aka.ms/new-console-template for more information
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.InteropServices;

var summary = BenchmarkRunner.Run<Test>();

[MemoryDiagnoser]
public class Test
{
    [Serializable]
    public class Order
    {
        public float Qty { get; set; }
        public float Price { get; set; }
    }

    private List<string> stringList;
    private List<Order> orderList;

    public Test()
    {
        stringList = new List<string>(1000);
        orderList = new List<Order>(1000);

        for(int i = 0; i < 1000; i++)
        {
            stringList.Add("testtest");
            orderList.Add(new Order() { Qty = 0.01f, Price = 0.02f });
        }
    }

    public static T BinaryFomartterClone<T>(T src)
    {
        using (var ms = new MemoryStream())
        {
            var binaryFormatter = new System.Runtime.Serialization
                .Formatters.Binary.BinaryFormatter();
            binaryFormatter.Serialize(ms, src);
            ms.Seek(0, SeekOrigin.Begin);
            return (T)binaryFormatter.Deserialize(ms);
        }
    }

    public static T JsonSerializerClone<T>(T src)
    {
        var json = System.Text.Json.JsonSerializer.Serialize(src);
        return System.Text.Json.JsonSerializer.Deserialize<T>(json);
    }

    public static T JsonSerializerByteClone<T>(T src)
    {
        ReadOnlySpan<byte> b = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes<T>(src);
        return System.Text.Json.JsonSerializer.Deserialize<T>(b);
    }

    [Benchmark]
    public void BinaryFomartterCloneStringList()
    {
        var v = BinaryFomartterClone(stringList);
        if (v[0] != stringList[0]) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerCloneStringList()
    {
        var v = JsonSerializerClone(stringList);
        if (v[0] != stringList[0]) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerByteCloneStringList()
    {
        var v = JsonSerializerByteClone(stringList);
        if (v[0] != stringList[0]) throw new Exception();
    }

    [Benchmark]
    public void BinaryFomartterCloneClassList()
    {
        var v = BinaryFomartterClone(orderList);
        if (v[0].Qty != orderList[0].Qty) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerCloneClassList()
    {
        var v = JsonSerializerClone(orderList);
        if (v[0].Qty != orderList[0].Qty) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerByteCloneClassList()
    {
        var v = JsonSerializerByteClone(orderList);
        if (v[0].Qty != orderList[0].Qty) throw new Exception();
    }
}

ベンチマーク結果

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1826 (21H2)
Intel Core i7-10700K CPU 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.5.22307.18
  [Host]     : .NET 7.0.0 (7.0.22.30112), X64 RyuJIT
  DefaultJob : .NET 7.0.0 (7.0.22.30112), X64 RyuJIT

|                             Method |        Mean |    Error |   StdDev |   Gen 0 |   Gen 1 | Allocated |
|----------------------------------- |------------:|---------:|---------:|--------:|--------:|----------:|
|     BinaryFomartterCloneStringList |    67.21 us | 0.358 us | 0.334 us |  3.9063 |  0.1221 |     33 KB |
|      JsonSerializerCloneStringList |    71.66 us | 0.244 us | 0.217 us |  9.3994 |  1.7090 |     77 KB |
|  JsonSerializerByteCloneStringList |    71.18 us | 0.788 us | 0.737 us |  8.0566 |  0.9766 |     66 KB |
|      BinaryFomartterCloneClassList | 1,118.30 us | 6.055 us | 5.663 us | 87.8906 | 31.2500 |    721 KB |
|       JsonSerializerCloneClassList |   630.56 us | 2.378 us | 2.224 us | 10.7422 |  1.9531 |     91 KB |
|   JsonSerializerByteCloneClassList |   626.13 us | 2.501 us | 2.340 us |  7.8125 |  0.9766 |     66 KB |

以外にもJsonSerializerがすごく優秀です!
リストが複雑の場合でも、アロケーションが変わらないのもすごいです!
ディープコピーする対象が単純なリストの場合は、BinaryFomatterを利用してもよいかもしれません。

まとめ

  • どんな時でもJsonSerializerを使うのがナウい!
  • 単純なListをディープコピーするときは、BinaryFormatterでも良いが、オブジェクトを入れたものをディープコピーするしようとするとめっちゃ遅いから注意!
11
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?