Investigating: FastCloner as DeepCloner replacement#444
Investigating: FastCloner as DeepCloner replacement#444
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. |
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.