Skip to content

reduce lookups per type#32

Merged
lofcz merged 7 commits into
nextfrom
feat-type-shape
Mar 6, 2026
Merged

reduce lookups per type#32
lofcz merged 7 commits into
nextfrom
feat-type-shape

Conversation

@lofcz
Copy link
Copy Markdown
Owner

@lofcz lofcz commented Mar 6, 2026

No description provided.

Copilot AI review requested due to automatic review settings March 6, 2026 00:20
@cursor
Copy link
Copy Markdown

cursor Bot commented Mar 6, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on April 2.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces repeated reflection/cache lookups during cloning by introducing a consolidated per-type “shape” cache and by emitting a specialized clone entrypoint for sealed reference types (so the runtime type lookup can be skipped when the compile-time type is exact).

Changes:

  • Add a cached TypeShape (members, ignored-event info, cycle field types, and derived flags) and update call sites to use it.
  • Introduce CloneClassInternalExact<T> and emit it for sealed member types to avoid per-call runtime type metadata lookup.
  • Add method-info caching for the new exact sealed-type clone method.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/FastCloner/Code/StaticMethodInfos.cs Adds cached MethodInfo + generic-method cache for CloneClassInternalExact<T>.
src/FastCloner/Code/FastClonerGenerator.cs Switches multiple metadata checks to TypeShape and adds CloneClassInternalExact<T>.
src/FastCloner/Code/FastClonerExprGenerator.cs Adds TypeShape retrieval/building and emits exact clone calls for sealed member types.
src/FastCloner/Code/FastClonerCache.cs Introduces TypeShape and a dedicated cache for it; removes older separate caches.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/FastCloner/Code/FastClonerExprGenerator.cs
Comment thread src/FastCloner/Code/FastClonerCache.cs
Comment thread src/FastCloner/Code/FastClonerGenerator.cs Outdated
Comment thread src/FastCloner/Code/FastClonerExprGenerator.cs Outdated
Comment thread src/FastCloner/Code/FastClonerExprGenerator.cs
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

Deep Clone Benchmarks

  • OS: ubuntu-latest
  • Generated (UTC): 2026-03-06 07:52:41

Current FastCloner vs DeepCloner

Benchmark DeepCloner FastCloner Delta Time DC Alloc FC Alloc Delta Alloc
SmallObject 83.49 ns 74.19 ns -11% faster 184 B 48 B -74% less
FileSpec 498.06 ns 295.21 ns -41% faster 920 B 416 B -55% less
StringArray_1000 527.99 ns 530.46 ns ~same 8,160 B 8,024 B ~same
SmallObjectWithCollections 651.18 ns 364.58 ns -44% faster 1,096 B 576 B -47% less
DynamicWithDictionary 1,361.49 ns 1,059.87 ns -22% faster 2,712 B 1,600 B -41% less
MediumNestedObject 1,689.00 ns 1,083.45 ns -36% faster 3,416 B 1,616 B -53% less
DynamicWithNestedObject 1,846.59 ns 1,266.98 ns -31% faster 3,560 B 1,776 B -50% less
DynamicWithArray 5,895.13 ns 4,477.32 ns -24% faster 8,800 B 2,744 B -69% less
LargeEventDocument_10MB 71,245.88 ns 38,142.10 ns -46% faster 129,792 B 49,824 B -62% less
ObjectList_100 167,762.46 ns 107,685.51 ns -36% faster 318,888 B 149,816 B -53% less
ObjectDictionary_50 820,433.39 ns 186,022.38 ns -77% faster 549,664 B 218,768 B -60% less
LargeLogBatch_10MB 6,050,328.74 ns 3,623,986.44 ns -40% faster 3,564,729 B 2,649,082 B -26% less

FastCloner vs latest next baseline

  • Baseline generated (UTC): 2026-03-04 18:50:52
  • Regression thresholds: time > 5%, alloc > 5%
Status Benchmark Delta Time Delta Alloc
🟢 DynamicWithArray -15% faster ~same
DynamicWithDictionary +5% slower ~same
DynamicWithNestedObject ~same ~same
FileSpec -4% faster ~same
🟢 LargeEventDocument_10MB -7% faster ~same
LargeLogBatch_10MB ~same ~same
MediumNestedObject ~same ~same
ObjectDictionary_50 ~same ~same
ObjectList_100 -2% faster ~same
🟢 SmallObject -6% faster ~same
SmallObjectWithCollections -2% faster ~same
StringArray_1000 ~same ~same

Regressions

  • none

Improvements

  • DynamicWithArray: time -15% faster, alloc ~same
  • LargeEventDocument_10MB: time -7% faster, alloc ~same
  • SmallObject: time -6% faster, alloc ~same

Mixed changes

  • none

Copilot AI review requested due to automatic review settings March 6, 2026 01:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/FastCloner/Code/FastClonerExprGenerator.cs:33

  • GetClassCloneMethod now accepts memberType but does not use it, and the newly-added CloneClassInternalExact/MakeExactClassCloneMethodInfo path appears unused. If the goal is to reduce per-member runtime type/metadata lookups, consider selecting the exact generic clone method when memberType is sealed (and not shallow/no-tracking); otherwise remove the parameter/dead code.
    private static MethodInfo GetClassCloneMethod(Type memberType, bool useShallowClassClone, bool skipCycleTracking)
    {
        if (useShallowClassClone)
            return StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack;


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/FastCloner/Code/FastClonerExprGenerator.cs
Comment thread src/FastCloner.Tests/CircularTests.cs
Comment thread src/FastCloner/Code/Polyfill.cs
Copilot AI review requested due to automatic review settings March 6, 2026 01:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/FastCloner/Code/FastClonerExprGenerator.cs:37

  • GetClassCloneMethod now takes memberType, but it’s not used in the method body. As a result, the new CloneClassInternalExact / MakeExactClassCloneMethodInfo path is never selected anywhere in the codebase (verified by searching for MakeExactClassCloneMethodInfo usages). Either wire memberType in here (e.g., use the exact generic cloner for sealed member types to reduce runtime type/metadata lookups) or drop the parameter/new cache to avoid dead code and confusion.
    private static MethodInfo GetClassCloneMethod(Type memberType, bool useShallowClassClone, bool skipCycleTracking)
    {
        if (useShallowClassClone)
            return StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack;

        return skipCycleTracking
            ? StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternalNoTracking
            : StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternal;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsExternalInit is declared with a trailing semicolon and no body, which is not valid C# syntax. This will break compilation for the non-MODERN TFMs (net46/netstandard2.0) that need the init-only setter polyfill. Define it with an empty body instead (the usual internal static class IsExternalInit { }).

Suggested change
internal static class IsExternalInit;
internal static class IsExternalInit
{
}

Copilot uses AI. Check for mistakes.
Comment on lines +618 to +626
ClonerCache<T>.CacheEntry cacheEntry = ClonerCache<T>.GetCurrent();
if (!FastClonerCache.HasActiveTypeBehaviorOverrides && cacheEntry.IsSafe)
return obj;

Type objType = typeof(T);
FastClonerCache.TypeCloneMetadata metadata = cacheEntry.Metadata ?? GetTypeMetadata(objType, state);

// Keep exact and polymorphic class cloning on the same decision path.
return (T?)CloneClassInternalTyped(obj, objType, state, metadata);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloneClassInternalExact<T> uses typeof(T) for metadata/cloner selection, which will produce incorrect results if it’s ever called with a derived runtime instance (e.g., T is a base type but obj is a subclass): derived fields/overrides and per-type behaviors would be skipped. Consider enforcing the contract (e.g., check obj.GetType() == typeof(T) and fall back to CloneClassInternal/polymorphic path if not), or otherwise ensure it’s only callable for sealed/exact types (at least via a debug assert).

Suggested change
ClonerCache<T>.CacheEntry cacheEntry = ClonerCache<T>.GetCurrent();
if (!FastClonerCache.HasActiveTypeBehaviorOverrides && cacheEntry.IsSafe)
return obj;
Type objType = typeof(T);
FastClonerCache.TypeCloneMetadata metadata = cacheEntry.Metadata ?? GetTypeMetadata(objType, state);
// Keep exact and polymorphic class cloning on the same decision path.
return (T?)CloneClassInternalTyped(obj, objType, state, metadata);
Type runtimeType = obj.GetType();
Type declaredType = typeof(T);
// If the runtime type is not exactly T, fall back to the polymorphic path
// to ensure derived fields/overrides and per-type behaviors are respected.
if (runtimeType != declaredType)
return (T?)CloneClassInternal(obj, state);
ClonerCache<T>.CacheEntry cacheEntry = ClonerCache<T>.GetCurrent();
if (!FastClonerCache.HasActiveTypeBehaviorOverrides && cacheEntry.IsSafe)
return obj;
FastClonerCache.TypeCloneMetadata metadata = cacheEntry.Metadata ?? GetTypeMetadata(declaredType, state);
// Keep exact and polymorphic class cloning on the same decision path.
return (T?)CloneClassInternalTyped(obj, declaredType, state, metadata);

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 6, 2026 07:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

src/FastCloner/Code/FastClonerGenerator.cs:236

  • BuildTypeMetadata computes hasBehaviorSensitiveMembers, but it’s not considered when selecting CyclePolicy. Using Worklist for types with ignored members can be incorrect because the worklist path starts with a shallow MemberwiseClone and the patching step (ClonerToExprGenerator) does not currently apply member-level Ignore semantics (defaults), leaving ignored fields copied from the source. Consider falling back to TrackReferences when hasBehaviorSensitiveMembers is true, or ensure the worklist patching step explicitly resets ignored members to default.
        FastClonerCache.CyclePolicy cyclePolicy = !canHaveCycles
            ? FastClonerCache.CyclePolicy.None
            : hasDirectSelfReference
                ? FastClonerCache.CyclePolicy.Worklist
                : FastClonerCache.CyclePolicy.TrackReferences;

src/FastCloner/Code/FastClonerExprGenerator.cs:37

  • GetClassCloneMethod now takes a memberType parameter but doesn’t use it. This adds noise and suggests there’s member-type-specific behavior that doesn’t exist; either remove the parameter or use it (if intended) to avoid confusion for future maintainers.
    private static MethodInfo GetClassCloneMethod(Type memberType, bool useShallowClassClone, bool skipCycleTracking)
    {
        if (useShallowClassClone)
            return StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack;

        return skipCycleTracking
            ? StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternalNoTracking
            : StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassInternal;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

/// Performs deep (full) copy of object and related graph
/// </summary>
public T DeepClone() => FastClonerGenerator.CloneObject(obj);
public T DeepClone() => FastClonerGenerator.CloneObject(obj)!;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeepClone() returns non-null T, but FastClonerGenerator.CloneObject(obj) is nullable (T?) and returns default when the receiver is null. The added null-forgiving operator hides that and makes the nullability contract incorrect for callers. Consider changing the extension method signature to return T? (or explicitly throw on null) instead of using !.

Suggested change
public T DeepClone() => FastClonerGenerator.CloneObject(obj)!;
public T? DeepClone() => FastClonerGenerator.CloneObject(obj);

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +29
public TTo DeepCloneTo<TTo>(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, true)!;

/// <summary>
/// Performs shallow copy of object to existing object
/// </summary>
/// <returns>existing filled object</returns>
/// <remarks>Method is valid only for classes, classes should be descendants in reality, not in declaration</remarks>
public TTo ShallowCloneTo<TTo>(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, false);
public TTo ShallowCloneTo<TTo>(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, false)!;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeepCloneTo/ShallowCloneTo cast the result to TTo and apply !, but CloneObjectTo(...) can return null (e.g., when objTo is null) and these methods are callable with a null receiver. Either adjust the signatures to return TTo? and remove the null-forgiving operator, or enforce non-null inputs (throw) so the non-null return type is truthful.

Copilot uses AI. Check for mistakes.
/// Performs shallow (only new object returned, without cloning of dependencies) copy of object
/// </summary>
public T ShallowClone() => ShallowClonerGenerator.CloneObject(obj);
public T ShallowClone() => ShallowClonerGenerator.CloneObject(obj)!;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShallowClone() returns non-null T, but ShallowClonerGenerator.CloneObject(obj) returns T? and returns default when the receiver is null. The ! masks that and produces an incorrect nullability contract; prefer returning T? (or throwing on null input) instead of using the null-forgiving operator.

Suggested change
public T ShallowClone() => ShallowClonerGenerator.CloneObject(obj)!;
public T? ShallowClone() => ShallowClonerGenerator.CloneObject(obj);

Copilot uses AI. Check for mistakes.
/// Internal helper class used to perform shallow object cloning
/// </summary>
public abstract class ShallowObjectCloner
internal static class ShallowObjectCloner
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShallowObjectCloner was changed from public to internal, which is a breaking change for any external consumers referencing this type (even if it’s “not intended” public API). If this needs to be internal, consider keeping a public shim/obsolete type for compatibility, or confirm this is acceptable as a major-version breaking change.

Suggested change
internal static class ShallowObjectCloner
public static class ShallowObjectCloner

Copilot uses AI. Check for mistakes.
@lofcz lofcz merged commit 7279709 into next Mar 6, 2026
11 checks passed
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.

2 participants