diff --git a/src/FastCloner.Benchmark.CI/Reporting/BenchmarkDiffReporter.cs b/src/FastCloner.Benchmark.CI/Reporting/BenchmarkDiffReporter.cs index 9b61003..47e52cf 100644 --- a/src/FastCloner.Benchmark.CI/Reporting/BenchmarkDiffReporter.cs +++ b/src/FastCloner.Benchmark.CI/Reporting/BenchmarkDiffReporter.cs @@ -87,30 +87,54 @@ private static BaselineDiffReport CreateDiffReport( { if (!baselinePairs.TryGetValue(currentPair.Benchmark, out BenchmarkPair? baselinePair)) { + double? newBenchmarkTimeScore = SafeRatio(currentPair.FastCloner.MeanNanoseconds, currentPair.DeepCloner.MeanNanoseconds); + double? newBenchmarkAllocScore = SafeRatio(currentPair.FastCloner.AllocatedBytes, currentPair.DeepCloner.AllocatedBytes); + items.Add(new BaselineDiffItem( Benchmark: currentPair.Benchmark, - BaselineMeanNanoseconds: null, - CurrentMeanNanoseconds: currentPair.FastCloner.MeanNanoseconds, - TimeDeltaRatio: null, - BaselineAllocatedBytes: null, - CurrentAllocatedBytes: currentPair.FastCloner.AllocatedBytes, - AllocDeltaRatio: null, + BaselineFastClonerMeanNanoseconds: null, + BaselineDeepClonerMeanNanoseconds: null, + CurrentFastClonerMeanNanoseconds: currentPair.FastCloner.MeanNanoseconds, + CurrentDeepClonerMeanNanoseconds: currentPair.DeepCloner.MeanNanoseconds, + BaselineTimeScore: null, + CurrentTimeScore: newBenchmarkTimeScore, + TimeScoreDeltaRatio: null, + BaselineFastClonerAllocatedBytes: null, + BaselineDeepClonerAllocatedBytes: null, + CurrentFastClonerAllocatedBytes: currentPair.FastCloner.AllocatedBytes, + CurrentDeepClonerAllocatedBytes: currentPair.DeepCloner.AllocatedBytes, + BaselineAllocScore: null, + CurrentAllocScore: newBenchmarkAllocScore, + AllocScoreDeltaRatio: null, Status: DiffStatus.NewBenchmark)); continue; } - double? timeDelta = SafeDelta(currentPair.FastCloner.MeanNanoseconds, baselinePair.FastCloner.MeanNanoseconds); - double? allocDelta = SafeDelta(currentPair.FastCloner.AllocatedBytes, baselinePair.FastCloner.AllocatedBytes); + double? baselineTimeScore = SafeRatio(baselinePair.FastCloner.MeanNanoseconds, baselinePair.DeepCloner.MeanNanoseconds); + double? currentTimeScore = SafeRatio(currentPair.FastCloner.MeanNanoseconds, currentPair.DeepCloner.MeanNanoseconds); + double? baselineAllocScore = SafeRatio(baselinePair.FastCloner.AllocatedBytes, baselinePair.DeepCloner.AllocatedBytes); + double? currentAllocScore = SafeRatio(currentPair.FastCloner.AllocatedBytes, currentPair.DeepCloner.AllocatedBytes); + + double? timeDelta = SafeDelta(currentTimeScore, baselineTimeScore); + double? allocDelta = SafeDelta(currentAllocScore, baselineAllocScore); DiffStatus status = ClassifyDiff(timeDelta, allocDelta, timeThreshold, allocThreshold, sameThreshold); items.Add(new BaselineDiffItem( Benchmark: currentPair.Benchmark, - BaselineMeanNanoseconds: baselinePair.FastCloner.MeanNanoseconds, - CurrentMeanNanoseconds: currentPair.FastCloner.MeanNanoseconds, - TimeDeltaRatio: timeDelta, - BaselineAllocatedBytes: baselinePair.FastCloner.AllocatedBytes, - CurrentAllocatedBytes: currentPair.FastCloner.AllocatedBytes, - AllocDeltaRatio: allocDelta, + BaselineFastClonerMeanNanoseconds: baselinePair.FastCloner.MeanNanoseconds, + BaselineDeepClonerMeanNanoseconds: baselinePair.DeepCloner.MeanNanoseconds, + CurrentFastClonerMeanNanoseconds: currentPair.FastCloner.MeanNanoseconds, + CurrentDeepClonerMeanNanoseconds: currentPair.DeepCloner.MeanNanoseconds, + BaselineTimeScore: baselineTimeScore, + CurrentTimeScore: currentTimeScore, + TimeScoreDeltaRatio: timeDelta, + BaselineFastClonerAllocatedBytes: baselinePair.FastCloner.AllocatedBytes, + BaselineDeepClonerAllocatedBytes: baselinePair.DeepCloner.AllocatedBytes, + CurrentFastClonerAllocatedBytes: currentPair.FastCloner.AllocatedBytes, + CurrentDeepClonerAllocatedBytes: currentPair.DeepCloner.AllocatedBytes, + BaselineAllocScore: baselineAllocScore, + CurrentAllocScore: currentAllocScore, + AllocScoreDeltaRatio: allocDelta, Status: status)); } @@ -203,13 +227,13 @@ private static string BuildCommentMarkdown( sb.AppendLine($"- Baseline generated (UTC): `{baseline.GeneratedAtUtc:yyyy-MM-dd HH:mm:ss}`"); sb.AppendLine($"- Regression thresholds: time > `{FormatPercent(options.TimeThreshold)}`, alloc > `{FormatPercent(options.AllocThreshold)}`"); sb.AppendLine(); - sb.AppendLine("| Status | Benchmark | FC Time (Baseline) | FC Time (Current) | Delta Time | FC Alloc (Baseline) | FC Alloc (Current) | Delta Alloc |"); - sb.AppendLine("|---|---|---:|---:|---|---:|---:|---|"); + sb.AppendLine("| Status | Benchmark | Delta Time | Delta Alloc |"); + sb.AppendLine("|---|---|---|---|"); foreach (BaselineDiffItem item in diff.Items) { sb.AppendLine( - $"| {FormatStatus(item.Status)} | {item.Benchmark} | {FormatNanoseconds(item.BaselineMeanNanoseconds)} | {FormatNanoseconds(item.CurrentMeanNanoseconds)} | {FormatCurrentDelta(item.TimeDeltaRatio, options.SameThreshold, "faster", "slower")} | {FormatBytes(item.BaselineAllocatedBytes)} | {FormatBytes(item.CurrentAllocatedBytes)} | {FormatCurrentDelta(item.AllocDeltaRatio, options.SameThreshold, "less", "more")} |"); + $"| {FormatStatus(item.Status)} | {item.Benchmark} | {FormatCurrentDelta(item.TimeScoreDeltaRatio, options.SameThreshold, "faster", "slower")} | {FormatCurrentDelta(item.AllocScoreDeltaRatio, options.SameThreshold, "less", "more")} |"); } AppendStatusSection(sb, "Regressions", diff.Items.Where(item => item.Status == DiffStatus.Regression).ToList(), options.SameThreshold); @@ -270,12 +294,20 @@ private static void AppendStatusSection( sb.AppendLine(); foreach (BaselineDiffItem item in items) { - string timeDelta = FormatCurrentDelta(item.TimeDeltaRatio, sameThreshold, "faster", "slower"); - string allocDelta = FormatCurrentDelta(item.AllocDeltaRatio, sameThreshold, "less", "more"); + string timeDelta = FormatCurrentDelta(item.TimeScoreDeltaRatio, sameThreshold, "faster", "slower"); + string allocDelta = FormatCurrentDelta(item.AllocScoreDeltaRatio, sameThreshold, "less", "more"); sb.AppendLine($"- `{item.Benchmark}`: time {timeDelta}, alloc {allocDelta}"); } } + private static double? SafeDelta(double? current, double? baseline) + { + if (current is null || baseline is null || baseline <= 0d) + return null; + + return (current.Value - baseline.Value) / baseline.Value; + } + private static double? SafeDelta(double current, double baseline) { if (baseline <= 0) @@ -292,6 +324,22 @@ private static void AppendStatusSection( return (current - (double)baseline) / baseline; } + private static double? SafeRatio(double numerator, double denominator) + { + if (denominator <= 0d) + return null; + + return numerator / denominator; + } + + private static double? SafeRatio(long numerator, long denominator) + { + if (denominator <= 0) + return null; + + return numerator / (double)denominator; + } + private static string FormatCurrentDelta(double? ratio, double sameThreshold, string betterWord, string worseWord) { if (ratio is null || double.IsNaN(ratio.Value) || double.IsInfinity(ratio.Value)) @@ -324,6 +372,14 @@ private static string FormatBytes(long? value) return $"{value.Value.ToString("N0", CultureInfo.InvariantCulture)} B"; } + private static string FormatScore(double? value) + { + if (value is null || double.IsNaN(value.Value) || double.IsInfinity(value.Value)) + return "n/a"; + + return $"{value.Value.ToString("0.000", CultureInfo.InvariantCulture)}x"; + } + private static string FormatPercent(double ratio) { return $"{(ratio * 100d).ToString("0.##", CultureInfo.InvariantCulture)}%"; @@ -461,12 +517,20 @@ internal sealed record BaselineDiffReport( internal sealed record BaselineDiffItem( string Benchmark, - double? BaselineMeanNanoseconds, - double CurrentMeanNanoseconds, - double? TimeDeltaRatio, - long? BaselineAllocatedBytes, - long CurrentAllocatedBytes, - double? AllocDeltaRatio, + double? BaselineFastClonerMeanNanoseconds, + double? BaselineDeepClonerMeanNanoseconds, + double CurrentFastClonerMeanNanoseconds, + double CurrentDeepClonerMeanNanoseconds, + double? BaselineTimeScore, + double? CurrentTimeScore, + double? TimeScoreDeltaRatio, + long? BaselineFastClonerAllocatedBytes, + long? BaselineDeepClonerAllocatedBytes, + long CurrentFastClonerAllocatedBytes, + long CurrentDeepClonerAllocatedBytes, + double? BaselineAllocScore, + double? CurrentAllocScore, + double? AllocScoreDeltaRatio, DiffStatus Status); internal enum DiffStatus diff --git a/src/FastCloner.Internalization.Builder/Program.cs b/src/FastCloner.Internalization.Builder/Program.cs index 9a60b96..c3772c3 100644 --- a/src/FastCloner.Internalization.Builder/Program.cs +++ b/src/FastCloner.Internalization.Builder/Program.cs @@ -354,6 +354,8 @@ private static void PrintUsage() Example: MODERN=true;NET8_0_OR_GREATER=true MODERN=true;SOMETHING=random_text + Default replacements: + MODERN_10=NET10_0_OR_GREATER Special values: true / false (recognized booleans) Non-boolean values are used as direct token replacement in #if expressions. @@ -389,6 +391,11 @@ internal enum PublicApiMode internal sealed class BuildOptions { + private static readonly IReadOnlyDictionary DefaultPreprocessor = new Dictionary(StringComparer.Ordinal) + { + ["MODERN_10"] = "NET10_0_OR_GREATER" + }; + public required string RootNamespace { get; init; } public required string Output { get; init; } public required string InputRoot { get; init; } @@ -475,7 +482,7 @@ public HashSet GetPreprocessorSymbols() private static Dictionary ParsePreprocessor(string raw) { - Dictionary map = new(StringComparer.Ordinal); + Dictionary map = new(DefaultPreprocessor, StringComparer.Ordinal); if (String.IsNullOrWhiteSpace(raw)) return map; diff --git a/src/FastCloner.Tests/CircularTests.cs b/src/FastCloner.Tests/CircularTests.cs index f82a2ec..e5d9d37 100644 --- a/src/FastCloner.Tests/CircularTests.cs +++ b/src/FastCloner.Tests/CircularTests.cs @@ -1,3 +1,5 @@ +using FastCloner.Code; + namespace FastCloner.Tests; [TestFixture(Low)] @@ -21,6 +23,12 @@ public class C1 public C1 A { get; set; } } + public sealed class SealedLoop + { + public int Value { get; set; } + public SealedLoop? Next { get; set; } + } + [Test] public void SimpleLoop_Should_Be_Handled() { @@ -71,6 +79,19 @@ public void Object_Own_Loop_Should_Be_Handled() Assert.That(cloned.A, Is.EqualTo(cloned)); } + [Test] + public void Sealed_Object_Own_Loop_Should_Be_Handled() + { + SealedLoop root = new SealedLoop { Value = 1 }; + root.Next = root; + + SealedLoop cloned = root.DeepClone(); + + Assert.That(cloned, Is.Not.SameAs(root)); + Assert.That(cloned.Next, Is.SameAs(cloned)); + Assert.That(cloned.Value, Is.EqualTo(1)); + } + [Test] public void Array_Of_Same_Objects_Should_Be_Cloned() { @@ -97,4 +118,35 @@ public void StructWrappedReferenceLoop_Should_Be_Handled() Assert.That(cloned.W.Ref, Is.Not.Null); Assert.That(cloned.W.Ref, Is.EqualTo(cloned)); } + + [Test] + public void Internal_CloneClassInternal_Should_Reset_CallDepth_After_WorkList_Switch() + { + C1 root = new C1 + { + F = 1, + A = new C1 + { + F = 2, + A = new C1 { F = 3 } + } + }; + + int previousMaxRecursionDepth = FastCloner.MaxRecursionDepth; + FastCloner.MaxRecursionDepth = 1; + FastCloneState state = FastCloneState.Rent(); + try + { + C1? clone = (C1?)FastClonerGenerator.CloneClassInternal(root, state); + + Assert.That(clone, Is.Not.Null); + Assert.That(state.UseWorkList, Is.True); + Assert.That(state.CurrentDepth, Is.EqualTo(0)); + } + finally + { + FastCloneState.Return(state); + FastCloner.MaxRecursionDepth = previousMaxRecursionDepth; + } + } } \ No newline at end of file diff --git a/src/FastCloner.Tests/DynamicMaxRecursionDepthTests.cs b/src/FastCloner.Tests/DynamicMaxRecursionDepthTests.cs index d9d9e28..d201910 100644 --- a/src/FastCloner.Tests/DynamicMaxRecursionDepthTests.cs +++ b/src/FastCloner.Tests/DynamicMaxRecursionDepthTests.cs @@ -1,8 +1,16 @@ +using FastCloner.Code; + namespace FastCloner.Tests; [TestFixture] public class DynamicMaxRecursionDepthTests { + public sealed class ExactNode + { + public int Value { get; set; } + public ExactNode? Child { get; set; } + } + public class A { public List Bs = []; @@ -120,4 +128,74 @@ public void TestDeepObjectGraph_1500Levels_WithMRD1000() Assert.That(level, Is.GreaterThan(maxRecursionDepth), $"Verified {level} levels"); } + + [Test] + public void TestDeepExactObjectGraph_1500Levels_WithMRD1() + { + const int nestLevel = 1500; + + ExactNode root = new ExactNode { Value = 0 }; + ExactNode current = root; + + for (int i = 1; i <= nestLevel; i++) + { + ExactNode next = new ExactNode { Value = i }; + current.Child = next; + current = next; + } + + FastCloner.MaxRecursionDepth = 1; + ExactNode? clone = FastCloner.DeepClone(root); + + Assert.That(clone, Is.Not.Null); + Assert.That(clone, Is.Not.SameAs(root)); + + ExactNode? originalCurrent = root; + ExactNode? cloneCurrent = clone; + int verifiedLevels = 0; + while (originalCurrent is not null && cloneCurrent is not null) + { + Assert.That(cloneCurrent, Is.Not.SameAs(originalCurrent), $"Node at level {verifiedLevels} should be cloned"); + Assert.That(cloneCurrent.Value, Is.EqualTo(originalCurrent.Value), $"Value at level {verifiedLevels} should match"); + + originalCurrent = originalCurrent.Child; + cloneCurrent = cloneCurrent.Child; + verifiedLevels++; + } + + Assert.That(verifiedLevels, Is.EqualTo(nestLevel + 1)); + Assert.That(cloneCurrent, Is.Null); + Assert.That(originalCurrent, Is.Null); + } + + [Test] + public void Internal_CloneClassInternal_Should_Reset_CallDepth_After_WorkList_Switch() + { + ExactNode root = new ExactNode + { + Value = 1, + Child = new ExactNode + { + Value = 2, + Child = new ExactNode { Value = 3 } + } + }; + + int previousMaxRecursionDepth = FastCloner.MaxRecursionDepth; + FastCloner.MaxRecursionDepth = 1; + FastCloneState state = FastCloneState.Rent(); + try + { + ExactNode? clone = (ExactNode?)FastClonerGenerator.CloneClassInternal(root, state); + + Assert.That(clone, Is.Not.Null); + Assert.That(state.UseWorkList, Is.True); + Assert.That(state.CurrentDepth, Is.EqualTo(0)); + } + finally + { + FastCloneState.Return(state); + FastCloner.MaxRecursionDepth = previousMaxRecursionDepth; + } + } } \ No newline at end of file diff --git a/src/FastCloner/AssemblyInfo.cs b/src/FastCloner/AssemblyInfo.cs index 9bffad1..6407892 100644 --- a/src/FastCloner/AssemblyInfo.cs +++ b/src/FastCloner/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("FastCloner.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100bd882e7dcc8a5cfdd6e2193c66bb144ab67e83964b825035b4b05ccdc40a5a7218a497799f98f98e628c38492fa227ad1579c7d701934ea2e30459a4dabfcfa498fc9f4dc6d3e3f118e4df6615aced3da480ea45d30832ddbfd56acebc957ee6345944d2e9b82705a725276146baadf08cf7a8612fd4f3ceb8b8ec9529b975c2")] -[assembly: InternalsVisibleTo("FastCloner.Contrib, PublicKey=0024000004800000940000000602000000240000525341310004000001000100bd882e7dcc8a5cfdd6e2193c66bb144ab67e83964b825035b4b05ccdc40a5a7218a497799f98f98e628c38492fa227ad1579c7d701934ea2e30459a4dabfcfa498fc9f4dc6d3e3f118e4df6615aced3da480ea45d30832ddbfd56acebc957ee6345944d2e9b82705a725276146baadf08cf7a8612fd4f3ceb8b8ec9529b975c2")] -namespace FastCloner; \ No newline at end of file +[assembly: InternalsVisibleTo("FastCloner.Contrib, PublicKey=0024000004800000940000000602000000240000525341310004000001000100bd882e7dcc8a5cfdd6e2193c66bb144ab67e83964b825035b4b05ccdc40a5a7218a497799f98f98e628c38492fa227ad1579c7d701934ea2e30459a4dabfcfa498fc9f4dc6d3e3f118e4df6615aced3da480ea45d30832ddbfd56acebc957ee6345944d2e9b82705a725276146baadf08cf7a8612fd4f3ceb8b8ec9529b975c2")] \ No newline at end of file diff --git a/src/FastCloner/Code/FastCloneState.cs b/src/FastCloner/Code/FastCloneState.cs index 1450623..dac3de7 100644 --- a/src/FastCloner/Code/FastCloneState.cs +++ b/src/FastCloner/Code/FastCloneState.cs @@ -96,6 +96,7 @@ private FastCloneState(bool trackReferences = true) private int workCount; public bool UseWorkList { get; set; } private int callDepth; + internal int CurrentDepth => callDepth; private readonly Type?[] metadataTypes = new Type?[MetadataCacheSize]; private readonly FastClonerCache.TypeCloneMetadata[] metadataValues = new FastClonerCache.TypeCloneMetadata[MetadataCacheSize]; private int metadataWriteIndex; diff --git a/src/FastCloner/Code/FastClonerCache.cs b/src/FastCloner/Code/FastClonerCache.cs index feeb373..736d1bc 100644 --- a/src/FastCloner/Code/FastClonerCache.cs +++ b/src/FastCloner/Code/FastClonerCache.cs @@ -52,7 +52,11 @@ internal readonly struct CacheEntry( public long Version { get; } = version; } + #if MODERN_10 + private static readonly Lock sync = new Lock(); + #else private static readonly object sync = new object(); + #endif private static Func? cloner; private static bool isSafe; private static bool canUseNoTrackingState; @@ -83,8 +87,7 @@ private static void Refresh(long currentVersion) bool computedIsSafe = FastClonerSafeTypes.CanReturnSameObject(type); bool computedCanUseNoTrackingState = !type.IsValueType && - typeMetadata.CyclePolicy == FastClonerCache.CyclePolicy.None && - !typeMetadata.HasBehaviorSensitiveMembers && + typeMetadata is { CyclePolicy: FastClonerCache.CyclePolicy.None, HasBehaviorSensitiveMembers: false } && !FastClonerCache.HasActiveTypeBehaviorOverrides; Func? computedCloner = null; @@ -97,8 +100,7 @@ private static void Refresh(long currentVersion) } else { - Func? objectCloner = clonerObj as Func; - if (objectCloner is not null) + if (clonerObj is Func objectCloner) { computedCloner = (obj, state) => (T)objectCloner(obj!, state); } @@ -158,7 +160,16 @@ internal sealed class TypeCloneMetadata public CloneExecutionMode ExecutionMode { get; set; } public CyclePolicy CyclePolicy { get; set; } public Func? RecursiveCloner { get; set; } - public Func? WorklistCloner { get; set; } + } + + internal sealed class TypeShape + { + public MemberInfo[] Members { get; init; } = []; + public Dictionary? IgnoredEventDetails { get; init; } + public Type[] CycleFieldTypes { get; init; } = []; + public bool HasReadonlyFields { get; init; } + public bool ContainsIgnoredMembers { get; init; } + public bool HasDirectSelfReference { get; init; } } internal static readonly ConcurrentDictionary TypeBehaviors = []; @@ -189,7 +200,7 @@ internal static bool IsTypeReference(Type type) internal static void RecalculateTypeBehaviorState() { HasTypeBehaviorOverrides = !TypeBehaviors.IsEmpty; - HasActiveTypeBehaviorOverrides = HasTypeBehaviorOverrides && !global::FastCloner.FastCloner.DisableOptionalFeatures; + HasActiveTypeBehaviorOverrides = HasTypeBehaviorOverrides && !FastCloner.DisableOptionalFeatures; HasSafeTypeOverrides = CalculateHasSafeTypeOverrides(); } @@ -209,15 +220,13 @@ private static bool CalculateHasSafeTypeOverrides() private static readonly ClrCache classCache = new ClrCache(); private static readonly ClrCache typeMetadataCache = new ClrCache(); + private static readonly ClrCache typeShapeCache = new ClrCache(); private static readonly ClrCache structCache = new ClrCache(); private static readonly ClrCache deepClassToCache = new ClrCache(); private static readonly ClrCache shallowClassToCache = new ClrCache(); private static readonly ConcurrentLazyCache typeConvertCache = new ConcurrentLazyCache(); private static readonly GenericClrCache fieldCache = new GenericClrCache(); - private static readonly ClrCache> ignoredEventInfoCache = new ClrCache>(); - private static readonly ClrCache> allMembersCache = new ClrCache>(); private static readonly GenericClrCache memberBehaviorCache = new GenericClrCache(); - private static readonly ClrCache typeContainsIgnoredMembersCache = new ClrCache(); private static readonly ClrCache attributedTypeBehaviorCache = new ClrCache(); private static readonly ClrCache immutableCollectionStatusCache = new ClrCache(); private static readonly ClrCache specialTypesCache = new ClrCache(); @@ -226,7 +235,6 @@ private static bool CalculateHasSafeTypeOverrides() private static readonly ClrCache stableHashSemanticsCache = new ClrCache(); private static readonly ClrCache canHaveCyclesCache = new ClrCache(); private static readonly ClrCache valueTypeContainsReferencesCache = new ClrCache(); - private static readonly ClrCache cycleFieldTypesCache = new ClrCache(); private static readonly ClrCache collectionPayloadTypeCache = new ClrCache(); private static readonly ClrCache compilerGeneratedTypeCache = new ClrCache(); @@ -234,28 +242,22 @@ private static bool CalculateHasSafeTypeOverrides() => fieldCache.GetOrAdd(new TypeNameKey(type, name), k => valueFactory(k.Type)); public static object? GetOrAddClass(Type type, Func valueFactory) => classCache.GetOrAdd(type, valueFactory); public static TypeCloneMetadata GetOrAddTypeMetadata(Type type, Func valueFactory) => typeMetadataCache.GetOrAdd(type, valueFactory); + public static TypeShape GetOrAddTypeShape(Type type, Func valueFactory) => typeShapeCache.GetOrAdd(type, valueFactory); public static object? GetOrAddStructAsObject(Type type, Func valueFactory) => structCache.GetOrAdd(type, valueFactory); public static object GetOrAddDeepClassTo(Type type, Func valueFactory) => deepClassToCache.GetOrAdd(type, valueFactory); public static object GetOrAddShallowClassTo(Type type, Func valueFactory) => shallowClassToCache.GetOrAdd(type, valueFactory); public static T GetOrAddConvertor(Type from, Type to, Func valueFactory) => (T)typeConvertCache.GetOrAdd(from, to, (f, t) => valueFactory(f, t)); - public static Dictionary GetOrAddIgnoredEventInfo(Type type, Func> valueFactory) => ignoredEventInfoCache.GetOrAdd(type, valueFactory); - public static List GetOrAddAllMembers(Type type, Func> valueFactory) => allMembersCache.GetOrAdd(type, valueFactory); public static CloneBehavior? GetOrAddMemberBehavior(MemberInfo memberInfo, Func valueFactory) => memberBehaviorCache.GetOrAdd(memberInfo, valueFactory); public static CloneBehavior? GetOrAddAttributedTypeBehavior(Type type, Func valueFactory) => attributedTypeBehaviorCache.GetOrAdd(type, valueFactory); public static bool GetOrAddImmutableCollectionStatus(Type type, Func valueFactory) => immutableCollectionStatusCache.GetOrAdd(type, valueFactory); - public static bool GetOrAddTypeContainsIgnoredMembers(Type type, Func valueFactory) - { - return typeContainsIgnoredMembersCache.GetOrAdd(type, valueFactory); - } public static object GetOrAddSpecialType(Type type, Func valueFactory) => specialTypesCache.GetOrAdd(type, valueFactory); public static bool GetOrAddIsTypeSafeHandle(Type type, Func valueFactory) => isTypeSafeHandleCache.GetOrAdd(type, valueFactory); public static bool GetOrAddAnonymousTypeStatus(Type type, Func valueFactory) => anonymousTypeStatusCache.GetOrAdd(type, valueFactory); public static bool GetOrAddStableHashSemantics(Type type, Func valueFactory) => stableHashSemanticsCache.GetOrAdd(type, valueFactory); public static bool GetOrAddCanHaveCycles(Type type, Func valueFactory) => canHaveCyclesCache.GetOrAdd(type, valueFactory); public static bool GetOrAddValueTypeContainsReferences(Type type, Func valueFactory) => valueTypeContainsReferencesCache.GetOrAdd(type, valueFactory); - public static Type[] GetOrAddCycleFieldTypes(Type type, Func valueFactory) => cycleFieldTypesCache.GetOrAdd(type, valueFactory); public static Type? GetOrAddCollectionPayloadType(Type type, Func valueFactory) => collectionPayloadTypeCache.GetOrAdd(type, valueFactory); public static bool GetOrAddCompilerGeneratedType(Type type, Func valueFactory) => compilerGeneratedTypeCache.GetOrAdd(type, valueFactory); @@ -266,15 +268,13 @@ public static void ClearCache() { classCache.Clear(); typeMetadataCache.Clear(); + typeShapeCache.Clear(); structCache.Clear(); deepClassToCache.Clear(); shallowClassToCache.Clear(); typeConvertCache.Clear(); fieldCache.Clear(); - ignoredEventInfoCache.Clear(); - allMembersCache.Clear(); memberBehaviorCache.Clear(); - typeContainsIgnoredMembersCache.Clear(); attributedTypeBehaviorCache.Clear(); immutableCollectionStatusCache.Clear(); specialTypesCache.Clear(); @@ -283,23 +283,19 @@ public static void ClearCache() stableHashSemanticsCache.Clear(); canHaveCyclesCache.Clear(); valueTypeContainsReferencesCache.Clear(); - cycleFieldTypesCache.Clear(); collectionPayloadTypeCache.Clear(); compilerGeneratedTypeCache.Clear(); BumpCacheVersion(); } - - internal sealed class ClrCache + + private sealed class ClrCache { private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); public TValue GetOrAdd(Type type, Func valueFactory) { IntPtr handle = type.TypeHandle.Value; - if (cache.TryGetValue(handle, out TValue? existing)) - return existing; - - return cache.GetOrAdd(handle, _ => valueFactory(type)); + return cache.TryGetValue(handle, out TValue? existing) ? existing : cache.GetOrAdd(handle, _ => valueFactory(type)); } public void Clear() => cache.Clear(); @@ -317,16 +313,10 @@ public TValue GetOrAdd(TKey key, Func valueFactory) public void Clear() => cache.Clear(); } - private readonly struct TypeNameKey : IEquatable + private readonly struct TypeNameKey(Type type, string name) : IEquatable { - public TypeNameKey(Type type, string name) - { - Type = type; - Name = name; - } - - public Type Type { get; } - public string Name { get; } + public Type Type { get; } = type; + public string Name { get; } = name; public bool Equals(TypeNameKey other) { diff --git a/src/FastCloner/Code/FastClonerExprGenerator.cs b/src/FastCloner/Code/FastClonerExprGenerator.cs index 146b794..dcd39e6 100644 --- a/src/FastCloner/Code/FastClonerExprGenerator.cs +++ b/src/FastCloner/Code/FastClonerExprGenerator.cs @@ -26,7 +26,7 @@ static FastClonerExprGenerator() fieldSetMethod = typeof(FieldInfo).GetMethod(nameof(FieldInfo.SetValue), [typeof(object), typeof(object)])!; } - private static MethodInfo GetClassCloneMethod(bool useShallowClassClone, bool skipCycleTracking) + private static MethodInfo GetClassCloneMethod(Type memberType, bool useShallowClassClone, bool skipCycleTracking) { if (useShallowClassClone) return StaticMethodInfos.DeepClonerGeneratorMethods.CloneClassShallowAndTrack; @@ -141,18 +141,19 @@ internal static bool MemberShouldCopyReference(MemberInfo memberInfo) } - internal static bool CalculateTypeContainsIgnoredMembers(Type type) + internal static FastClonerCache.TypeShape GetTypeShape(Type type) { - IEnumerable members = FastClonerCache.GetOrAddAllMembers(type, GetAllMembers); + return FastClonerCache.GetOrAddTypeShape(type, BuildTypeShape); + } - return members.Any(MemberIsIgnored); + internal static bool CalculateTypeContainsIgnoredMembers(Type type) + { + return GetTypeShape(type).ContainsIgnoredMembers; } private static bool TypeHasReadonlyFields(Type type) { - return FastClonerCache.GetOrAddAllMembers(type, GetAllMembers) - .OfType() - .Any(f => f.IsInitOnly); + return GetTypeShape(type).HasReadonlyFields; } private static void AddStructReadonlyFieldsCloneExpressions( @@ -164,22 +165,9 @@ private static void AddStructReadonlyFieldsCloneExpressions( bool useShallowClassClone = false, bool skipCycleTracking = false) { - IEnumerable members = FastClonerCache.GetOrAddAllMembers(type, GetAllMembers); - Dictionary ignoredEventDetails = FastClonerCache.GetOrAddIgnoredEventInfo(type, t => - { - Dictionary details = new Dictionary(); - EventInfo[] events = t.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - foreach (EventInfo evtInfo in events) - { - if (MemberIsIgnored(evtInfo)) - { - details[evtInfo.Name] = evtInfo.EventHandlerType; - } - } - - return details; - }); + FastClonerCache.TypeShape typeShape = GetTypeShape(type); + MemberInfo[] members = typeShape.Members; + Dictionary? ignoredEventDetails = typeShape.IgnoredEventDetails; foreach (MemberInfo member in members) { @@ -195,7 +183,7 @@ private static void AddStructReadonlyFieldsCloneExpressions( { shouldBeIgnored = true; } - else if (ignoredEventDetails.TryGetValue(fieldInfo.Name, out Type? evtType)) + else if (ignoredEventDetails is not null && ignoredEventDetails.TryGetValue(fieldInfo.Name, out Type? evtType)) { if (evtType == memberType) { @@ -247,7 +235,7 @@ private static void AddStructReadonlyFieldsCloneExpressions( } else { - MethodInfo classCloneMethod = GetClassCloneMethod(useShallowClassClone, skipCycleTracking); + MethodInfo classCloneMethod = GetClassCloneMethod(memberType, useShallowClassClone, skipCycleTracking); if (!useShallowClassClone && !skipCycleTracking) { Expression deepCall = Expression.Call(classCloneMethod, originalMemberValue, state); @@ -283,7 +271,6 @@ internal static void ForceSetField(FieldInfo field, object obj, object value) internal readonly record struct ExpressionPosition(int Depth, int Index) { public ExpressionPosition Next() => this with { Index = Index + 1 }; - public ExpressionPosition Nested() => new ExpressionPosition(Depth + 1, 0); } #else internal readonly struct ExpressionPosition : IEquatable @@ -298,7 +285,6 @@ public ExpressionPosition(int depth, int index) } public ExpressionPosition Next() => new ExpressionPosition(Depth, Index + 1); - public ExpressionPosition Nested() => new ExpressionPosition(Depth + 1, 0); public bool Equals(ExpressionPosition other) { @@ -347,25 +333,13 @@ private static bool ShouldDeepCloneStructReadonlyFields(Type type) { return true; } - - // Some namespaces use structs as opaque handles to internal static state or singletons. - // Clone cloning these handles breaks their identity (e.g. they no longer match the static singletons), - // causing equality checks and lookups to fail. - // For these specific namespaces, we assume structs with readonly fields are intended to be "Safe Handles" - // and should have their readonly references preserved (shallow copied), not deep cloned. - // We append "." to ensuring we match full namespace segments (e.g. "System.Net" matches "System.Net." but not "System.Network.") + if (readonlyStructSafeHandleNamespaces.ContainsAnyPattern(type.Namespace + ".")) { return false; } - - // Check for the user-defined safe handle attribute - if (FastClonerCache.GetOrAddIsTypeSafeHandle(type, t => t.GetCustomAttribute() != null)) - { - return false; - } - - return true; + + return !FastClonerCache.GetOrAddIsTypeSafeHandle(type, t => t.GetCustomAttribute() != null); } internal static object? GenerateProcessMethod(Type realType, bool asObject) => GenerateProcessMethod(realType, asObject && realType.IsValueType(), new ExpressionPosition(0, 0)); @@ -454,22 +428,9 @@ private static void AddMemberCloneExpressions( bool skipCycleTracking = false) { ExpressionPosition currentPosition = new ExpressionPosition(0, 0); - IEnumerable members = FastClonerCache.GetOrAddAllMembers(type, GetAllMembers); - Dictionary ignoredEventDetails = FastClonerCache.GetOrAddIgnoredEventInfo(type, t => - { - Dictionary details = new Dictionary(); - EventInfo[] events = t.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - foreach (EventInfo evtInfo in events) - { - if (MemberIsIgnored(evtInfo)) - { - details[evtInfo.Name] = evtInfo.EventHandlerType; - } - } - - return details; - }); + FastClonerCache.TypeShape typeShape = GetTypeShape(type); + MemberInfo[] members = typeShape.Members; + Dictionary? ignoredEventDetails = typeShape.IgnoredEventDetails; foreach (MemberInfo member in members) { @@ -500,7 +461,9 @@ private static void AddMemberCloneExpressions( continue; } - if (member is FieldInfo fieldInfoForEventCheck && ignoredEventDetails.TryGetValue(fieldInfoForEventCheck.Name, out Type? evtType)) + if (member is FieldInfo fieldInfoForEventCheck && + ignoredEventDetails is not null && + ignoredEventDetails.TryGetValue(fieldInfoForEventCheck.Name, out Type? evtType)) { if (evtType == memberType) { @@ -552,7 +515,7 @@ private static void AddMemberCloneExpressions( } else if (member is FieldInfo fi) { - if (ignoredEventDetails.TryGetValue(fi.Name, out Type? eventHandlerTypeFromCache)) + if (ignoredEventDetails is not null && ignoredEventDetails.TryGetValue(fi.Name, out Type? eventHandlerTypeFromCache)) { if (eventHandlerTypeFromCache == fi.FieldType) { @@ -620,7 +583,7 @@ private static void AddMemberCloneExpressions( } else { - MethodInfo classCloneMethod = GetClassCloneMethod(useShallowClassClone, skipCycleTracking); + MethodInfo classCloneMethod = GetClassCloneMethod(memberType, useShallowClassClone, skipCycleTracking); if (!useShallowClassClone && !skipCycleTracking) { Expression deepCall = Expression.Call(classCloneMethod, originalMemberValue, state); @@ -681,8 +644,8 @@ private static object GenerateMemberwiseCloner(Type type, ExpressionPosition pos if (!type.IsValueType()) { - MethodInfo methodInfo = StaticMethodInfos.CommonMethods.MemberwiseClone; - expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(from, methodInfo), type))); + MethodInfo methodInfo = StaticMethodInfos.CommonMethods.DirectCloneObject; + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(methodInfo, from), type))); expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); expressionList.Add(Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, toLocal)); @@ -785,35 +748,96 @@ private static bool IsCloneable(Type type) return !badTypes.ContainsAnyPattern(type.FullName); } - private static List GetAllMembers(Type type) + private static FastClonerCache.TypeShape BuildTypeShape(Type type) { List members = []; + Dictionary? ignoredEventDetails = null; + List cycleFieldTypes = new List(); + bool hasReadonlyFields = false; + bool containsIgnoredMembers = false; + bool hasDirectSelfReference = false; + bool includeMemberMetadata = true; Type? currentType = type; - while (currentType != null && currentType != typeof(ContextBoundObject)) + while (currentType != null && currentType != typeof(object)) { + if (currentType == typeof(ContextBoundObject)) + { + includeMemberMetadata = false; + } + FieldInfo[] fields = currentType.GetDeclaredFields(); for (int i = 0; i < fields.Length; i++) { - members.Add(fields[i]); + FieldInfo field = fields[i]; + cycleFieldTypes.Add(field.FieldType); + + if (!includeMemberMetadata) + { + continue; + } + + members.Add(field); + if (field.IsInitOnly) + { + hasReadonlyFields = true; + } + + if (MemberIsIgnored(field)) + { + containsIgnoredMembers = true; + } + + Type fieldType = field.FieldType; + if (fieldType == type || fieldType.IsArray && fieldType.GetElementType() == type) + { + hasDirectSelfReference = true; + } } - PropertyInfo[] properties = currentType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - for (int i = 0; i < properties.Length; i++) + if (includeMemberMetadata) { - PropertyInfo property = properties[i]; - if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0) + PropertyInfo[] properties = currentType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + for (int i = 0; i < properties.Length; i++) { - continue; + PropertyInfo property = properties[i]; + if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0) + { + continue; + } + + members.Add(property); + if (MemberIsIgnored(property)) + { + containsIgnoredMembers = true; + } } - members.Add(property); + EventInfo[] events = currentType.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + for (int i = 0; i < events.Length; i++) + { + EventInfo evtInfo = events[i]; + if (MemberIsIgnored(evtInfo) && evtInfo.EventHandlerType is not null) + { + containsIgnoredMembers = true; + ignoredEventDetails ??= new Dictionary(); + ignoredEventDetails[evtInfo.Name] = evtInfo.EventHandlerType; + } + } } currentType = currentType.BaseType; } - return members; + return new FastClonerCache.TypeShape + { + Members = members.ToArray(), + IgnoredEventDetails = ignoredEventDetails, + CycleFieldTypes = cycleFieldTypes.ToArray(), + HasReadonlyFields = hasReadonlyFields, + ContainsIgnoredMembers = containsIgnoredMembers, + HasDirectSelfReference = hasDirectSelfReference + }; } private static object? CloneIClonable(Type type) @@ -929,8 +953,8 @@ private static List GetAllMembers(Type type) if (!type.IsValueType()) { - MethodInfo methodInfo = StaticMethodInfos.CommonMethods.MemberwiseClone; - expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(from, methodInfo), type))); + MethodInfo methodInfo = StaticMethodInfos.CommonMethods.DirectCloneObject; + expressionList.Add(Expression.Assign(toLocal, Expression.Convert(Expression.Call(methodInfo, from), type))); fromLocal = Expression.Variable(type); expressionList.Add(Expression.Assign(fromLocal, Expression.Convert(from, type))); if (!skipCycleTracking) @@ -1018,7 +1042,7 @@ private static object GenerateHttpRequestOptionsProcessor(ExpressionPosition pos Expression.Assign(result, Expression.Property(tempMessage, "Options")), Expression.Call(state, StaticMethodInfos.DeepCloneStateMethods.AddKnownRef, from, result), Expression.Assign(result, Expression.Convert( - Expression.Call(fromOptions, StaticMethodInfos.CommonMethods.MemberwiseClone), + Expression.Call(StaticMethodInfos.CommonMethods.DirectCloneObject, fromOptions), typeof(HttpRequestOptions) )), Expression.Call(tempMessage, StaticMethodInfos.CommonMethods.Dispose), @@ -1113,20 +1137,18 @@ private static object GenerateDictionaryProcessor(Type dictType, Type keyType, T { bool isImmutable = IsImmutableCollection(dictType); - if (!isImmutable && - dictType.IsGenericType && - dictType.GetGenericTypeDefinition() == typeof(Dictionary<,>) && - FastClonerSafeTypes.HasStableHashSemantics(keyType) && - (keyType.IsValueType || FastClonerSafeTypes.CanReturnSameObject(keyType))) + switch (isImmutable) { - return GenerateAdaptiveDictionaryProcessor(dictType, keyType, valueType, position); + case false when + dictType.IsGenericType && + dictType.GetGenericTypeDefinition() == typeof(Dictionary<,>) && + FastClonerSafeTypes.HasStableHashSemantics(keyType) && + (keyType.IsValueType || FastClonerSafeTypes.CanReturnSameObject(keyType)): + return GenerateAdaptiveDictionaryProcessor(dictType, keyType, valueType, position); + case false when FastClonerSafeTypes.HasStableHashSemantics(keyType) && !FastClonerCache.IsTypeIgnored(keyType) && !FastClonerCache.IsTypeIgnored(valueType): + return GenerateMemberwiseCloner(dictType, position); } - if (!isImmutable && FastClonerSafeTypes.HasStableHashSemantics(keyType) && !FastClonerCache.IsTypeIgnored(keyType) && !FastClonerCache.IsTypeIgnored(valueType)) - { - return GenerateMemberwiseCloner(dictType, position); - } - ParameterExpression from = Expression.Parameter(typeof(object)); ParameterExpression state = Expression.Parameter(typeof(FastCloneState)); LabelTarget returnNullLabel = Expression.Label(typeof(object)); @@ -1743,7 +1765,7 @@ private static object CreateAdaptiveDictionaryProcessorSafe(Expres if (typed is null) return null!; return typed.Count <= AdaptiveDictionaryRebuildThreshold - ? FastClonerGenerator.CloneDictionarySafeInternal(typed, state)! + ? FastClonerGenerator.CloneDictionarySafeInternal(typed, state)! : memberwise(obj, state); }); } @@ -1758,7 +1780,7 @@ private static object CreateAdaptiveDictionaryProcessorStruct(Expr if (typed is null) return null!; return typed.Count <= AdaptiveDictionaryRebuildThreshold - ? FastClonerGenerator.CloneDictionaryStructValueInternal(typed, state)! + ? FastClonerGenerator.CloneDictionaryStructValueInternal(typed, state)! : memberwise(obj, state); }); } @@ -1773,7 +1795,7 @@ private static object CreateAdaptiveDictionaryProcessorClass(Expre if (typed is null) return null!; return typed.Count <= AdaptiveDictionaryRebuildThreshold - ? FastClonerGenerator.CloneDictionaryClassValueInternal(typed, state)! + ? FastClonerGenerator.CloneDictionaryClassValueInternal(typed, state)! : memberwise(obj, state); }); } @@ -2130,7 +2152,6 @@ private static object GenerateProcessArrayMethod(Type type) { if (rank == 2 && type == elementType?.MakeArrayType(2)) { - // small optimization for 2 dim arrays methodInfo = typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.Clone2DimArrayInternal))!.MakeGenericMethod(elementType); } else diff --git a/src/FastCloner/Code/FastClonerGenerator.cs b/src/FastCloner/Code/FastClonerGenerator.cs index 76ce7b3..7b80da4 100644 --- a/src/FastCloner/Code/FastClonerGenerator.cs +++ b/src/FastCloner/Code/FastClonerGenerator.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -58,37 +57,31 @@ internal static class FastClonerGenerator private static readonly Type DictionaryInterfaceDefinition = typeof(IDictionary<,>); private static readonly Type EnumerableInterfaceDefinition = typeof(IEnumerable<>); - private struct TypeCloneDispatchCache2 + private struct TypeCloneDispatchCache { private Type? typeA; private FastClonerCache.TypeCloneMetadata? metadataA; private Func? recursiveA; - private Func? worklistA; private Type? typeB; private FastClonerCache.TypeCloneMetadata? metadataB; private Func? recursiveB; - private Func? worklistB; private Type? typeC; private FastClonerCache.TypeCloneMetadata? metadataC; private Func? recursiveC; - private Func? worklistC; private Type? typeD; private FastClonerCache.TypeCloneMetadata? metadataD; private Func? recursiveD; - private Func? worklistD; public void Resolve( Type runtimeType, FastCloneState state, out FastClonerCache.TypeCloneMetadata metadata, - out Func? recursiveCloner, - out Func? worklistCloner) + out Func? recursiveCloner) { if (runtimeType == typeA && metadataA is not null) { metadata = metadataA; recursiveCloner = recursiveA; - worklistCloner = worklistA; return; } @@ -96,7 +89,6 @@ public void Resolve( { metadata = metadataB; recursiveCloner = recursiveB; - worklistCloner = worklistB; PromoteSecondToFirst(); return; } @@ -105,7 +97,6 @@ public void Resolve( { metadata = metadataC; recursiveCloner = recursiveC; - worklistCloner = worklistC; PromoteThirdToFirst(); return; } @@ -114,34 +105,28 @@ public void Resolve( { metadata = metadataD; recursiveCloner = recursiveD; - worklistCloner = worklistD; PromoteFourthToFirst(); return; } metadata = GetTypeMetadata(runtimeType, state); recursiveCloner = metadata.RecursiveCloner; - worklistCloner = metadata.WorklistCloner ?? recursiveCloner; typeD = typeC; metadataD = metadataC; recursiveD = recursiveC; - worklistD = worklistC; typeC = typeB; metadataC = metadataB; recursiveC = recursiveB; - worklistC = worklistB; typeB = typeA; metadataB = metadataA; recursiveB = recursiveA; - worklistB = worklistA; typeA = runtimeType; metadataA = metadata; recursiveA = recursiveCloner; - worklistA = worklistCloner; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -159,10 +144,6 @@ private void PromoteSecondToFirst() Func? previousRecursiveA = recursiveA; recursiveA = recursiveB; recursiveB = previousRecursiveA; - - Func? previousWorklistA = worklistA; - worklistA = worklistB; - worklistB = previousWorklistA; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -186,12 +167,6 @@ private void PromoteThirdToFirst() recursiveA = recursiveC; recursiveB = previousRecursiveA; recursiveC = previousRecursiveB; - - Func? previousWorklistA = worklistA; - Func? previousWorklistB = worklistB; - worklistA = worklistC; - worklistB = previousWorklistA; - worklistC = previousWorklistB; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -221,14 +196,6 @@ private void PromoteFourthToFirst() recursiveB = previousRecursiveA; recursiveC = previousRecursiveB; recursiveD = previousRecursiveC; - - Func? previousWorklistA = worklistA; - Func? previousWorklistB = worklistB; - Func? previousWorklistC = worklistC; - worklistA = worklistD; - worklistB = previousWorklistA; - worklistC = previousWorklistB; - worklistD = previousWorklistC; } } @@ -247,12 +214,12 @@ private static FastClonerCache.TypeCloneMetadata GetTypeMetadata(Type type, Fast private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type) { + FastClonerCache.TypeShape typeShape = FastClonerExprGenerator.GetTypeShape(type); bool isSafe = FastClonerSafeTypes.CanReturnSameObject(type) && (!type.IsValueType || type.IsPrimitive || type.IsEnum); bool canHaveCycles = !type.IsValueType && FastClonerCache.GetOrAddCanHaveCycles(type, CalculateCanHaveCycles); bool canSkipReferenceTracking = type.IsValueType && !ValueTypeContainsReferenceFieldsCached(type); - bool hasDirectSelfReference = canHaveCycles && TypeHasDirectSelfReference(type); - bool hasBehaviorSensitiveMembers = !FastCloner.DisableOptionalFeatures && - FastClonerCache.GetOrAddTypeContainsIgnoredMembers(type, FastClonerExprGenerator.CalculateTypeContainsIgnoredMembers); + bool hasDirectSelfReference = canHaveCycles && typeShape.HasDirectSelfReference; + bool hasBehaviorSensitiveMembers = !FastCloner.DisableOptionalFeatures && typeShape.ContainsIgnoredMembers; bool requiresSpecializedCloner = type.IsArray || FastClonerExprGenerator.IsDictionaryType(type) || FastClonerExprGenerator.IsSetType(type) || @@ -261,9 +228,6 @@ private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type) Func? recursive = isSafe ? null : (Func?)FastClonerCache.GetOrAddClass(type, t => GenerateClonerRecursive(t, canSkipReferenceTracking)); - Func? worklist = isSafe - ? null - : GenerateClonerWorklist(type); FastClonerCache.CyclePolicy cyclePolicy = !canHaveCycles ? FastClonerCache.CyclePolicy.None @@ -287,8 +251,7 @@ private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type) ? FastClonerCache.CloneExecutionMode.SafeReturn : FastClonerCache.CloneExecutionMode.MemberwiseThenPatch, CyclePolicy = cyclePolicy, - RecursiveCloner = recursive, - WorklistCloner = worklist ?? recursive + RecursiveCloner = recursive }; } @@ -316,10 +279,9 @@ private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type) if (cacheEntry.Cloner is not null) { - if (cacheEntry.CanUseNoTrackingState) - return cacheEntry.Cloner(obj, FastCloneState.GetSimpleState()); - - return CloneRootWithTrackedState(obj, cacheEntry.Cloner, concreteTypeOfObj, cacheEntry.Metadata); + return cacheEntry.CanUseNoTrackingState ? + cacheEntry.Cloner(obj, FastCloneState.GetSimpleState()) : + CloneRootWithTrackedState(obj, cacheEntry.Cloner, cacheEntry.Metadata!); } } @@ -348,7 +310,7 @@ private static FastClonerCache.TypeCloneMetadata BuildTypeMetadata(Type type) { if (typeOfT == concreteTypeOfObj) { - bool hasIgnoredMembers = FastClonerCache.GetOrAddTypeContainsIgnoredMembers(concreteTypeOfObj, FastClonerExprGenerator.CalculateTypeContainsIgnoredMembers); + bool hasIgnoredMembers = FastClonerExprGenerator.GetTypeShape(concreteTypeOfObj).ContainsIgnoredMembers; if (hasIgnoredMembers || !FastClonerSafeTypes.CanReturnSameObject(concreteTypeOfObj)) { @@ -421,23 +383,20 @@ private static bool TryCloneSafeArrayRoot(T obj, Type runtimeType, out T? clo } if (metadata.CanSkipReferenceTracking || - (metadata.CyclePolicy == FastClonerCache.CyclePolicy.None && !metadata.HasBehaviorSensitiveMembers && !rootType.IsValueType)) + (metadata is { CyclePolicy: FastClonerCache.CyclePolicy.None, HasBehaviorSensitiveMembers: false } && !rootType.IsValueType)) return cloner(obj, FastCloneState.GetSimpleState()); - return CloneRootWithTrackedState(obj, cloner, rootType, metadata); + return CloneRootWithTrackedState(obj, cloner, metadata); } - private static T CloneRootWithTrackedState(T obj, Func cloner, Type rootType, FastClonerCache.TypeCloneMetadata? metadata = null) + private static T CloneRootWithTrackedState(T obj, Func cloner, FastClonerCache.TypeCloneMetadata metadata) { FastCloneState state = FastCloneState.Rent(); - state.UseWorkList = (metadata?.CyclePolicy ?? FastClonerCache.CyclePolicy.TrackReferences) == FastClonerCache.CyclePolicy.Worklist; - if (!state.UseWorkList && metadata is null) - state.UseWorkList = TypeHasDirectSelfReference(rootType); - - T result; + state.UseWorkList = metadata.CyclePolicy == FastClonerCache.CyclePolicy.Worklist; try { + T result; if (!state.UseWorkList) { int current = state.IncrementDepth(); @@ -497,28 +456,6 @@ private static T CloneRootWithTrackedState(T obj, Func } } - private static bool TypeHasDirectSelfReference(Type type) - { - Type? tp = type; - while (tp != null && tp != typeof(ContextBoundObject)) - { - 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; - } - } - tp = tp.BaseType; - } - return false; - } - private static bool CalculateCanHaveCycles(Type type) { if (type.IsValueType || FastClonerSafeTypes.CanReturnSameObject(type)) @@ -608,23 +545,7 @@ private static bool ValueTypeContainsReferenceFieldsCached(Type valueType) private static Type[] GetCycleFieldTypes(Type type) { - return FastClonerCache.GetOrAddCycleFieldTypes(type, static t => - { - List fieldTypes = new List(); - Type? currentType = t; - while (currentType != null && currentType != typeof(object)) - { - FieldInfo[] fields = currentType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); - for (int i = 0; i < fields.Length; i++) - { - fieldTypes.Add(fields[i].FieldType); - } - - currentType = currentType.BaseType; - } - - return fieldTypes.ToArray(); - }); + return FastClonerExprGenerator.GetTypeShape(type).CycleFieldTypes; } private static Type? GetCollectionPayloadType(Type type) @@ -714,8 +635,7 @@ private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterf state, objType, metadata, - metadata.RecursiveCloner, - metadata.WorklistCloner ?? metadata.RecursiveCloner + metadata.RecursiveCloner ); } @@ -724,49 +644,19 @@ private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterf FastCloneState state, Type objType, FastClonerCache.TypeCloneMetadata metadata, - Func? recursiveCloner, - Func? worklistCloner) + Func? recursiveCloner) { - bool hasOptionalTypeOverrides = FastClonerCache.HasActiveTypeBehaviorOverrides; - if (!hasOptionalTypeOverrides) + if (TryGetNonCloningResult(obj, objType, metadata, recursiveCloner, out object? nonCloningResult)) { - if (metadata.IsSafe || recursiveCloner is null) - return obj; - } - else - { - if (!FastClonerCache.HasSafeTypeOverrides) - { - if (FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(objType)) - return obj; - - if (FastClonerCache.IsTypeIgnored(objType)) - return null; - } - else - { - if (FastClonerCache.IsTypeIgnored(objType)) - return null; - - if (FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(objType)) - return obj; - } + return nonCloningResult; } - // safe object - if (recursiveCloner is null) - { - return obj; - } - + object? knownRef = state.GetKnownRef(obj); + if (knownRef is not null) + return knownRef; + if (state.UseWorkList) { - object? knownA = state.GetKnownRef(obj); - if (knownA is not null) - { - return knownA; - } - // value types: avoid the worklist because ClonerToExprGenerator.GenerateClonerInternal doesn't support value types if (objType.IsValueType()) { @@ -778,20 +668,18 @@ private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterf return CloneClassShallowAndTrack(obj, state); } + bool depthIncremented = false; try { + depthIncremented = true; int current = state.IncrementDepth(); if (current >= FastCloner.MaxRecursionDepth) { state.DecrementDepth(); + depthIncremented = false; state.UseWorkList = true; - object? knownB = state.GetKnownRef(obj); - if (knownB is not null) - { - return knownB; - } - + // value types: avoid the worklist because ClonerToExprGenerator.GenerateClonerInternal doesn't support value types if (objType.IsValueType()) { @@ -803,17 +691,72 @@ private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterf return CloneClassShallowAndTrack(obj, state); } - object? knownRef = state.GetKnownRef(obj); - if (knownRef is not null) - return knownRef; - return recursiveCloner(obj, state); } finally { - state.DecrementDepth(); + if (depthIncremented) + state.DecrementDepth(); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetNonCloningResult( + object obj, + Type objType, + FastClonerCache.TypeCloneMetadata metadata, + Func? recursiveCloner, + out object? result) + { + if (!FastClonerCache.HasActiveTypeBehaviorOverrides) + { + if (metadata.IsSafe || recursiveCloner is null) + { + result = obj; + return true; + } + } + else + { + if (!FastClonerCache.HasSafeTypeOverrides) + { + if (FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(objType)) + { + result = obj; + return true; + } + + if (FastClonerCache.IsTypeIgnored(objType)) + { + result = null; + return true; + } + } + else + { + if (FastClonerCache.IsTypeIgnored(objType)) + { + result = null; + return true; + } + + if (FastClonerSafeTypes.DefaultKnownTypes.ContainsKey(objType)) + { + result = obj; + return true; + } + } + + if (recursiveCloner is null) + { + result = obj; + return true; + } + } + + result = null; + return false; + } internal static object? CloneClassShallowAndTrack(object? obj, FastCloneState state) { @@ -853,7 +796,7 @@ private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterf } } - object? shallow = ShallowObjectCloner.CloneObject(obj); + object shallow = ShallowObjectCloner.CloneObject(obj); state.AddKnownRef(obj, shallow); if (state.UseWorkList) @@ -863,13 +806,6 @@ private static bool TryGetGenericInterfaceArgument(Type type, Type genericInterf return shallow; } - - private static bool RequiresSpecializedCloner(Type type) - { - return type.IsArray || - FastClonerExprGenerator.IsDictionaryType(type) || - FastClonerExprGenerator.IsSetType(type); - } internal static T CloneStructInternal(T obj, FastCloneState state) { @@ -965,7 +901,10 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) } bool hasOptionalTypeOverrides = FastClonerCache.HasActiveTypeBehaviorOverrides; - TypeCloneDispatchCache2 dispatch = default; + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; for (int i = 0; i < l; i++) { T? item = obj[i]; @@ -985,9 +924,22 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) continue; } - dispatch.Resolve(runtimeType, state, out FastClonerCache.TypeCloneMetadata metadata, out Func? recursiveCloner, out Func? worklistCloner); + FastClonerCache.TypeCloneMetadata metadata; + Func? recursiveCloner; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } - outArray[i] = (T)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner, worklistCloner)!; + outArray[i] = (T)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner)!; } return outArray; @@ -997,7 +949,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) { if (obj is null) return null; int count = obj.Count; - List result = new(count); + List result = new List(count); state.AddKnownRef(obj, result); #if NET5_0_OR_GREATER @@ -1016,7 +968,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) { if (obj is null) return null; int count = obj.Count; - List result = new(count); + List result = new List(count); state.AddKnownRef(obj, result); Func? cloner = GetClonerForValueType(); @@ -1053,7 +1005,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) state.EnsureKnownRefCapacity(count + 1); if (state.UseWorkList) state.EnsureWorkQueueCapacity(count); - List result = new(count); + List result = new List(count); state.AddKnownRef(obj, result); Type declaredType = typeof(T); @@ -1065,19 +1017,21 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) { FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(declaredType, state); Func? recursiveCloner = metadata.RecursiveCloner; - Func? worklistCloner = metadata.WorklistCloner ?? recursiveCloner; for (int i = 0; i < count; i++) { T? item = srcSpan[i]; span[i] = item is null ? null - : (T?)CloneClassInternalResolved(item, state, declaredType, metadata, recursiveCloner, worklistCloner); + : (T?)CloneClassInternalResolved(item, state, declaredType, metadata, recursiveCloner); } return result; } - TypeCloneDispatchCache2 dispatch = default; + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; for (int i = 0; i < count; i++) { T? item = srcSpan[i]; @@ -1088,28 +1042,43 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) } Type runtimeType = item.GetType(); - dispatch.Resolve(runtimeType, state, out FastClonerCache.TypeCloneMetadata metadata, out Func? recursiveCloner, out Func? worklistCloner); + FastClonerCache.TypeCloneMetadata metadata; + Func? recursiveCloner; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } - span[i] = (T?)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner, worklistCloner); + span[i] = (T?)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner); } #else if (declaredType.IsSealed) { FastClonerCache.TypeCloneMetadata metadata = GetTypeMetadata(declaredType, state); Func? recursiveCloner = metadata.RecursiveCloner; - Func? worklistCloner = metadata.WorklistCloner ?? recursiveCloner; for (int i = 0; i < count; i++) { T? item = obj[i]; result.Add(item is null ? null - : (T?)CloneClassInternalResolved(item, state, declaredType, metadata, recursiveCloner, worklistCloner)); + : (T?)CloneClassInternalResolved(item, state, declaredType, metadata, recursiveCloner)); } return result; } - TypeCloneDispatchCache2 dispatch = default; + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; for (int i = 0; i < count; i++) { T? item = obj[i]; @@ -1120,9 +1089,22 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) } Type runtimeType = item.GetType(); - dispatch.Resolve(runtimeType, state, out FastClonerCache.TypeCloneMetadata metadata, out Func? recursiveCloner, out Func? worklistCloner); + FastClonerCache.TypeCloneMetadata metadata; + Func? recursiveCloner; + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } - result.Add((T?)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner, worklistCloner)); + result.Add((T?)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner)); } #endif @@ -1133,7 +1115,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) Dictionary? obj, FastCloneState state) where TKey : notnull { if (obj is null) return null; - Dictionary result = new(obj.Count, obj.Comparer); + Dictionary result = new Dictionary(obj.Count, obj.Comparer); state.AddKnownRef(obj, result); bool ignoreValues = FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(typeof(TValue)); @@ -1155,7 +1137,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) Dictionary? obj, FastCloneState state) where TKey : notnull where TValue : struct { if (obj is null) return null; - Dictionary result = new(obj.Count, obj.Comparer); + Dictionary result = new Dictionary(obj.Count, obj.Comparer); state.AddKnownRef(obj, result); bool ignoreValues = FastClonerCache.HasActiveTypeBehaviorOverrides && FastClonerCache.IsTypeIgnored(typeof(TValue)); @@ -1217,14 +1199,13 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) if (declaredType == typeof(object)) return (Dictionary?)(object?)CloneDictionaryObjectValueInternal((Dictionary)(object)obj, state); - Dictionary result = new(obj.Count, obj.Comparer); + Dictionary result = new Dictionary(obj.Count, obj.Comparer); state.AddKnownRef(obj, result); if (declaredType.IsSealed) { FastClonerCache.TypeCloneMetadata? metadata = ignoreDeclaredValues ? null : GetTypeMetadata(declaredType, state); Func? recursiveCloner = metadata?.RecursiveCloner; - Func? worklistCloner = metadata?.WorklistCloner ?? recursiveCloner; foreach (KeyValuePair kvp in obj) { TValue? cloned; @@ -1234,7 +1215,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) } else { - cloned = (TValue?)CloneClassInternalResolved(kvp.Value, state, declaredType, metadata!, recursiveCloner, worklistCloner); + cloned = (TValue?)CloneClassInternalResolved(kvp.Value, state, declaredType, metadata!, recursiveCloner); } #if NET6_0_OR_GREATER ref TValue? valueRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); @@ -1249,7 +1230,6 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) Type? lastType = null; FastClonerCache.TypeCloneMetadata? lastMetadata = null; Func? lastRecursive = null; - Func? lastWorklist = null; foreach (KeyValuePair kvp in obj) { TValue? value = kvp.Value; @@ -1297,11 +1277,10 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) { metadata = lastMetadata = GetTypeMetadata(runtimeType, state); lastRecursive = metadata.RecursiveCloner; - lastWorklist = metadata.WorklistCloner ?? lastRecursive; lastType = runtimeType; } - TValue? cloned = (TValue?)CloneClassInternalResolved(value, state, runtimeType, metadata, lastRecursive, lastWorklist); + TValue? cloned = (TValue?)CloneClassInternalResolved(value, state, runtimeType, metadata, lastRecursive); #if NET6_0_OR_GREATER ref TValue? clonedRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); clonedRef = cloned; @@ -1316,9 +1295,12 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) private static Dictionary CloneDictionaryObjectValueInternal( Dictionary obj, FastCloneState state) where TKey : notnull { - Dictionary result = new(obj.Count, obj.Comparer); + Dictionary result = new Dictionary(obj.Count, obj.Comparer); state.AddKnownRef(obj, result); - TypeCloneDispatchCache2 dispatch = default; + TypeCloneDispatchCache dispatch = default; + Type? lastType = null; + FastClonerCache.TypeCloneMetadata? lastMetadata = null; + Func? lastRecursiveCloner = null; foreach (KeyValuePair kvp in obj) { @@ -1348,10 +1330,20 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) FastClonerCache.TypeCloneMetadata metadata; Func? recursiveCloner; - Func? worklistCloner; - dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner, out worklistCloner); + if (runtimeType == lastType && lastMetadata is not null) + { + metadata = lastMetadata; + recursiveCloner = lastRecursiveCloner; + } + else + { + dispatch.Resolve(runtimeType, state, out metadata, out recursiveCloner); + lastType = runtimeType; + lastMetadata = metadata; + lastRecursiveCloner = recursiveCloner; + } - object cloned = CloneClassInternalResolved(value, state, runtimeType, metadata, recursiveCloner, worklistCloner)!; + object cloned = CloneClassInternalResolved(value, state, runtimeType, metadata, recursiveCloner)!; #if NET6_0_OR_GREATER ref object? clonedRef = ref CollectionsMarshal.GetValueRefOrAddDefault(result, kvp.Key, out _); clonedRef = cloned; @@ -1377,7 +1369,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) int lb1 = obj.GetLowerBound(0); int lb2 = obj.GetLowerBound(1); if (lb1 != 0 || lb2 != 0) - return (T[,]) CloneAbstractArrayInternal(obj, state); + return (T[,]) CloneAbstractArrayInternal(obj, state)!; int l1 = obj.GetLength(0); int l2 = obj.GetLength(1); @@ -1405,7 +1397,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) { for (int i = 0; i < l1; i++) for (int k = 0; k < l2; k++) - outArray[i, k] = (T)CloneClassInternal(obj[i, k], state); + outArray[i, k] = (T)CloneClassInternal(obj[i, k], state)!; } return outArray; @@ -1428,7 +1420,7 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) hasZeroLength |= length == 0; } - Type? elementType = obj.GetType().GetElementType(); + Type elementType = obj.GetType().GetElementType()!; Array outArray = Array.CreateInstance(elementType, lengths, lowerBounds); state.AddKnownRef(obj, outArray); @@ -1502,14 +1494,6 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) return FastClonerExprGenerator.GenerateClonerInternal(t, asObject: true, skipCycleTracking: skipCycleTracking, useShallowClassClone: false); } - private static Func? GenerateClonerWorklist(Type t) - { - if (FastClonerSafeTypes.CanReturnSameObject(t) && !t.IsValueType()) - return null; - - return (Func?)FastClonerExprGenerator.GenerateClonerInternal(t, asObject: true, skipCycleTracking: false, useShallowClassClone: true); - } - private static object? GenerateCloner(Type t, bool asObject) { if (FastClonerSafeTypes.CanReturnSameObject(t) && asObject && !t.IsValueType()) @@ -1541,28 +1525,30 @@ internal static T[] Clone1DimArraySafeInternal(T[] obj, FastCloneState state) try { object result = cloner(objFrom, objTo, state); + + if (!isDeep) + { + return result; + } - if (isDeep) + Type? lastWorkType = null; + Func? lastClonerTo = null; + while (state.TryPop(out object from, out object to, out Type workItemType)) { - Type? lastWorkType = null; - Func? lastClonerTo = null; - while (state.TryPop(out object from, out object to, out Type workItemType)) + Func clonerTo; + if (workItemType == lastWorkType && lastClonerTo is not null) { - Func clonerTo; - if (workItemType == lastWorkType && lastClonerTo is not null) - { - clonerTo = lastClonerTo; - } - else - { - clonerTo = (Func)FastClonerCache.GetOrAddDeepClassTo(workItemType, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)); - lastWorkType = workItemType; - lastClonerTo = clonerTo; - } - clonerTo(from, to, state); + clonerTo = lastClonerTo; } + else + { + clonerTo = (Func)FastClonerCache.GetOrAddDeepClassTo(workItemType, t => ClonerToExprGenerator.GenerateClonerInternal(t, true)); + lastWorkType = workItemType; + lastClonerTo = clonerTo; + } + clonerTo(from, to, state); } - + return result; } finally diff --git a/src/FastCloner/Code/PartialModels.cs b/src/FastCloner/Code/PartialModels.cs deleted file mode 100644 index 171a082..0000000 --- a/src/FastCloner/Code/PartialModels.cs +++ /dev/null @@ -1 +0,0 @@ -namespace FastCloner.Code; \ No newline at end of file diff --git a/src/FastCloner/Code/Polyfill.cs b/src/FastCloner/Code/Polyfill.cs new file mode 100644 index 0000000..bfa8b19 --- /dev/null +++ b/src/FastCloner/Code/Polyfill.cs @@ -0,0 +1,7 @@ +#if !MODERN +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit; +} +#endif \ No newline at end of file diff --git a/src/FastCloner/Code/ReflectionHelper.cs b/src/FastCloner/Code/ReflectionHelper.cs index 08b5777..633e5e0 100644 --- a/src/FastCloner/Code/ReflectionHelper.cs +++ b/src/FastCloner/Code/ReflectionHelper.cs @@ -13,26 +13,9 @@ internal static class ReflectionHelper public FieldInfo[] GetAllFields() => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); public PropertyInfo[] GetPublicProperties() => t.GetProperties(BindingFlags.Instance | BindingFlags.Public); public FieldInfo[] GetDeclaredFields() => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); - public ConstructorInfo[] GetPrivateConstructors() => t.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); public ConstructorInfo[] GetPublicConstructors() => t.GetConstructors(BindingFlags.Public | BindingFlags.Instance); public MethodInfo? GetPrivateMethod(string methodName) => t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); - public MethodInfo? GetMethod(string methodName) => t.GetMethod(methodName); public MethodInfo? GetPrivateStaticMethod(string methodName) => t.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static); - public FieldInfo? GetPrivateField(string fieldName) => t.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - public FieldInfo? GetPrivateStaticField(string fieldName) => t.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static); - - public bool IsSubclassOfTypeByName(string typeName) - { - while (t != null) - { - if (t.Name == typeName) - return true; - t = t.BaseType(); - } - - return false; - } - public Type[] GenericArguments() => t.GetGenericArguments(); } } \ No newline at end of file diff --git a/src/FastCloner/Code/ShallowClonerGenerator.cs b/src/FastCloner/Code/ShallowClonerGenerator.cs index de2d4de..23b7808 100644 --- a/src/FastCloner/Code/ShallowClonerGenerator.cs +++ b/src/FastCloner/Code/ShallowClonerGenerator.cs @@ -1,26 +1,23 @@ -namespace FastCloner.Code; +namespace FastCloner.Code; internal static class ShallowClonerGenerator { public static T? CloneObject(T obj) { - // this is faster than typeof(T).IsValueType - if (obj is ValueType) - { - if (typeof(T) == obj.GetType()) - return obj; + if (typeof(T).IsValueType) + return obj; - // we're here so, we clone value type obj as object type T - // so, we need to copy it, bcs we have a reference, not real object. - return (T)ShallowObjectCloner.CloneObject(obj); - } + if (obj is null) + return default; - if (ReferenceEquals(obj, null)) - return (T?)(object?)null; + Type runtimeType = obj.GetType(); + + if (runtimeType.IsValueType) + return (T)ShallowObjectCloner.DirectCloneObject(obj); - if (FastClonerSafeTypes.CanReturnSameObject(obj.GetType())) + if (FastClonerSafeTypes.CanReturnSameObject(runtimeType)) return obj; - return (T)ShallowObjectCloner.CloneObject(obj); + return (T)ShallowObjectCloner.DirectCloneObject(obj); } } \ No newline at end of file diff --git a/src/FastCloner/Code/ShallowObjectCloner.cs b/src/FastCloner/Code/ShallowObjectCloner.cs index 251913a..5ef06ff 100644 --- a/src/FastCloner/Code/ShallowObjectCloner.cs +++ b/src/FastCloner/Code/ShallowObjectCloner.cs @@ -1,39 +1,34 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; namespace FastCloner.Code; /// -/// Internal class but due implementation restriction should be public +/// Internal helper class used to perform shallow object cloning /// -public abstract class ShallowObjectCloner +internal static class ShallowObjectCloner { - /// - /// Abstract method for real object cloning - /// - protected abstract object DoCloneObject(object obj); - - private static readonly ShallowObjectCloner instance; - - /// - /// Performs real shallow object clone - /// - public static object CloneObject(object obj) => instance.DoCloneObject(obj); - - static ShallowObjectCloner() => instance = new ShallowSafeObjectCloner(); - - private class ShallowSafeObjectCloner : ShallowObjectCloner + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object CloneObject(object obj) + => DirectCloneObject(obj); + +#if MODERN + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "MemberwiseClone")] + public static extern object DirectCloneObject(object obj); +#else + public static object DirectCloneObject(object obj) { - private static readonly Func cloneFunc; - - static ShallowSafeObjectCloner() - { - MethodInfo? methodInfo = typeof(object).GetPrivateMethod(nameof(MemberwiseClone)); - ParameterExpression p = Expression.Parameter(typeof(object)); - MethodCallExpression mce = Expression.Call(p, methodInfo); - cloneFunc = Expression.Lambda>(mce, p).Compile(); - } + return cloneFunc(obj); + } + private static readonly Func cloneFunc = CreateCloneFunc(); - protected override object DoCloneObject(object obj) => cloneFunc(obj); + private static Func CreateCloneFunc() + { + MethodInfo methodInfo = typeof(object).GetPrivateMethod(nameof(MemberwiseClone))!; + ParameterExpression p = Expression.Parameter(typeof(object)); + MethodCallExpression mce = Expression.Call(p, methodInfo); + return Expression.Lambda>(mce, p).Compile(); } +#endif } \ No newline at end of file diff --git a/src/FastCloner/Code/StaticMethodInfos.cs b/src/FastCloner/Code/StaticMethodInfos.cs index e6360fe..877bbe5 100644 --- a/src/FastCloner/Code/StaticMethodInfos.cs +++ b/src/FastCloner/Code/StaticMethodInfos.cs @@ -36,15 +36,15 @@ internal static MethodInfo MakeFieldCloneMethodInfo(Type fieldType) => fieldType.IsValueType ? MakeStructCloneMethodInfo(fieldType) : CloneClassInternal; - + internal static MethodInfo MakeStructCloneMethodInfo(Type valueType) => structCloneMethodCache.GetOrAdd(valueType.TypeHandle.Value, _ => CloneStructInternal.MakeGenericMethod(valueType)); } internal static class CommonMethods { - internal static readonly MethodInfo MemberwiseClone = - typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance)!; + internal static readonly MethodInfo DirectCloneObject = + typeof(ShallowObjectCloner).GetMethod(nameof(ShallowObjectCloner.DirectCloneObject))!; internal static readonly MethodInfo Dispose = typeof(IDisposable).GetMethod(nameof(IDisposable.Dispose))!; internal static readonly MethodInfo EnumeratorMoveNext = diff --git a/src/FastCloner/FastCloner.csproj b/src/FastCloner/FastCloner.csproj index 1c09b63..5041de9 100644 --- a/src/FastCloner/FastCloner.csproj +++ b/src/FastCloner/FastCloner.csproj @@ -25,6 +25,7 @@ $(DefineConstants);MODERN + $(DefineConstants);MODERN_10 diff --git a/src/FastCloner/FastClonerExtensions.cs b/src/FastCloner/FastClonerExtensions.cs index 603db70..2e9260f 100644 --- a/src/FastCloner/FastClonerExtensions.cs +++ b/src/FastCloner/FastClonerExtensions.cs @@ -12,25 +12,25 @@ internal static class FastClonerExtensions /// /// Performs deep (full) copy of object and related graph /// - public T DeepClone() => FastClonerGenerator.CloneObject(obj); + public T DeepClone() => FastClonerGenerator.CloneObject(obj)!; /// /// Performs deep (full) copy of object and related graph to existing object /// /// existing filled object /// Method is valid only for classes, classes should be descendants in reality, not in declaration - public TTo DeepCloneTo(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, true); + public TTo DeepCloneTo(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, true)!; /// /// Performs shallow copy of object to existing object /// /// existing filled object /// Method is valid only for classes, classes should be descendants in reality, not in declaration - public TTo ShallowCloneTo(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, false); + public TTo ShallowCloneTo(TTo objTo) where TTo : class, T => (TTo)FastClonerGenerator.CloneObjectTo(obj, objTo, false)!; /// /// Performs shallow (only new object returned, without cloning of dependencies) copy of object /// - public T ShallowClone() => ShallowClonerGenerator.CloneObject(obj); + public T ShallowClone() => ShallowClonerGenerator.CloneObject(obj)!; } } \ No newline at end of file