Skip to content

[DO NOT MERGE] FastCloner as DeepCloner replacement#469

Draft
lofcz wants to merge 1 commit intoFoundatioFx:mainfrom
lofcz:feat-fast-cloner
Draft

[DO NOT MERGE] FastCloner as DeepCloner replacement#469
lofcz wants to merge 1 commit intoFoundatioFx:mainfrom
lofcz:feat-fast-cloner

Conversation

@lofcz
Copy link

@lofcz lofcz commented Mar 4, 2026

This supersedes #444, I recommend checking that PR first for context and discussion.

FastCloner is a modern cloning library. I was approached by @niemyjski to see if we could get this in Foundatio in lieu of DeepCloner, which at this point is fairly antique.

What's in it for Foundatio?

  • From my perspective, primary correctness. We have over 800 tests. DeepCloner has ~200. This is the reason FastCloner even exists. I'm surprised there aren't more complaints about cloning in your issues, maybe people just don't do it that often..
  • Potential to bring in the source generator in a follow-up PR. It can coexist with the runtime cloning library (this PR) and resolve graphs mostly AOT with just selected members being runtime cloned. FastCloner generates code that is often better than by-hand cloning, because it doesn't shy away from generating a lot of boilerplate
  • Reduced allocations & faster runtime cloning now
  • Clean upgrade path for pulling upstream bug fixes
  • Foundatio could decide to make certain things public later - customizing cloning behavior per member/type and other cool tricks are just internal->public away
  • You aren't using a dead library! Nice day for fishing, ain't it..

What's in it for FastCloner?

  • I'd like for the library to become the de facto cloning standard. While Foundatio is not the first big adopter, it's certainly a recognizable name in certain circles

Proposed path to adoption

  • Initial try on Foundatio's benchmark
  • Side by side benchmarks with DeepCloner
  • Foundatio's & FastCloner's tests pass
  • Verify results on macOS @niemyjski
  • Remove DeepCloner
  • :shipit:

Benchmarks

Benchmark DeepCloner FastCloner Δ Time DC Alloc FC Alloc Δ Alloc
SmallObject 71.89 ns 57.47 ns -20% faster 184 B 48 B -74% less
FileSpec 433.34 ns 264.46 ns -39% faster 920 B 416 B -55% less
SmallObjectWithCollections 563.78 ns 333.38 ns -41% faster 1,096 B 576 B -47% less
StringArray_1000 615.59 ns 607.97 ns ~same 8,160 B 8,024 B -2% less
DynamicWithDictionary 1,190.02 ns 926.69 ns -22% faster 2,712 B 1,600 B -41% less
MediumNestedObject 1,450.22 ns 985.14 ns -32% faster 3,416 B 1,616 B -53% less
DynamicWithNestedObject 1,618.30 ns 1,190.47 ns -26% faster 3,560 B 1,776 B -50% less
DynamicWithArray 5,221.34 ns 3,290.43 ns -37% faster 8,800 B 2,840 B -68% less
LargeEventDocument_10MB 68,898.55 ns 33,269.37 ns -52% faster 129,792 B 49,824 B -62% less
ObjectList_100 164,443.49 ns 96,483.25 ns -41% faster 318,888 B 149,816 B -53% less
ObjectDictionary_50 1,164,784.56 ns 167,281.23 ns -86% faster 549,653 B 218,768 B -60% less
LargeLogBatch_10MB 9,345,106.49 ns 5,524,640.05 ns -41% faster 3,564,668 B 2,649,086 B -26% less
Method Categories Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
FastCloner_DynamicWithArray DynamicWithArray 3,290.43 ns 63.259 ns 77.688 ns 0.63 0.02 0.4501 0.0038 - 2840 B 0.32
DeepCloner_DynamicWithArray DynamicWithArray 5,221.34 ns 99.428 ns 97.652 ns 1.00 0.03 1.3962 0.0229 - 8800 B 1.00
FastCloner_DynamicWithDictionary DynamicWithDictionary 926.69 ns 18.471 ns 25.283 ns 0.78 0.04 0.2537 - - 1600 B 0.59
DeepCloner_DynamicWithDictionary DynamicWithDictionary 1,190.02 ns 23.234 ns 52.444 ns 1.00 0.06 0.4311 0.0019 - 2712 B 1.00
FastCloner_DynamicWithNestedObject DynamicWithNestedObject 1,190.47 ns 23.740 ns 35.533 ns 0.74 0.03 0.2823 - - 1776 B 0.50
DeepCloner_DynamicWithNestedObject DynamicWithNestedObject 1,618.30 ns 32.349 ns 45.348 ns 1.00 0.04 0.5665 0.0038 - 3560 B 1.00
FastCloner_FileSpec FileSpec 264.46 ns 5.145 ns 7.857 ns 0.61 0.02 0.0663 - - 416 B 0.45
DeepCloner_FileSpec FileSpec 433.34 ns 7.841 ns 6.951 ns 1.00 0.02 0.1464 - - 920 B 1.00
FastCloner_LargeEventDocument_10MB LargeEventDocument 33,269.37 ns 642.951 ns 660.263 ns 0.48 0.01 7.9346 1.0376 - 49824 B 0.38
DeepCloner_LargeEventDocument_10MB LargeEventDocument 68,898.55 ns 1,340.616 ns 1,543.855 ns 1.00 0.03 20.6299 4.0283 - 129792 B 1.00
FastCloner_LargeLogBatch_10MB LargeLogBatch 5,524,640.05 ns 130,344.687 ns 384,324.278 ns 0.59 0.05 359.3750 242.1875 54.6875 2649086 B 0.74
DeepCloner_LargeLogBatch_10MB LargeLogBatch 9,345,106.49 ns 184,868.458 ns 428,460.795 ns 1.00 0.06 484.3750 296.8750 62.5000 3564668 B 1.00
FastCloner_MediumNestedObject MediumNestedObject 985.14 ns 19.576 ns 28.075 ns 0.68 0.02 0.2575 - - 1616 B 0.47
DeepCloner_MediumNestedObject MediumNestedObject 1,450.22 ns 22.793 ns 17.795 ns 1.00 0.02 0.5436 0.0038 - 3416 B 1.00
FastCloner_ObjectDictionary_50 ObjectDictionary 167,281.23 ns 3,257.019 ns 2,719.758 ns 0.14 0.01 34.6680 11.2305 - 218768 B 0.40
DeepCloner_ObjectDictionary_50 ObjectDictionary 1,164,784.56 ns 23,026.387 ns 53,367.157 ns 1.00 0.06 76.1719 39.0625 7.8125 549653 B 1.00
FastCloner_ObjectList_100 ObjectList 96,483.25 ns 1,913.336 ns 2,349.748 ns 0.59 0.02 23.8037 5.9814 - 149816 B 0.47
DeepCloner_ObjectList_100 ObjectList 164,443.49 ns 3,063.628 ns 5,445.596 ns 1.00 0.05 50.7813 16.8457 - 318888 B 1.00
FastCloner_SmallObject SmallObject 57.47 ns 1.128 ns 1.617 ns 0.80 0.03 0.0076 - - 48 B 0.26
DeepCloner_SmallObject SmallObject 71.89 ns 1.500 ns 2.465 ns 1.00 0.05 0.0293 - - 184 B 1.00
FastCloner_SmallObjectWithCollections SmallObjectWithCollections 333.38 ns 6.658 ns 7.400 ns 0.59 0.02 0.0916 - - 576 B 0.53
DeepCloner_SmallObjectWithCollections SmallObjectWithCollections 563.78 ns 10.726 ns 19.066 ns 1.00 0.05 0.1745 - - 1096 B 1.00
FastCloner_StringArray_1000 StringArray 607.97 ns 12.860 ns 37.309 ns 0.99 0.08 1.2779 - - 8024 B 0.98
DeepCloner_StringArray_1000 StringArray 615.59 ns 12.340 ns 35.207 ns 1.00 0.08 1.2999 0.0362 - 8160 B 1.00

Updating

One liner, replace USERNAME, run from /src in local FastCloner clone:

dotnet build FastCloner.Internalization.Builder/FastCloner.Internalization.Builder.csproj && dotnet run --project FastCloner.Internalization.Builder/FastCloner.Internalization.Builder.csproj -- --root-namespace Foundatio.FastCloner --output "C:\Users\USERNAME\Documents\GitHub\Foundatio\src\Foundatio\FastCloner" --preprocessor "MODERN=true;NET5_0_OR_GREATER=true;NET6_0_OR_GREATER=true;NET8_0_OR_GREATER=true" --visibility internal --public-api none --runtime-only true --self-check

@CLAassistant
Copy link

CLAassistant commented Mar 4, 2026

CLA assistant check
All committers have signed the CLA.

@lofcz lofcz force-pushed the feat-fast-cloner branch 2 times, most recently from 8309c06 to bc1af18 Compare March 4, 2026 01:40
@niemyjski
Copy link
Member

Thank you so much for taking the time to create this pr, will definitely look into this very very soon.

@lofcz lofcz marked this pull request as draft March 4, 2026 02:03
@lofcz
Copy link
Author

lofcz commented Mar 4, 2026

Btw, your test CanRunQueueJobWithLockFailAsync is flaky, if it fails, it is unrelated to this PR. The test can be seen failing in #444 too and it happened to me locally as well.

- 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.

reduce to fastcloner vs deepcloner

internals

rebase

use generated fastcloner code

group benchmarks, simplify position tracking

work on perf

remove experiment

return original DeepCloner into source code until we sort this out for reproducible benchmarks

remove behaviors, add licenses

sync fastcloner
@lofcz lofcz force-pushed the feat-fast-cloner branch from 7457f03 to 6b91c07 Compare March 4, 2026 11:41
@lofcz
Copy link
Author

lofcz commented Mar 4, 2026

Benchmark results updated, FastCloner is now consistently faster in every measured category. StringArray_1000 is effecively memcpy + whatever both libs do before that, so that will stay at the same speed.

@lofcz
Copy link
Author

lofcz commented Mar 4, 2026

FastCloner now runs reproducible benchmarks on each change: lofcz/FastCloner#31 (comment)

@niemyjski
Copy link
Member

Btw, your test CanRunQueueJobWithLockFailAsync is flaky, if it fails, it is unrelated to this PR. The test can be seen failing in #444 too and it happened to me locally as well.

Fixed via #470 Thanks for letting us know.

Will look at this pr, hopefully today or tomorrow! Thanks again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants