Repository navigation
Conversation
|
@lofcz - We evaluated FastCloner as a potential replacement for Force.DeepCloner in Foundatio and ran comprehensive benchmarks. Our results show FastCloner's reflection-based Questions:
Our benchmark objects include:
The full benchmark code is available in this PR. We'd appreciate any insights on expected performance characteristics or if we're missing something in our usage. |
| state.DecrementDepth(); | ||
|
|
||
| // if UseWorkList was set during recursive cloning, process the worklist | ||
| if (!state.UseWorkList) |
| List<Expression> expressionList = []; | ||
|
|
||
| ParameterExpression from = Expression.Parameter(methodType); | ||
| ParameterExpression fromLocal = from; |
| ParameterExpression from = Expression.Parameter(methodType); | ||
| ParameterExpression fromLocal = from; | ||
| ParameterExpression to = Expression.Parameter(methodType); | ||
| ParameterExpression toLocal = to; |
| foreach (KeyValuePair<Type, CloneBehavior> kvp in TypeBehaviors) | ||
| { | ||
| if (kvp.Value == CloneBehavior.Ignore && FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(kvp.Key)) | ||
| { | ||
| HasSafeTypeOverrides = true; | ||
| return; | ||
| } | ||
| } |
| foreach (EventInfo evtInfo in events) | ||
| { | ||
| if (MemberIsIgnored(evtInfo)) | ||
| { | ||
| details[evtInfo.Name] = evtInfo.EventHandlerType; | ||
| } | ||
| } |
| foreach (EventInfo evtInfo in events) | ||
| { | ||
| if (MemberIsIgnored(evtInfo)) | ||
| { | ||
| details[evtInfo.Name] = evtInfo.EventHandlerType; | ||
| } | ||
| } |
| foreach (FieldInfo fi in tp.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly)) | ||
| { | ||
| Type ft = fi.FieldType; | ||
| if (ft == type) | ||
| { | ||
| return true; | ||
| } | ||
| if (ft.IsArray && ft.GetElementType() == type) | ||
| { | ||
| return true; | ||
| } | ||
| } |
| foreach (FieldInfo fieldInfo in GetAllTypeFields(type)) | ||
| { | ||
| Type fieldType = fieldInfo.FieldType; | ||
|
|
||
| if (processingTypes.Contains(fieldType)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (CanReturnSameType(fieldType, processingTypes)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| knownTypes.TryAdd(type, false); | ||
| return false; | ||
| } |
| if (member is FieldInfo fieldInfoForEventCheck && ignoredEventDetails.TryGetValue(fieldInfoForEventCheck.Name, out Type? evtType)) | ||
| { | ||
| if (evtType == memberType) | ||
| { | ||
| if (canAssignDirect) | ||
| { | ||
| expressionList.Add(Expression.Assign( | ||
| Expression.MakeMemberAccess(toLocal, member), | ||
| Expression.Default(memberType) | ||
| )); | ||
| } | ||
|
|
||
| continue; | ||
| } | ||
| } |
| else if (member is FieldInfo fi) | ||
| { | ||
| if (ignoredEventDetails.TryGetValue(fi.Name, out Type? eventHandlerTypeFromCache)) | ||
| { | ||
| if (eventHandlerTypeFromCache == fi.FieldType) | ||
| { | ||
| shouldBeIgnored = true; | ||
| } | ||
| } | ||
| } |
|
@niemyjski I'll look into your setup and tests. I'm very busy with dayjob for the upcoming two weeks, but if you can keep the PR open, I'll get to this after that. Note that I don't have a MacOS machine available, but if the performance characteristic will show similar results under Windows, I'll work on it. Note that original DeepCloner is doing a lot of things naively, that leads to subtle bugs that surface only with certain usage patterns of the cloned object. Since you are internalizing, I might also strip certain parts of FastCloner to create a "slim" distribution. |
|
@lofcz no worries, totally understand time commitments and no rush to close this. I appreciate your time and help. |
- Replace Force.DeepCloner with FastCloner v3.4.4 (source imported) - Add Update-FastCloner.ps1 script for source import automation - Update ObjectExtensions.DeepClone to use FastClonerGenerator - Add nullable annotations to DeepClone extension method - Replace #if MODERN with #if true // MODERN for .NET 8+ targets - Add benchmark comparison results Benchmark results show FastCloner is 7-149% slower than DeepCloner for our test cases, contrary to published benchmarks.
0711b0c to
b85edeb
Compare
|
Just a quick update for now, that this is being worked on, progress in https://github.com/lofcz/Foundatio/tree/feat-fast-cloner, will open a new PR once it's done. For now I have both cloners side by side in my branch for reproducible benchmarks. |
|
Quick update - results seem good, at least on my Windows machine. But I'd be surprised if macOS differed much. Need to figure out how to bring this back upstream first, enable "Ultra" mode that disables the features slimmed for Foundatio's build and how to properly generate code for internalization. BenchmarkDotNet v0.15.8, Windows 11 (10.0.26300.7939)
Intel Core i7-8700 CPU 3.20GHz (Max: 3.19GHz) (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.102
[Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3
Job-IJPESX : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3
IterationCount=5 LaunchCount=1 WarmupCount=3 // <-- note that this is low, full precision runs for an hour..
| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|-------------------------------------- |--------------------------- |----------------:|-----------------:|---------------:|------:|--------:|---------:|---------:|--------:|----------:|------------:|
| FastCloner_DynamicWithArray | DynamicWithArray | 4,209.09 ns | 503.379 ns | 77.898 ns | 0.52 | 0.10 | 0.9537 | 0.0076 | - | 6008 B | 0.68 |
| DeepCloner_DynamicWithArray | DynamicWithArray | 8,406.90 ns | 7,091.861 ns | 1,841.734 ns | 1.04 | 0.29 | 1.3962 | 0.0229 | - | 8800 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_DynamicWithDictionary | DynamicWithDictionary | 1,321.86 ns | 647.517 ns | 100.204 ns | 0.84 | 0.12 | 0.2651 | - | - | 1664 B | 0.61 |
| DeepCloner_DynamicWithDictionary | DynamicWithDictionary | 1,588.48 ns | 1,386.457 ns | 214.556 ns | 1.01 | 0.17 | 0.4311 | 0.0019 | - | 2712 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_DynamicWithNestedObject | DynamicWithNestedObject | 1,155.78 ns | 172.854 ns | 26.749 ns | 0.74 | 0.02 | 0.2918 | - | - | 1840 B | 0.52 |
| DeepCloner_DynamicWithNestedObject | DynamicWithNestedObject | 1,566.68 ns | 148.512 ns | 22.982 ns | 1.00 | 0.02 | 0.5646 | 0.0038 | - | 3560 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_FileSpec | FileSpec | 231.15 ns | 33.752 ns | 5.223 ns | 0.54 | 0.01 | 0.0663 | - | - | 416 B | 0.45 |
| DeepCloner_FileSpec | FileSpec | 428.18 ns | 25.398 ns | 3.930 ns | 1.00 | 0.01 | 0.1464 | - | - | 920 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_LargeEventDocument_10MB | LargeEventDocument | 35,487.81 ns | 4,447.006 ns | 1,154.873 ns | 0.45 | 0.02 | 7.9346 | 0.8545 | - | 50112 B | 0.39 |
| DeepCloner_LargeEventDocument_10MB | LargeEventDocument | 78,126.04 ns | 14,403.120 ns | 2,228.899 ns | 1.00 | 0.04 | 20.6299 | 4.0283 | - | 129792 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_LargeLogBatch_10MB | LargeLogBatch | 5,310,627.81 ns | 1,117,946.138 ns | 290,327.062 ns | 0.57 | 0.03 | 367.1875 | 226.5625 | 62.5000 | 2993234 B | 0.84 |
| DeepCloner_LargeLogBatch_10MB | LargeLogBatch | 9,340,684.06 ns | 1,198,884.797 ns | 311,346.575 ns | 1.00 | 0.04 | 484.3750 | 296.8750 | 62.5000 | 3564667 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_MediumNestedObject | MediumNestedObject | 978.26 ns | 71.164 ns | 11.013 ns | 0.65 | 0.03 | 0.2670 | - | - | 1680 B | 0.49 |
| DeepCloner_MediumNestedObject | MediumNestedObject | 1,496.87 ns | 262.514 ns | 68.174 ns | 1.00 | 0.06 | 0.5436 | 0.0038 | - | 3416 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_ObjectDictionary_50 | ObjectDictionary | 172,718.54 ns | 26,168.577 ns | 4,049.616 ns | 0.16 | 0.01 | 37.1094 | 11.2305 | - | 233168 B | 0.42 |
| DeepCloner_ObjectDictionary_50 | ObjectDictionary | 1,050,157.46 ns | 191,182.882 ns | 49,649.588 ns | 1.00 | 0.06 | 76.1719 | 39.0625 | 7.8125 | 549653 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_ObjectList_100 | ObjectList | 110,054.03 ns | 4,760.622 ns | 1,236.318 ns | 0.59 | 0.05 | 26.3672 | 6.9580 | - | 165784 B | 0.52 |
| DeepCloner_ObjectList_100 | ObjectList | 186,597.24 ns | 65,584.427 ns | 17,032.067 ns | 1.01 | 0.12 | 50.7813 | 16.8457 | - | 318888 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_SmallObject | SmallObject | 42.32 ns | 8.577 ns | 1.327 ns | 0.60 | 0.02 | 0.0076 | - | - | 48 B | 0.26 |
| DeepCloner_SmallObject | SmallObject | 70.44 ns | 5.380 ns | 1.397 ns | 1.00 | 0.03 | 0.0293 | - | - | 184 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_SmallObjectWithCollections | SmallObjectWithCollections | 301.94 ns | 14.615 ns | 3.795 ns | 0.43 | 0.01 | 0.0916 | - | - | 576 B | 0.53 |
| DeepCloner_SmallObjectWithCollections | SmallObjectWithCollections | 702.77 ns | 89.850 ns | 23.334 ns | 1.00 | 0.04 | 0.1745 | - | - | 1096 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_StringArray_1000 | StringArray | 560.76 ns | 158.421 ns | 24.516 ns | 0.83 | 0.10 | 1.2779 | - | - | 8024 B | 0.98 |
| DeepCloner_StringArray_1000 | StringArray | 686.98 ns | 339.262 ns | 88.105 ns | 1.01 | 0.16 | 1.2999 | 0.0362 | - | 8160 B | 1.00 |
|
|
Quick update, I now have the improvements backported upstream and a new utility that generates builds for internalization of FastCloner is finished. This uses Roslyn and can evaluate preprocessor symbols used by us - allowing to either remap them, or evaluate (eg. our BenchmarkDotNet v0.15.8, Windows 11 (10.0.26300.7939)
Intel Core i7-8700 CPU 3.20GHz (Max: 3.19GHz) (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.102
[Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3
Job-IJPESX : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3
IterationCount=5 LaunchCount=1 WarmupCount=3
| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|----------------------------------------------- |--------------------------- |-----------------:|-----------------:|-----------------:|------:|--------:|---------:|---------:|--------:|----------:|------------:|
| FastCloner_DynamicWithArray | DynamicWithArray | 5,813.30 ns | 6,063.814 ns | 1,574.753 ns | 0.99 | 0.25 | 0.9537 | 0.0076 | - | 6008 B | 0.68 |
| DeepCloner_DynamicWithArray | DynamicWithArray | 5,925.25 ns | 2,729.998 ns | 422.470 ns | 1.00 | 0.09 | 1.3962 | 0.0229 | - | 8800 B | 1.00 |
| FastClonerGenerated_DynamicWithArray | DynamicWithArray | 6,409.19 ns | 846.122 ns | 219.735 ns | 1.09 | 0.08 | 0.9537 | 0.0076 | - | 6008 B | 0.68 |
| | | | | | | | | | | | |
| FastClonerGenerated_DynamicWithDictionary | DynamicWithDictionary | 970.66 ns | 160.009 ns | 24.762 ns | 0.78 | 0.03 | 0.2651 | - | - | 1664 B | 0.61 |
| FastCloner_DynamicWithDictionary | DynamicWithDictionary | 1,165.39 ns | 1,016.465 ns | 157.299 ns | 0.93 | 0.11 | 0.2651 | - | - | 1664 B | 0.61 |
| DeepCloner_DynamicWithDictionary | DynamicWithDictionary | 1,247.57 ns | 137.055 ns | 35.593 ns | 1.00 | 0.04 | 0.4311 | 0.0019 | - | 2712 B | 1.00 |
| | | | | | | | | | | | |
| FastClonerGenerated_DynamicWithNestedObject | DynamicWithNestedObject | 1,227.54 ns | 127.891 ns | 19.791 ns | 0.65 | 0.07 | 0.2918 | - | - | 1840 B | 0.52 |
| FastCloner_DynamicWithNestedObject | DynamicWithNestedObject | 1,289.43 ns | 401.271 ns | 104.209 ns | 0.68 | 0.09 | 0.2918 | - | - | 1840 B | 0.52 |
| DeepCloner_DynamicWithNestedObject | DynamicWithNestedObject | 1,914.23 ns | 859.106 ns | 223.107 ns | 1.01 | 0.15 | 0.5646 | 0.0038 | - | 3560 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_FileSpec | FileSpec | 237.98 ns | 17.312 ns | 4.496 ns | 0.53 | 0.04 | 0.0663 | - | - | 416 B | 0.45 |
| FastClonerGenerated_FileSpec | FileSpec | 269.11 ns | 85.462 ns | 22.194 ns | 0.60 | 0.06 | 0.0663 | - | - | 416 B | 0.45 |
| DeepCloner_FileSpec | FileSpec | 447.15 ns | 133.748 ns | 34.734 ns | 1.00 | 0.10 | 0.1459 | - | - | 920 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_LargeEventDocument_10MB | LargeEventDocument | 34,861.69 ns | 4,824.717 ns | 1,252.964 ns | 0.50 | 0.02 | 7.9346 | 0.8545 | - | 50112 B | 0.39 |
| FastClonerGenerated_LargeEventDocument_10MB | LargeEventDocument | 35,611.13 ns | 1,679.673 ns | 259.931 ns | 0.51 | 0.01 | 8.0566 | 1.0986 | - | 50880 B | 0.39 |
| DeepCloner_LargeEventDocument_10MB | LargeEventDocument | 69,310.21 ns | 6,187.628 ns | 1,606.907 ns | 1.00 | 0.03 | 20.6299 | 4.0283 | - | 129792 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_LargeLogBatch_10MB | LargeLogBatch | 5,147,517.77 ns | 1,651,474.501 ns | 255,567.507 ns | 0.47 | 0.05 | 367.1875 | 218.7500 | 62.5000 | 2993232 B | 0.84 |
| FastClonerGenerated_LargeLogBatch_10MB | LargeLogBatch | 5,214,198.44 ns | 597,141.796 ns | 155,075.828 ns | 0.48 | 0.05 | 367.1875 | 218.7500 | 62.5000 | 2993483 B | 0.84 |
| DeepCloner_LargeLogBatch_10MB | LargeLogBatch | 11,054,202.19 ns | 4,553,765.728 ns | 1,182,598.501 ns | 1.01 | 0.15 | 484.3750 | 296.8750 | 62.5000 | 3564670 B | 1.00 |
| | | | | | | | | | | | |
| FastClonerGenerated_MediumNestedObject | MediumNestedObject | 1,159.87 ns | 517.129 ns | 134.297 ns | 0.52 | 0.06 | 0.2670 | - | - | 1680 B | 0.49 |
| FastCloner_MediumNestedObject | MediumNestedObject | 1,269.05 ns | 816.668 ns | 212.086 ns | 0.57 | 0.09 | 0.2670 | - | - | 1680 B | 0.49 |
| DeepCloner_MediumNestedObject | MediumNestedObject | 2,224.15 ns | 532.301 ns | 138.237 ns | 1.00 | 0.08 | 0.5436 | 0.0038 | - | 3416 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_ObjectDictionary_50 | ObjectDictionary | 177,222.43 ns | 9,931.593 ns | 2,579.203 ns | 0.16 | 0.00 | 37.1094 | 11.2305 | - | 233168 B | 0.42 |
| FastClonerGenerated_ObjectDictionary_50 | ObjectDictionary | 186,210.18 ns | 38,377.616 ns | 9,966.545 ns | 0.17 | 0.01 | 35.4004 | 10.2539 | - | 223568 B | 0.41 |
| DeepCloner_ObjectDictionary_50 | ObjectDictionary | 1,092,449.30 ns | 110,816.656 ns | 28,778.734 ns | 1.00 | 0.03 | 76.1719 | 39.0625 | 7.8125 | 549653 B | 1.00 |
| | | | | | | | | | | | |
| FastClonerGenerated_ObjectList_100 | ObjectList | 114,175.20 ns | 6,075.752 ns | 1,577.854 ns | 0.69 | 0.02 | 26.3672 | 7.3242 | - | 165720 B | 0.52 |
| FastCloner_ObjectList_100 | ObjectList | 118,679.80 ns | 8,062.871 ns | 2,093.902 ns | 0.72 | 0.03 | 26.3672 | 7.0801 | - | 165784 B | 0.52 |
| DeepCloner_ObjectList_100 | ObjectList | 165,527.96 ns | 40,553.032 ns | 6,275.627 ns | 1.00 | 0.05 | 50.7813 | 16.8457 | - | 318888 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_SmallObject | SmallObject | 40.10 ns | 2.455 ns | 0.380 ns | 0.57 | 0.01 | 0.0076 | - | - | 48 B | 0.26 |
| FastClonerGenerated_SmallObject | SmallObject | 56.29 ns | 2.954 ns | 0.767 ns | 0.81 | 0.01 | 0.0076 | - | - | 48 B | 0.26 |
| DeepCloner_SmallObject | SmallObject | 69.81 ns | 3.598 ns | 0.934 ns | 1.00 | 0.02 | 0.0293 | - | - | 184 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_SmallObjectWithCollections | SmallObjectWithCollections | 337.32 ns | 183.386 ns | 47.625 ns | 0.59 | 0.08 | 0.0916 | - | - | 576 B | 0.53 |
| FastClonerGenerated_SmallObjectWithCollections | SmallObjectWithCollections | 348.10 ns | 62.316 ns | 16.183 ns | 0.61 | 0.04 | 0.0916 | - | - | 576 B | 0.53 |
| DeepCloner_SmallObjectWithCollections | SmallObjectWithCollections | 570.44 ns | 107.813 ns | 27.999 ns | 1.00 | 0.06 | 0.1745 | - | - | 1096 B | 1.00 |
| | | | | | | | | | | | |
| FastCloner_StringArray_1000 | StringArray | 612.44 ns | 144.210 ns | 37.451 ns | 0.82 | 0.17 | 1.2779 | - | - | 8024 B | 0.98 |
| FastClonerGenerated_StringArray_1000 | StringArray | 626.36 ns | 371.226 ns | 96.406 ns | 0.83 | 0.21 | 1.2779 | - | - | 8024 B | 0.98 |
| DeepCloner_StringArray_1000 | StringArray | 783.26 ns | 699.819 ns | 181.741 ns | 1.04 | 0.31 | 1.2999 | 0.0362 | - | 8160 B | 1.00 | |
Summary
This PR investigates replacing Force.DeepCloner with FastCloner for our
DeepClone()implementation.Changes
Update-FastCloner.ps1script for source import automationObjectExtensions.DeepCloneto useFastClonerGenerator[NotNullIfNotNull]) to DeepClone extension method#if MODERNwith#if true // MODERNfor .NET 8+ targetsBenchmark Results
Observations
Questions
Are the published benchmarks using the source generator (
FastDeepClone()) rather than reflection (DeepClone())? The README shows ~25x improvement which we're not seeing with reflection-based cloning.Is there something specific about ARM64/Apple Silicon that might cause different performance characteristics?
Are there configuration options or usage patterns that would improve performance for complex nested objects with collections?
Test Environment
MODERNcode paths enabled)Conclusion
Based on these benchmarks, FastCloner does not appear to be a performance improvement over Force.DeepCloner for our use cases. We should either:
Note: This is a draft PR for investigation purposes. Do not merge.