diff --git a/FastCloner.Benchmark/BenchArray2d.cs b/FastCloner.Benchmark/BenchArray2d.cs new file mode 100644 index 0000000..d639157 --- /dev/null +++ b/FastCloner.Benchmark/BenchArray2d.cs @@ -0,0 +1,85 @@ +using BenchmarkDotNet.Attributes; +using Force.DeepCloner; + +namespace FastCloner.Benchmark; + +[RankColumn] +[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] +[MemoryDiagnoser] +public class Bench2DArray +{ + private int[,] testData; + private const int SIZE = 1000; + + [GlobalSetup] + public void Setup() + { + testData = new int[SIZE, SIZE]; + + for (int i = 0; i < SIZE; i++) + { + for (int j = 0; j < SIZE; j++) + { + testData[i, j] = i * j; + } + } + } + + [Benchmark(Baseline = true)] + public object? FastCloner() + { + return global::FastCloner.FastCloner.DeepClone(testData); + } + + [Benchmark] + public object? DeepCopier() + { + return global::DeepCopier.Copier.Copy(testData); + } + + [Benchmark] + public object? DeepCopy() + { + return global::DeepCopy.DeepCopier.Copy(testData); + } + + [Benchmark] + public object DeepCopyExpression() + { + return global::DeepCopy.ObjectCloner.Clone(testData); + } + + [Benchmark] + public object? FastDeepCloner() + { + return global::FastDeepCloner.DeepCloner.Clone(testData); + } + + [Benchmark] + public object? DeepCloner() + { + return testData.DeepClone(); + } + + [Benchmark] + public object? ArrayCopy() + { + int[,] clone = new int[SIZE, SIZE]; + Array.Copy(testData, clone, testData.Length); + return clone; + } + + [Benchmark] + public object? ManualCopy() + { + int[,] clone = new int[SIZE, SIZE]; + for (int i = 0; i < SIZE; i++) + { + for (int j = 0; j < SIZE; j++) + { + clone[i, j] = testData[i, j]; + } + } + return clone; + } +} \ No newline at end of file diff --git a/FastCloner.Benchmark/Dictionary.cs b/FastCloner.Benchmark/BenchDictionary.cs similarity index 70% rename from FastCloner.Benchmark/Dictionary.cs rename to FastCloner.Benchmark/BenchDictionary.cs index e4618be..4a037de 100644 --- a/FastCloner.Benchmark/Dictionary.cs +++ b/FastCloner.Benchmark/BenchDictionary.cs @@ -6,18 +6,18 @@ namespace FastCloner.Benchmark; [RankColumn] [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] [MemoryDiagnoser] -public class DictionaryBenchmark +public class BenchDictionary { - private Dictionary testData; + private Dictionary testData; [GlobalSetup] public void Setup() { - testData = new Dictionary(); + testData = new Dictionary(); for (int i = 0; i < 1000; i++) { - ComplexKey key = new ComplexKey { Id = i, Name = $"Key{i}" }; + SimpleKey key = new SimpleKey { Id = i, Name = $"Key{i}" }; testData.Add(key, $"Value{i}"); } } @@ -59,19 +59,8 @@ public object DeepCopyExpression() } } -public class ComplexKey +public class SimpleKey { public int Id { get; set; } public string Name { get; set; } - - public override bool Equals(object obj) - { - if (obj is not ComplexKey other) return false; - return Id == other.Id && Name == other.Name; - } - - public override int GetHashCode() - { - return HashCode.Combine(Id, Name); - } } \ No newline at end of file diff --git a/FastCloner.Benchmark/Minimal.cs b/FastCloner.Benchmark/BenchMinimal.cs similarity index 98% rename from FastCloner.Benchmark/Minimal.cs rename to FastCloner.Benchmark/BenchMinimal.cs index 117de7c..caca508 100644 --- a/FastCloner.Benchmark/Minimal.cs +++ b/FastCloner.Benchmark/BenchMinimal.cs @@ -6,7 +6,7 @@ namespace FastCloner.Benchmark; [RankColumn] [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] [MemoryDiagnoser] -public class Minimal +public class BenchMinimal { private TestObject testData; diff --git a/FastCloner.Benchmark/FeatureDictionary.cs b/FastCloner.Benchmark/FeatureDictionary.cs new file mode 100644 index 0000000..bea3bc5 --- /dev/null +++ b/FastCloner.Benchmark/FeatureDictionary.cs @@ -0,0 +1,70 @@ +using Force.DeepCloner; + +namespace FastCloner.Benchmark; + +using System; +using System.Collections.Generic; + +public class FeatureDictionary +{ + public void ValidateHashCodesAfterCloning() + { + // Arrange + Dictionary originalDict = new Dictionary(); + SimpleKey key1 = new SimpleKey { Id = 1, Name = "Test1" }; + originalDict.Add(key1, "Value1"); + + // Act - test všech implementací + TestCloner("FastCloner", () => TryClone(() => global::FastCloner.FastCloner.DeepClone(originalDict))); + TestCloner("DeepCopier", () => TryClone(() => global::DeepCopier.Copier.Copy(originalDict))); + TestCloner("DeepCopy", () => TryClone(() => global::DeepCopy.DeepCopier.Copy(originalDict))); + TestCloner("DeepCopyExpression", () => TryClone(() => global::DeepCopy.ObjectCloner.Clone(originalDict))); + TestCloner("FastDeepCloner", () => TryClone(() => global::FastDeepCloner.DeepCloner.Clone(originalDict))); + TestCloner("DeepCloner", () => TryClone(() => originalDict.DeepClone())); + } + + private static void TestCloner(string clonerName, Func<(bool success, Dictionary clonedDict)> cloneFunction) + { + try + { + // Act + (bool success, Dictionary clonedDict) = cloneFunction(); + + if (!success) + { + Console.WriteLine($"{clonerName}: ❌"); + return; + } + + // Arrange + KeyValuePair searchKey = clonedDict.First(); + + // Assert + if (clonedDict.TryGetValue(searchKey.Key, out string? value) && value == "Value1") + { + Console.WriteLine($"{clonerName}: ✅"); + } + else + { + Console.WriteLine($"{clonerName}: ❌"); + } + } + catch (Exception ex) + { + Console.WriteLine($"{clonerName}: ❌ (Exception: {ex.Message})"); + } + } + + private (bool success, Dictionary? clonedDict) TryClone(Func> cloneFunction) + { + try + { + Dictionary clonedDict = cloneFunction(); + return (true, clonedDict); + } + catch + { + return (false, null); + } + } +} \ No newline at end of file diff --git a/FastCloner.Benchmark/FeatureValidator.cs b/FastCloner.Benchmark/FeatureValidator.cs new file mode 100644 index 0000000..aa480c2 --- /dev/null +++ b/FastCloner.Benchmark/FeatureValidator.cs @@ -0,0 +1,9 @@ +namespace FastCloner.Benchmark; + +public class FeatureValidator +{ + public static void ValidateDictionary() + { + new FeatureDictionary().ValidateHashCodesAfterCloning(); + } +} \ No newline at end of file diff --git a/FastCloner.Benchmark/Program.cs b/FastCloner.Benchmark/Program.cs index 4f05583..3a794bd 100644 --- a/FastCloner.Benchmark/Program.cs +++ b/FastCloner.Benchmark/Program.cs @@ -13,7 +13,7 @@ static void Main(string[] args) ManualConfig config = DefaultConfig.Instance .WithOptions(ConfigOptions.DisableOptimizationsValidator); - Summary summary = BenchmarkRunner.Run(config); + Summary summary = BenchmarkRunner.Run(config); Console.WriteLine(summary); } } \ No newline at end of file diff --git a/FastCloner.Benchmark/README.md b/FastCloner.Benchmark/README.md new file mode 100644 index 0000000..dbe14cf --- /dev/null +++ b/FastCloner.Benchmark/README.md @@ -0,0 +1,42 @@ +# Benchmark results + +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. + +We alocate some extra memory, but this is mostly one-time price for various lookup tables. + +### Simple Dictionary: + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | +|----------------------|---------:|--------:|--------:|------:|--------:|-----:|--------:|--------:|----------:|------------:| +| DeepCloner** | 111.8 us | 1.55 us | 1.45 us | 0.58 | 0.03 | 1 | 26.7334 | 8.7891 | 164.51 KB | 0.46 | +| FastDeepCloner | 117.4 us | 1.36 us | 1.20 us | 0.61 | 0.03 | 2 | 16.3574 | 4.0283 | 100.79 KB | 0.28 | +| ⭐ FastCloner | 191.9 us | 3.81 us | 9.13 us | 1.00 | 0.07 | 3 | 58.5938 | 1.9531 | 359.94 KB | 1.00 | +| DeepCopy** | 211.9 us | 3.53 us | 3.30 us | 1.11 | 0.05 | 4 | 50.2930 | 24.9023 | 310.47 KB | 0.86 | +| DeepCopyExpression** | 241.5 us | 4.69 us | 7.02 us | 1.26 | 0.07 | 5 | 38.3301 | 9.5215 | 235.23 KB | 0.65 | +| DeepCopier (crashes) | NA | NA | NA | ? | ? | ? | NA | NA | NA | ? | + +** clones items incorrectly, unless hash code is overriden. + +### Minimal: + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | +|--------------------|------------:|----------:|----------:|------:|--------:|-----:|-------:|----------:|------------:| +| DeepCopier | 23.48 ns | 0.525 ns | 0.998 ns | 0.15 | 0.01 | 1 | 0.0115 | 72 B | 0.20 | +| DeepCopy | 41.55 ns | 0.751 ns | 0.702 ns | 0.27 | 0.01 | 2 | 0.0114 | 72 B | 0.20 | +| DeepCloner | 134.54 ns | 2.654 ns | 3.057 ns | 0.87 | 0.03 | 3 | 0.0370 | 232 B | 0.64 | +| ⭐ FastCloner | 155.21 ns | 3.007 ns | 4.215 ns | 1.00 | 0.04 | 4 | 0.0572 | 360 B | 1.00 | +| DeepCopyExpression | 169.37 ns | 3.269 ns | 3.498 ns | 1.09 | 0.04 | 5 | 0.0393 | 248 B | 0.69 | +| FastDeepCloner | 2,210.93 ns | 43.556 ns | 58.146 ns | 14.26 | 0.53 | 6 | 0.2060 | 1296 B | 3.60 | + +### Array 2D Big + +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|----------------------|-----------:|---------:|----------:|-----------:|------:|--------:|-----:|---------:|---------:|---------:|----------:|------------:| +| 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 | +| 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 | +| 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 | +| 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 | +| 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 | +| ⭐ 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 | +| 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 | +| DeepCopier (crashes) | NA | NA | NA | NA | ? | ? | ? | NA | NA | NA | NA | ? | \ No newline at end of file diff --git a/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs b/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs index d258a61..c9afe44 100644 --- a/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs +++ b/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs @@ -12,6 +12,10 @@ public class FastClonerIncrementalGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { + #if !ENABLE_SOURCEGEN + return; + #endif + IncrementalValuesProvider<(TypeDeclarationSyntax Syntax, SemanticModel SemanticModel)> typesToProcess = context.SyntaxProvider .CreateSyntaxProvider(