Skip to content

Commit 75a997a

Browse files
committed
add benchmark results
1 parent a544676 commit 75a997a

File tree

8 files changed

+217
-18
lines changed

8 files changed

+217
-18
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Force.DeepCloner;
3+
4+
namespace FastCloner.Benchmark;
5+
6+
[RankColumn]
7+
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
8+
[MemoryDiagnoser]
9+
public class Bench2DArray
10+
{
11+
private int[,] testData;
12+
private const int SIZE = 1000;
13+
14+
[GlobalSetup]
15+
public void Setup()
16+
{
17+
testData = new int[SIZE, SIZE];
18+
19+
for (int i = 0; i < SIZE; i++)
20+
{
21+
for (int j = 0; j < SIZE; j++)
22+
{
23+
testData[i, j] = i * j;
24+
}
25+
}
26+
}
27+
28+
[Benchmark(Baseline = true)]
29+
public object? FastCloner()
30+
{
31+
return global::FastCloner.FastCloner.DeepClone(testData);
32+
}
33+
34+
[Benchmark]
35+
public object? DeepCopier()
36+
{
37+
return global::DeepCopier.Copier.Copy(testData);
38+
}
39+
40+
[Benchmark]
41+
public object? DeepCopy()
42+
{
43+
return global::DeepCopy.DeepCopier.Copy(testData);
44+
}
45+
46+
[Benchmark]
47+
public object DeepCopyExpression()
48+
{
49+
return global::DeepCopy.ObjectCloner.Clone(testData);
50+
}
51+
52+
[Benchmark]
53+
public object? FastDeepCloner()
54+
{
55+
return global::FastDeepCloner.DeepCloner.Clone(testData);
56+
}
57+
58+
[Benchmark]
59+
public object? DeepCloner()
60+
{
61+
return testData.DeepClone();
62+
}
63+
64+
[Benchmark]
65+
public object? ArrayCopy()
66+
{
67+
int[,] clone = new int[SIZE, SIZE];
68+
Array.Copy(testData, clone, testData.Length);
69+
return clone;
70+
}
71+
72+
[Benchmark]
73+
public object? ManualCopy()
74+
{
75+
int[,] clone = new int[SIZE, SIZE];
76+
for (int i = 0; i < SIZE; i++)
77+
{
78+
for (int j = 0; j < SIZE; j++)
79+
{
80+
clone[i, j] = testData[i, j];
81+
}
82+
}
83+
return clone;
84+
}
85+
}
Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ namespace FastCloner.Benchmark;
66
[RankColumn]
77
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
88
[MemoryDiagnoser]
9-
public class DictionaryBenchmark
9+
public class BenchDictionary
1010
{
11-
private Dictionary<ComplexKey, string> testData;
11+
private Dictionary<SimpleKey, string> testData;
1212

1313
[GlobalSetup]
1414
public void Setup()
1515
{
16-
testData = new Dictionary<ComplexKey, string>();
16+
testData = new Dictionary<SimpleKey, string>();
1717

1818
for (int i = 0; i < 1000; i++)
1919
{
20-
ComplexKey key = new ComplexKey { Id = i, Name = $"Key{i}" };
20+
SimpleKey key = new SimpleKey { Id = i, Name = $"Key{i}" };
2121
testData.Add(key, $"Value{i}");
2222
}
2323
}
@@ -59,19 +59,8 @@ public object DeepCopyExpression()
5959
}
6060
}
6161

62-
public class ComplexKey
62+
public class SimpleKey
6363
{
6464
public int Id { get; set; }
6565
public string Name { get; set; }
66-
67-
public override bool Equals(object obj)
68-
{
69-
if (obj is not ComplexKey other) return false;
70-
return Id == other.Id && Name == other.Name;
71-
}
72-
73-
public override int GetHashCode()
74-
{
75-
return HashCode.Combine(Id, Name);
76-
}
7766
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace FastCloner.Benchmark;
66
[RankColumn]
77
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
88
[MemoryDiagnoser]
9-
public class Minimal
9+
public class BenchMinimal
1010
{
1111
private TestObject testData;
1212

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Force.DeepCloner;
2+
3+
namespace FastCloner.Benchmark;
4+
5+
using System;
6+
using System.Collections.Generic;
7+
8+
public class FeatureDictionary
9+
{
10+
public void ValidateHashCodesAfterCloning()
11+
{
12+
// Arrange
13+
Dictionary<SimpleKey, string> originalDict = new Dictionary<SimpleKey, string>();
14+
SimpleKey key1 = new SimpleKey { Id = 1, Name = "Test1" };
15+
originalDict.Add(key1, "Value1");
16+
17+
// Act - test všech implementací
18+
TestCloner("FastCloner", () => TryClone(() => global::FastCloner.FastCloner.DeepClone(originalDict)));
19+
TestCloner("DeepCopier", () => TryClone(() => global::DeepCopier.Copier.Copy(originalDict)));
20+
TestCloner("DeepCopy", () => TryClone(() => global::DeepCopy.DeepCopier.Copy(originalDict)));
21+
TestCloner("DeepCopyExpression", () => TryClone(() => global::DeepCopy.ObjectCloner.Clone(originalDict)));
22+
TestCloner("FastDeepCloner", () => TryClone(() => global::FastDeepCloner.DeepCloner.Clone(originalDict)));
23+
TestCloner("DeepCloner", () => TryClone(() => originalDict.DeepClone()));
24+
}
25+
26+
private static void TestCloner(string clonerName, Func<(bool success, Dictionary<SimpleKey, string> clonedDict)> cloneFunction)
27+
{
28+
try
29+
{
30+
// Act
31+
(bool success, Dictionary<SimpleKey, string> clonedDict) = cloneFunction();
32+
33+
if (!success)
34+
{
35+
Console.WriteLine($"{clonerName}: ❌");
36+
return;
37+
}
38+
39+
// Arrange
40+
KeyValuePair<SimpleKey, string> searchKey = clonedDict.First();
41+
42+
// Assert
43+
if (clonedDict.TryGetValue(searchKey.Key, out string? value) && value == "Value1")
44+
{
45+
Console.WriteLine($"{clonerName}: ✅");
46+
}
47+
else
48+
{
49+
Console.WriteLine($"{clonerName}: ❌");
50+
}
51+
}
52+
catch (Exception ex)
53+
{
54+
Console.WriteLine($"{clonerName}: ❌ (Exception: {ex.Message})");
55+
}
56+
}
57+
58+
private (bool success, Dictionary<SimpleKey, string>? clonedDict) TryClone(Func<Dictionary<SimpleKey, string>> cloneFunction)
59+
{
60+
try
61+
{
62+
Dictionary<SimpleKey, string> clonedDict = cloneFunction();
63+
return (true, clonedDict);
64+
}
65+
catch
66+
{
67+
return (false, null);
68+
}
69+
}
70+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace FastCloner.Benchmark;
2+
3+
public class FeatureValidator
4+
{
5+
public static void ValidateDictionary()
6+
{
7+
new FeatureDictionary().ValidateHashCodesAfterCloning();
8+
}
9+
}

FastCloner.Benchmark/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ static void Main(string[] args)
1313
ManualConfig config = DefaultConfig.Instance
1414
.WithOptions(ConfigOptions.DisableOptimizationsValidator);
1515

16-
Summary summary = BenchmarkRunner.Run<Minimal>(config);
16+
Summary summary = BenchmarkRunner.Run<Bench2DArray>(config);
1717
Console.WriteLine(summary);
1818
}
1919
}

FastCloner.Benchmark/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Benchmark results
2+
3+
FastCloner aims to _work correctly_ in the first place. It's currently reflection based, with opt-in source generator for increased performance. The benchmarking is made harder by the fact that many competing libraries behave incorrectly for scenarios that would be interested to bechmark. For example, only a few libraries deep clone dictionaries correctly. Broadly speaking, FastCloner in reflection mode is _fast_ in the reflection category, lags behind IL generation (when it works) and source generators.
4+
5+
We alocate some extra memory, but this is mostly one-time price for various lookup tables.
6+
7+
### Simple Dictionary:
8+
9+
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
10+
|----------------------|---------:|--------:|--------:|------:|--------:|-----:|--------:|--------:|----------:|------------:|
11+
| DeepCloner** | 111.8 us | 1.55 us | 1.45 us | 0.58 | 0.03 | 1 | 26.7334 | 8.7891 | 164.51 KB | 0.46 |
12+
| FastDeepCloner | 117.4 us | 1.36 us | 1.20 us | 0.61 | 0.03 | 2 | 16.3574 | 4.0283 | 100.79 KB | 0.28 |
13+
| ⭐ FastCloner | 191.9 us | 3.81 us | 9.13 us | 1.00 | 0.07 | 3 | 58.5938 | 1.9531 | 359.94 KB | 1.00 |
14+
| DeepCopy** | 211.9 us | 3.53 us | 3.30 us | 1.11 | 0.05 | 4 | 50.2930 | 24.9023 | 310.47 KB | 0.86 |
15+
| DeepCopyExpression** | 241.5 us | 4.69 us | 7.02 us | 1.26 | 0.07 | 5 | 38.3301 | 9.5215 | 235.23 KB | 0.65 |
16+
| DeepCopier (crashes) | NA | NA | NA | ? | ? | ? | NA | NA | NA | ? |
17+
18+
** clones items incorrectly, unless hash code is overriden.
19+
20+
### Minimal:
21+
22+
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio |
23+
|--------------------|------------:|----------:|----------:|------:|--------:|-----:|-------:|----------:|------------:|
24+
| DeepCopier | 23.48 ns | 0.525 ns | 0.998 ns | 0.15 | 0.01 | 1 | 0.0115 | 72 B | 0.20 |
25+
| DeepCopy | 41.55 ns | 0.751 ns | 0.702 ns | 0.27 | 0.01 | 2 | 0.0114 | 72 B | 0.20 |
26+
| DeepCloner | 134.54 ns | 2.654 ns | 3.057 ns | 0.87 | 0.03 | 3 | 0.0370 | 232 B | 0.64 |
27+
| ⭐ FastCloner | 155.21 ns | 3.007 ns | 4.215 ns | 1.00 | 0.04 | 4 | 0.0572 | 360 B | 1.00 |
28+
| DeepCopyExpression | 169.37 ns | 3.269 ns | 3.498 ns | 1.09 | 0.04 | 5 | 0.0393 | 248 B | 0.69 |
29+
| FastDeepCloner | 2,210.93 ns | 43.556 ns | 58.146 ns | 14.26 | 0.53 | 6 | 0.2060 | 1296 B | 3.60 |
30+
31+
### Array 2D Big
32+
33+
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
34+
|----------------------|-----------:|---------:|----------:|-----------:|------:|--------:|-----:|---------:|---------:|---------:|----------:|------------:|
35+
| DeepCopyExpression | 657.1 us | 15.29 us | 44.11 us | 652.0 us | 0.78 | 0.06 | 1 | 395.5078 | 394.5313 | 394.5313 | 3.81 MB | 1.00 |
36+
| FastDeepCloner | 663.9 us | 19.89 us | 57.38 us | 646.3 us | 0.79 | 0.07 | 1 | 399.4141 | 398.4375 | 398.4375 | 3.82 MB | 1.00 |
37+
| ArrayCopy | 784.2 us | 14.09 us | 18.81 us | 783.6 us | 0.93 | 0.04 | 2 | 281.2500 | 281.2500 | 281.2500 | 3.81 MB | 1.00 |
38+
| DeepCloner | 819.2 us | 13.67 us | 16.27 us | 820.5 us | 0.97 | 0.04 | 2 | 296.8750 | 296.8750 | 296.8750 | 3.81 MB | 1.00 |
39+
| DeepCopy | 832.6 us | 16.88 us | 48.15 us | 822.2 us | 0.99 | 0.07 | 2 | 273.4375 | 273.4375 | 273.4375 | 3.81 MB | 1.00 |
40+
| ⭐ FastCloner | 842.9 us | 16.79 us | 35.05 us | 833.3 us | 1.00 | 0.06 | 2 | 328.1250 | 328.1250 | 328.1250 | 3.82 MB | 1.00 |
41+
| ManualCopy | 2,574.0 us | 50.50 us | 115.02 us | 2,555.7 us | 3.06 | 0.18 | 3 | 390.6250 | 390.6250 | 390.6250 | 3.81 MB | 1.00 |
42+
| DeepCopier (crashes) | NA | NA | NA | NA | ? | ? | ? | NA | NA | NA | NA | ? |

FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ public class FastClonerIncrementalGenerator : IIncrementalGenerator
1212
{
1313
public void Initialize(IncrementalGeneratorInitializationContext context)
1414
{
15+
#if !ENABLE_SOURCEGEN
16+
return;
17+
#endif
18+
1519
IncrementalValuesProvider<(TypeDeclarationSyntax Syntax, SemanticModel SemanticModel)> typesToProcess =
1620
context.SyntaxProvider
1721
.CreateSyntaxProvider(

0 commit comments

Comments
 (0)