Skip to content

Commit 992bb95

Browse files
committed
performance improvements: constructor analyzing and reference counter dictionary optimization for small types
1 parent 255be97 commit 992bb95

11 files changed

Lines changed: 203 additions & 29 deletions

DeepCloner.Tests/ArraysSpec.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,16 @@ public void Dictionary_Should_Be_Cloned()
140140
public void Array_Of_Same_Arrays_Should_Be_Cloned()
141141
{
142142
var c1 = new[] { 1, 2, 3 };
143-
var arr = new[] { c1, c1, c1 };
143+
var arr = new[] { c1, c1, c1, c1, c1 };
144144
var cloned = arr.DeepClone();
145145

146-
Assert.That(cloned.Length, Is.EqualTo(3));
146+
Assert.That(cloned.Length, Is.EqualTo(5));
147+
// lot of objects for checking reference dictionary optimization
147148
Assert.That(ReferenceEquals(arr[0], cloned[0]), Is.False);
148149
Assert.That(ReferenceEquals(cloned[0], cloned[1]), Is.True);
149150
Assert.That(ReferenceEquals(cloned[1], cloned[2]), Is.True);
151+
Assert.That(ReferenceEquals(cloned[1], cloned[3]), Is.True);
152+
Assert.That(ReferenceEquals(cloned[1], cloned[4]), Is.True);
150153
}
151154

152155
public class AC

DeepCloner.Tests/DeepCloner.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
<Compile Include="Properties\AssemblyInfo.cs" />
6464
<Compile Include="ShallowClonerSpec.cs" />
6565
<Compile Include="SimpleObjectSpec.cs" />
66-
<Compile Include="StandardTypesSpec.cs" />
66+
<Compile Include="SystemTypesSpec.cs" />
6767
</ItemGroup>
6868
<ItemGroup>
6969
<None Include="packages.config" />

DeepCloner.Tests/PerformanceSpec.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ private class C2 : C1
3232
{
3333
}
3434

35+
// safe class
36+
public class C3
37+
{
38+
public int V1 { get; set; }
39+
40+
public string V2 { get; set; }
41+
}
42+
3543
private C1 ManualDeepClone(C1 x)
3644
{
3745
var y = new C1();
@@ -77,15 +85,15 @@ public void Test_Construct_Variants(bool isSafe)
7785
var sw = new Stopwatch();
7886
sw.Start();
7987

80-
for (var i = 0; i < 10000000; i++) ManualDeepClone(c1);
88+
for (var i = 0; i < 1000000; i++) ManualDeepClone(c1);
8189
Console.WriteLine("Manual: " + sw.ElapsedMilliseconds);
8290
sw.Restart();
8391

84-
for (var i = 0; i < 10000000; i++) c1.DeepClone();
92+
for (var i = 0; i < 1000000; i++) c1.DeepClone();
8593
Console.WriteLine("Deep: " + sw.ElapsedMilliseconds);
8694
sw.Restart();
8795

88-
for (var i = 0; i < 10000000; i++) c1.GetClone();
96+
for (var i = 0; i < 1000000; i++) c1.GetClone();
8997
Console.WriteLine("Clone Extensions: " + sw.ElapsedMilliseconds);
9098
sw.Restart();
9199

@@ -94,6 +102,30 @@ public void Test_Construct_Variants(bool isSafe)
94102
Console.WriteLine("Binary Formatter: " + (sw.ElapsedMilliseconds * 10));
95103
}
96104

105+
[Test, Ignore("Manual")]
106+
[TestCase(false)]
107+
[TestCase(true)]
108+
public void Test_Construct_Safe_Class_Variants(bool isSafe)
109+
{
110+
// we cache cloners for type, so, this only variant with separate run
111+
BaseTest.SwitchTo(isSafe);
112+
var c1 = new C3 { V1 = 1, V2 = "xxx" };
113+
for (var i = 0; i < 1000; i++) c1.GetClone();
114+
for (var i = 0; i < 1000; i++) c1.DeepClone();
115+
116+
// test
117+
var sw = new Stopwatch();
118+
sw.Start();
119+
120+
for (var i = 0; i < 10000000; i++) c1.DeepClone();
121+
Console.WriteLine("Deep: " + sw.ElapsedMilliseconds);
122+
sw.Restart();
123+
124+
for (var i = 0; i < 10000000; i++) c1.GetClone();
125+
Console.WriteLine("Clone Extensions: " + sw.ElapsedMilliseconds);
126+
sw.Restart();
127+
}
128+
97129
[Test, Ignore("Manual")]
98130
public void Test_Parent_Casting_Variants()
99131
{
Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ namespace Force.DeepCloner.Tests
1111
{
1212
[TestFixture(false)]
1313
[TestFixture(true)]
14-
public class StandardTypesSpec : BaseTest
14+
public class SystemTypesSpec : BaseTest
1515
{
16-
public StandardTypesSpec(bool isSafeInit)
16+
public SystemTypesSpec(bool isSafeInit)
1717
: base(isSafeInit)
1818
{
1919
}
@@ -76,5 +76,43 @@ public void Funcs_Should_Be_Cloned()
7676
Assert.That(df(3), Is.EqualTo("1233"));
7777
Assert.That(cf(3), Is.EqualTo("xxx3"));
7878
}
79+
80+
public class EventHandlerTest1
81+
{
82+
public event Action<int> Event;
83+
84+
public int Call(int x)
85+
{
86+
if (Event != null)
87+
{
88+
Event(x);
89+
return Event.GetInvocationList().Length;
90+
}
91+
92+
return 0;
93+
}
94+
}
95+
96+
[Test(Description = "Some libraries notifies about problems with this types")]
97+
public void Events_Should_Be_Cloned()
98+
{
99+
var eht = new EventHandlerTest1();
100+
var summ = new int[1];
101+
Action<int> a1 = x => summ[0] += x;
102+
Action<int> a2 = x => summ[0] += x;
103+
eht.Event += a1;
104+
eht.Event += a2;
105+
eht.Call(1);
106+
Assert.That(summ[0], Is.EqualTo(2));
107+
var clone = eht.DeepClone();
108+
clone.Call(1);
109+
// do not call
110+
Assert.That(summ[0], Is.EqualTo(2));
111+
eht.Event -= a1;
112+
eht.Event -= a2;
113+
Assert.That(eht.Call(1), Is.EqualTo(0)); // 0
114+
Assert.That(summ[0], Is.EqualTo(2)); // nothing to increment
115+
Assert.That(clone.Call(1), Is.EqualTo(2));
116+
}
79117
}
80118
}

DeepCloner.nuspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@
33
<metadata>
44
<id>DeepCloner</id>
55
<title>DeepCloner</title>
6-
<version>0.9.0.1</version>
6+
<version>0.9.1</version>
77
<authors>force</authors>
88
<owners>force</owners>
99
<licenseUrl>https://github.com/force-net/DeepCloner/blob/develop/LICENSE</licenseUrl>
1010
<projectUrl>https://github.com/force-net/DeepCloner</projectUrl>
1111
<!--iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl-->
1212
<requireLicenseAcceptance>false</requireLicenseAcceptance>
13-
<description>Small Library for fast deep or shallow cloning .NET objects</description>
13+
<description>Small Library for fast deep or shallow cloning .NET objects. It allows to copy everything and has a lot of performance tricks for fast copying.</description>
1414
<releaseNotes>
1515
Fixed an issue with handling references to arrays
1616
Added support for multidimensional arrays and non-zero-based arrays
1717
Added fallback for non-fulltrust code through expressions
1818
Due build issue, release 0.9.0 was invalid, this is fixed variant
1919
</releaseNotes>
2020
<copyright>Copyright by Force 2016</copyright>
21-
<tags>.NET shallow deep clone</tags>
21+
<tags>.NET shallow deep clone DeepClone fast</tags>
2222
</metadata>
2323
<files><file src="DeepCloner\bin\Release\DeepCloner.*" target="lib\net40" /></files>
2424
</package>

DeepCloner/DeepCloner.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<Compile Include="Helpers\DeepClonerCache.cs" />
5555
<Compile Include="Helpers\DeepClonerGenerator.cs" />
5656
<Compile Include="Helpers\DeepClonerMsilGenerator.cs" />
57+
<Compile Include="Helpers\DeepClonerMsilHelper.cs" />
5758
<Compile Include="Helpers\DeepClonerSafeTypes.cs" />
5859
<Compile Include="Helpers\DeepCloneState.cs" />
5960
<Compile Include="Helpers\ShallowClonerGenerator.cs" />

DeepCloner/Helpers/DeepCloneState.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
using System.Collections.Generic;
1+
using System.Collections;
2+
using System.Collections.Generic;
23
using System.Runtime.CompilerServices;
34

45
namespace Force.DeepCloner.Helpers
56
{
67
internal class DeepCloneState
78
{
8-
private class CustomEqualityComparer : IEqualityComparer<object>
9+
private class CustomEqualityComparer : IEqualityComparer<object>, IEqualityComparer
910
{
1011
bool IEqualityComparer<object>.Equals(object x, object y)
1112
{
1213
return ReferenceEquals(x, y);
1314
}
1415

16+
bool IEqualityComparer.Equals(object x, object y)
17+
{
18+
return ReferenceEquals(x, y);
19+
}
20+
1521
public int GetHashCode(object obj)
1622
{
1723
return RuntimeHelpers.GetHashCode(obj);
@@ -20,10 +26,21 @@ public int GetHashCode(object obj)
2026

2127
private static readonly CustomEqualityComparer Instance = new CustomEqualityComparer();
2228

23-
private readonly Dictionary<object, object> _loops = new Dictionary<object, object>(Instance);
29+
private Dictionary<object, object> _loops;
30+
31+
private readonly object[] _baseFrom = new object[3];
32+
private readonly object[] _baseTo = new object[3];
33+
34+
private int _idx;
2435

2536
public object GetKnownRef(object from)
2637
{
38+
// this is faster than call Diectionary from begin
39+
// also, small poco objects does not have a lot of references
40+
if (ReferenceEquals(from, _baseFrom[0])) return _baseTo[0];
41+
if (ReferenceEquals(from, _baseFrom[1])) return _baseTo[1];
42+
if (ReferenceEquals(from, _baseFrom[2])) return _baseTo[2];
43+
if (_loops == null) return null;
2744
object value;
2845
if (_loops.TryGetValue(from, out value)) return value;
2946
// null cannot bee a loop
@@ -32,6 +49,14 @@ public object GetKnownRef(object from)
3249

3350
public void AddKnownRef(object from, object to)
3451
{
52+
if (_idx < 3)
53+
{
54+
_baseFrom[_idx] = from;
55+
_baseTo[_idx] = to;
56+
_idx++;
57+
return;
58+
}
59+
if (_loops == null) _loops = new Dictionary<object, object>(Instance);
3560
_loops[from] = to;
3661
}
3762
}

DeepCloner/Helpers/DeepClonerGenerator.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Linq;
3-
using System.Reflection;
43

54
namespace Force.DeepCloner.Helpers
65
{
@@ -10,18 +9,23 @@ public static T CloneObject<T>(T obj)
109
{
1110
return obj is ValueType && typeof(T) == obj.GetType()
1211
? CloneStructInternal(obj, new DeepCloneState())
13-
: CloneClassRoot<T>(obj);
12+
: (T)CloneClassRoot(obj);
1413
}
1514

16-
private static T CloneClassRoot<T>(object obj)
15+
private static object CloneClassRoot(object obj)
1716
{
18-
if (obj == null) return default(T);
17+
if (obj == null) return null;
1918

2019
// we can receive an poco objects which is faster to copy in shallow way if possible
21-
if (DeepClonerSafeTypes.IsClassSafe(obj.GetType()))
22-
return (T)ShallowObjectCloner.CloneObject(obj);
23-
24-
return (T)CloneClassInternal(obj, new DeepCloneState());
20+
var type = obj.GetType();
21+
// 200ms
22+
if (DeepClonerSafeTypes.IsClassSafe(type))
23+
return ShallowObjectCloner.CloneObject(obj);
24+
// 350ms
25+
var cloner = (Func<object, DeepCloneState, object>)DeepClonerCache.GetOrAddClass(type, t => GenerateCloner(t, true));
26+
27+
// 200ms
28+
return cloner(obj, new DeepCloneState());
2529
}
2630

2731
public static T CloneStruct<T>(T obj) where T : struct

DeepCloner/Helpers/DeepClonerMsilGenerator.cs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ internal static object GenerateClonerInternal(Type realType, bool asObject)
2121
var dt = new DynamicMethod(
2222
"DeepObjectCloner_" + realType.Name + "_" + Interlocked.Increment(ref _methodCounter), methodType, new[] { methodType, typeof(DeepCloneState) }, mb, true);
2323

24+
dt.InitLocals = false;
25+
2426
var il = dt.GetILGenerator();
27+
/*il.Emit(OpCodes.Ldarg_0);
28+
il.Emit(OpCodes.Ret);*/
2529

2630
GenerateProcessMethod(il, realType, asObject && realType.IsValueType);
2731

@@ -41,10 +45,27 @@ private static void GenerateProcessMethod(ILGenerator il, Type type, bool unboxS
4145
var typeLocal = il.DeclareLocal(type);
4246
LocalBuilder structLoc = null;
4347

48+
var isGoodConstructor = false;
49+
4450
if (!type.IsValueType)
4551
{
46-
il.Emit(OpCodes.Ldarg_0);
47-
il.Emit(OpCodes.Call, typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic));
52+
var constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
53+
54+
isGoodConstructor = DeepClonerMsilHelper.IsConstructorDoNothing(type, constructor);
55+
56+
// isGoodConstructor = false;
57+
58+
// objects are used for locking and they are safe for constructor call
59+
if (isGoodConstructor)
60+
{
61+
il.Emit(OpCodes.Newobj, constructor);
62+
}
63+
else
64+
{
65+
il.Emit(OpCodes.Ldarg_0);
66+
il.Emit(OpCodes.Call, typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic));
67+
}
68+
4869
il.Emit(OpCodes.Stloc, typeLocal);
4970
}
5071
else
@@ -96,12 +117,24 @@ private static void GenerateProcessMethod(ILGenerator il, Type type, bool unboxS
96117
il.Emit(OpCodes.Ldfld, fieldInfo);
97118
il.Emit(OpCodes.Ldarg_1);
98119

99-
var methodInfo = fieldInfo.FieldType.IsValueType
100-
? typeof(DeepClonerGenerator).GetMethod("CloneStructInternal", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(fieldInfo.FieldType)
101-
: typeof(DeepClonerGenerator).GetMethod("CloneClassInternal", BindingFlags.NonPublic | BindingFlags.Static);
120+
var methodInfo = fieldInfo.FieldType.IsValueType
121+
? typeof(DeepClonerGenerator).GetMethod("CloneStructInternal", BindingFlags.NonPublic | BindingFlags.Static)
122+
.MakeGenericMethod(fieldInfo.FieldType)
123+
: typeof(DeepClonerGenerator).GetMethod("CloneClassInternal", BindingFlags.NonPublic | BindingFlags.Static);
102124
il.Emit(OpCodes.Call, methodInfo);
103125
il.Emit(OpCodes.Stfld, fieldInfo);
104126
}
127+
else
128+
{
129+
// for good consturctor we use direct field copy
130+
if (isGoodConstructor)
131+
{
132+
il.Emit(type.IsClass ? OpCodes.Ldloc : OpCodes.Ldloca_S, typeLocal);
133+
il.Emit(OpCodes.Ldarg_0);
134+
il.Emit(OpCodes.Ldfld, fieldInfo);
135+
il.Emit(OpCodes.Stfld, fieldInfo);
136+
}
137+
}
105138
}
106139

107140
il.Emit(OpCodes.Ldloc, typeLocal);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace Force.DeepCloner.Helpers
5+
{
6+
internal static class DeepClonerMsilHelper
7+
{
8+
public static bool IsConstructorDoNothing(Type type, ConstructorInfo constructor)
9+
{
10+
if (constructor == null) return false;
11+
try
12+
{
13+
var methodBody = constructor.GetMethodBody();
14+
15+
var ilAsByteArray = methodBody.GetILAsByteArray();
16+
if (ilAsByteArray.Length == 7
17+
&& ilAsByteArray[0] == 0x02 // Ldarg_0
18+
&& ilAsByteArray[1] == 0x28 // newobj
19+
&& ilAsByteArray[6] == 0x2a // ret
20+
&& type.Module.ResolveMethod(BitConverter.ToInt32(ilAsByteArray, 2)) == typeof(object).GetConstructor(Type.EmptyTypes)) // call object
21+
{
22+
return true;
23+
}
24+
else if (ilAsByteArray.Length == 1 && ilAsByteArray[0] == 0x2a) // ret
25+
{
26+
return true;
27+
}
28+
29+
return false;
30+
}
31+
catch (Exception)
32+
{
33+
// no permissions or something similar
34+
return false;
35+
}
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)