Skip to content

Commit 88dfe9a

Browse files
committed
use iterative approach when cloning deeply nested objects to avoid running out of stack
1 parent 02612b3 commit 88dfe9a

File tree

6 files changed

+288
-23
lines changed

6 files changed

+288
-23
lines changed

FastCloner.Tests/SpecialCaseTests.cs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2883,10 +2883,10 @@ public void JsonObjectConstructorTest()
28832883
// JsonObject has constructor: JsonObject(JsonNodeOptions? options = null)
28842884
// So it should be callable with no arguments
28852885

2886-
var original = new JsonObject { ["test"] = "value" };
2886+
JsonObject original = new JsonObject { ["test"] = "value" };
28872887

28882888
// This should now work without the special JsonNode processors
2889-
var clone = original.DeepClone();
2889+
JsonNode clone = original.DeepClone();
28902890

28912891
Assert.Multiple(() =>
28922892
{
@@ -3046,15 +3046,15 @@ public void Drawing_Brush_DeepClone_Test()
30463046
public void AssemblyName_DeepClone_Test()
30473047
{
30483048
// Arrange
3049-
var original = new AssemblyName
3049+
AssemblyName original = new AssemblyName
30503050
{
30513051
Name = "MyTestAssembly",
30523052
Version = new Version(1, 2, 3, 4)
30533053
};
3054-
var originalVersion = new Version(1, 2, 3, 4);
3054+
Version originalVersion = new Version(1, 2, 3, 4);
30553055

30563056
// Act
3057-
var clone = original.DeepClone();
3057+
AssemblyName clone = original.DeepClone();
30583058
original.Version = new Version(5, 6, 7, 8); // Modify the original
30593059

30603060
// Assert
@@ -3067,4 +3067,72 @@ public void AssemblyName_DeepClone_Test()
30673067
Assert.That(clone.Version, Is.Not.EqualTo(original.Version));
30683068
});
30693069
}
3070+
3071+
public class LargeNode
3072+
{
3073+
public LargeNode Parent { get; set; }
3074+
public LargeNode Child { get; set; }
3075+
public LargeNode Previous { get; set; }
3076+
public LargeNode Next { get; set; }
3077+
public List<int> Data { get; set; } = [];
3078+
}
3079+
3080+
[Test]
3081+
public void LargeCircular_Test()
3082+
{
3083+
// Arrange
3084+
LargeNode root = new LargeNode { Data = [0] };
3085+
LargeNode current = root;
3086+
const int nodeCount = 10_000; // stackoverflow for n >= 8000
3087+
3088+
for (int i = 1; i < nodeCount; i++)
3089+
{
3090+
LargeNode next = new LargeNode { Data = [i], Parent = current };
3091+
current.Child = next;
3092+
next.Previous = current;
3093+
current.Next = next;
3094+
current = next;
3095+
}
3096+
3097+
current.Next = root;
3098+
root.Previous = current;
3099+
3100+
// Act
3101+
LargeNode clone = root.DeepClone();
3102+
3103+
// Assert
3104+
Assert.That(clone, Is.Not.SameAs(root));
3105+
3106+
LargeNode forward = clone;
3107+
for (int i = 0; i < nodeCount; i++)
3108+
{
3109+
forward = forward.Next;
3110+
}
3111+
Assert.That(forward, Is.SameAs(clone));
3112+
3113+
LargeNode backward = clone;
3114+
for (int i = 0; i < nodeCount; i++)
3115+
{
3116+
backward = backward.Previous;
3117+
}
3118+
Assert.That(backward, Is.SameAs(clone));
3119+
3120+
// Traverse to a deeply nested node
3121+
LargeNode originalNode = root;
3122+
LargeNode clonedNode = clone;
3123+
for (int i = 0; i < nodeCount / 2; i++)
3124+
{
3125+
originalNode = originalNode.Next;
3126+
clonedNode = clonedNode.Next;
3127+
}
3128+
3129+
Assert.That(clonedNode, Is.Not.SameAs(originalNode));
3130+
Assert.That(clonedNode.Data, Is.Not.SameAs(originalNode.Data));
3131+
Assert.That(clonedNode.Data, Is.EqualTo(originalNode.Data));
3132+
3133+
clonedNode.Data.Add(999);
3134+
Assert.That(originalNode.Data, Has.Count.EqualTo(1));
3135+
Assert.That(clonedNode.Data, Has.Count.EqualTo(2));
3136+
Assert.That(originalNode.Data[0], Is.Not.EqualTo(clonedNode.Data[1]));
3137+
}
30703138
}

FastCloner/Code/FastCloneState.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
1-
namespace FastCloner.Code;
1+
using System.Reflection;
2+
3+
namespace FastCloner.Code;
24

35
using System.Runtime.CompilerServices;
46

57
internal sealed class FastCloneState
68
{
9+
internal static readonly PropertyInfo UseWorkListProp = typeof(FastCloneState).GetProperty("UseWorkList")!;
10+
711
private MiniDictionary? loops;
812
private readonly object[] baseFromTo = new object[6];
913
private int idx;
1014

15+
// iterative worklist support
16+
private WorkItem[]? workItems;
17+
private int workCount;
18+
public bool UseWorkList { get; set; }
19+
private int callDepth;
20+
21+
private readonly struct WorkItem
22+
{
23+
public readonly object From;
24+
public readonly object To;
25+
public readonly Type Type;
26+
27+
public WorkItem(object from, object to, Type type)
28+
{
29+
From = from;
30+
To = to;
31+
Type = type;
32+
}
33+
}
34+
1135
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1236
public object? GetKnownRef(object from)
1337
{
@@ -37,6 +61,47 @@ public void AddKnownRef(object from, object to)
3761
loops.Insert(from, to);
3862
}
3963

64+
public void EnqueueProcess(object from, object to, Type type)
65+
{
66+
WorkItem[] local = workItems ??= new WorkItem[16];
67+
if (workCount == local.Length)
68+
{
69+
int newSize = local.Length * 2;
70+
WorkItem[] resized = new WorkItem[newSize];
71+
Array.Copy(local, resized, workCount);
72+
workItems = local = resized;
73+
}
74+
75+
local[workCount++] = new WorkItem(from, to, type);
76+
}
77+
78+
public bool TryPop(out object from, out object to, out Type type)
79+
{
80+
if (workCount == 0)
81+
{
82+
from = null;
83+
to = null;
84+
type = null;
85+
return false;
86+
}
87+
88+
WorkItem wi = workItems![--workCount];
89+
from = wi.From;
90+
to = wi.To;
91+
type = wi.Type;
92+
return true;
93+
}
94+
95+
public int IncrementDepth()
96+
{
97+
return ++callDepth;
98+
}
99+
100+
public void DecrementDepth()
101+
{
102+
if (callDepth > 0) callDepth--;
103+
}
104+
40105
private sealed class MiniDictionary
41106
{
42107
private struct Entry(int hashCode, int next, object key, object value)

FastCloner/Code/FastClonerExprGenerator.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -288,21 +288,25 @@ private static void AddMemberCloneExpressions(
288288
continue;
289289
}
290290

291-
MethodInfo cloneMethodInfo = memberType.IsValueType()
292-
? typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneStructInternal))!.MakeGenericMethod(memberType)
293-
: typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneClassInternal))!;
294-
291+
Expression clonedValueExpression;
295292
MemberExpression getMemberValue = Expression.MakeMemberAccess(fromLocal, member);
296293
Expression originalMemberValue = getMemberValue;
297294

298-
Expression callClone = Expression.Call(cloneMethodInfo, originalMemberValue, state);
299-
300-
if (!memberType.IsValueType())
295+
if (memberType.IsValueType())
301296
{
302-
callClone = Expression.Convert(callClone, memberType);
297+
MethodInfo structClone = typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneStructInternal))!.MakeGenericMethod(memberType);
298+
clonedValueExpression = Expression.Call(structClone, originalMemberValue, state);
299+
}
300+
else
301+
{
302+
MethodInfo classDeep = typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneClassInternal))!;
303+
MethodInfo classShallowTrack = typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneClassShallowAndTrack))!;
304+
PropertyInfo useWorkListProp = FastCloneState.UseWorkListProp;
305+
Expression deepCall = Expression.Call(classDeep, originalMemberValue, state);
306+
Expression shallowCall = Expression.Call(classShallowTrack, originalMemberValue, state);
307+
Expression selected = Expression.Condition(Expression.Property(state, useWorkListProp), shallowCall, deepCall);
308+
clonedValueExpression = Expression.Convert(selected, memberType);
303309
}
304-
305-
Expression clonedValueExpression = callClone;
306310

307311
switch (member)
308312
{

FastCloner/Code/FastClonerGenerator.cs

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,75 @@ internal static class FastClonerGenerator
6767
if (obj == null)
6868
return null;
6969

70-
Func<object, FastCloneState, object>? cloner = (Func<object, FastCloneState, object>?)FastClonerCache.GetOrAddClass(obj.GetType(), t => GenerateCloner(t, true));
70+
Type rootType = obj.GetType();
71+
Func<object, FastCloneState, object>? cloner = (Func<object, FastCloneState, object>?)FastClonerCache.GetOrAddClass(rootType, t => GenerateCloner(t, true));
7172

7273
// null -> should return same type
73-
return cloner is null ? obj : cloner(obj, new FastCloneState());
74+
if (cloner is null)
75+
{
76+
return obj;
77+
}
78+
79+
FastCloneState state = new FastCloneState
80+
{
81+
UseWorkList = TypeHasDirectSelfReference(rootType)
82+
};
83+
84+
if (!state.UseWorkList)
85+
{
86+
try
87+
{
88+
int current = state.IncrementDepth();
89+
90+
if (current >= FastCloner.MaxRecursionDepth)
91+
{
92+
state.DecrementDepth();
93+
state.UseWorkList = true;
94+
}
95+
else
96+
{
97+
object resultNormal = cloner(obj, state);
98+
state.DecrementDepth();
99+
return resultNormal;
100+
}
101+
}
102+
catch
103+
{
104+
state.DecrementDepth();
105+
throw;
106+
}
107+
}
108+
109+
object result = cloner(obj, state);
110+
while (state.TryPop(out object from, out object to, out Type type))
111+
{
112+
Func<object, object, FastCloneState, object> clonerTo = (Func<object, object, FastCloneState, object>)FastClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true));
113+
clonerTo(from, to, state);
114+
}
115+
116+
return result;
117+
}
118+
119+
private static bool TypeHasDirectSelfReference(Type type)
120+
{
121+
Type? tp = type;
122+
while (tp != null && tp != typeof(ContextBoundObject))
123+
{
124+
foreach (FieldInfo fi in tp.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly))
125+
{
126+
Type ft = fi.FieldType;
127+
if (ft == type)
128+
{
129+
return true;
130+
}
131+
if (ft.IsArray && ft.GetElementType() == type)
132+
{
133+
return true;
134+
}
135+
}
136+
tp = tp.BaseType;
137+
}
138+
return false;
74139
}
75140

76141
internal static object? CloneClassInternal(object? obj, FastCloneState state)
@@ -94,10 +159,68 @@ internal static class FastClonerGenerator
94159
{
95160
return obj;
96161
}
162+
163+
if (state.UseWorkList)
164+
{
165+
object? knownA = state.GetKnownRef(obj);
166+
return knownA ?? CloneClassShallowAndTrack(obj, state);
167+
}
168+
169+
try
170+
{
171+
int current = state.IncrementDepth();
172+
if (current > 1000)
173+
{
174+
state.DecrementDepth();
175+
state.UseWorkList = true;
176+
object? knownB = state.GetKnownRef(obj);
177+
return knownB ?? CloneClassShallowAndTrack(obj, state);
178+
}
179+
180+
object? knownRef = state.GetKnownRef(obj);
181+
return knownRef ?? cloner(obj, state);
182+
}
183+
finally
184+
{
185+
state.DecrementDepth();
186+
}
187+
}
188+
189+
internal static object? CloneClassShallowAndTrack(object? obj, FastCloneState state)
190+
{
191+
if (obj is null)
192+
{
193+
return null;
194+
}
195+
196+
Type objType = obj.GetType();
197+
198+
if (FastClonerCache.IsTypeIgnored(objType))
199+
{
200+
return null;
201+
}
202+
203+
if (FastClonerSafeTypes.CanReturnSameObject(objType))
204+
{
205+
return obj;
206+
}
97207

98-
// loop
99208
object? knownRef = state.GetKnownRef(obj);
100-
return knownRef ?? cloner(obj, state);
209+
if (knownRef is not null)
210+
{
211+
return knownRef;
212+
}
213+
214+
MethodInfo methodInfo = typeof(object).GetPrivateMethod(nameof(MemberwiseClone))!;
215+
object? shallow = methodInfo.Invoke(obj, null);
216+
state.AddKnownRef(obj, shallow);
217+
218+
if (state.UseWorkList)
219+
{
220+
state.EnqueueProcess(obj, shallow, objType);
221+
}
222+
223+
return shallow;
101224
}
102225

103226
internal static T CloneStructInternal<T>(T obj, FastCloneState state)
@@ -266,10 +389,9 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] obj, FastCloneState state)
266389
throw new InvalidOperationException("From object should be derived from From object, but From object has type " + objFrom.GetType().FullName + " and to " + objTo.GetType().FullName);
267390
if (objFrom is string)
268391
throw new InvalidOperationException("It is forbidden to clone strings");
269-
Func<object, object, FastCloneState, object>? cloner = (Func<object, object, FastCloneState, object>)(isDeep
392+
Func<object, object, FastCloneState, object>? cloner = (Func<object, object, FastCloneState, object>?)(isDeep
270393
? FastClonerCache.GetOrAddDeepClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, true))
271394
: FastClonerCache.GetOrAddShallowClassTo(type, t => ClonerToExprGenerator.GenerateClonerInternal(t, false)));
272-
if (cloner == null) return objTo;
273-
return cloner(objFrom, objTo, new FastCloneState());
395+
return cloner is null ? objTo : cloner(objFrom, objTo, new FastCloneState());
274396
}
275397
}

0 commit comments

Comments
 (0)