はじめに
タイトル詐欺ですいません。。。
非同期処理を書いているときに、大きなListのスナップショットが欲しいと思い、DeepCopyの方法を調べてみました。
最近のC#はどんどん新しい機能が追加されているため、ナウい最新の方法を探します。
BinaryFormatter
みんな大好きBinaryFormatter!
ですが、最新のC#ではセキュリティ上、推奨されないようです。
コードはこんな感じです。
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にシリアライズしたあと、デシリアライズしています。
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>
を使ってみます。
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アイテム追加したものをそれぞれディープコピーしていきます。
// 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
でも良いが、オブジェクトを入れたものをディープコピーするしようとするとめっちゃ遅いから注意!