
Recently, I faced the following task: it is necessary to compare many pairs of objects. But there is one nuance: objects are the object’s most, and you need to compare across the entire set of public properties. And it is absolutely not necessary that the types of compared objects implement the
IEquatable<T>
interface.
It was obvious that reflection should be used. However, during the implementation, I was faced with many subtleties and eventually resorted to the
black magic of the dynamic generation of IL. In this article I would like to share my experience and talk about how I solved this problem. If you are interested in the topic, then please under the cat!
Part 1. Object in reflection
“Submit here MFC IEqualityComparer!”, He shouted, stamping with all four paws
In .NET, it is assumed that classes that perform object comparisons implement the
IEqualityComparer <T> interface. This allows you to embed them in collection classes, such as lists and dictionaries, and use custom equality equality when searching. We will not deviate from this agreement and proceed to implement the
IEqualityComparer<object>
interface using the reflection mechanism. Recall that reflection refers to inspecting metadata and compiled code during program execution (readers unfamiliar with reflection are strongly advised to read the chapter “Reflection and Metadata” in the book by Joseph and Ben Albahari
“C # 5.0. Reference. Full Language Description” ).
')
public class ReflectionComparer : IEqualityComparer<object> { public new bool Equals(object x, object y) { public new bool Equals(object x, object y) { return CompareObjectsInternal(x?.GetType(), x, y); } } public int GetHashCode(object obj) { return obj.GetHashCode(); } private bool CompareObjectsInternal(Type type, object x, object y) { throw new NotImplementedException(); } }
Note that the
Equals
method is marked as
new
. This is done because it overlaps the
Object.Equals(object, object)
method.
Now let's take a step back and determine more precisely what we are going to compare. Objects can be arbitrarily complex, everything must be considered. By combining the following types of properties we can cover a wide class of input data:
- all primitive types (Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, Single);
- lines;
- arrays (for simplicity, we will consider only one-dimensional arrays);
- transfers;
- collections that implement the IEnumerable <T> interface;
- structures;
- classes.
We write the framework of the
CompareObjectsInternal
method, and then consider some special cases.
private bool CompareObjectsInternal(Type type, object x, object y) { // if (ReferenceEquals(x, y)) return true; // null if (ReferenceEquals(x, null) != ReferenceEquals(y, null)) return false; // if (x.GetType() != y.GetType()) return false; // if (Type.GetTypeCode(type) == TypeCode.String) return ((string)x).Equals((string)y); // if (type.IsArray) return CompareArrays(type, x, y); // if (type.IsImplementIEnumerable()) return CompareEnumerables(type, x, y); // if (type.IsClass || type.IsInterface) return CompareAllProperties(type, x, y); // if (type.IsPrimitive || type.IsEnum) return x.Equals(y); // if (type.IsNullable()) return CompareNullables(type, x, y); // if (type.IsValueType) return CompareAllProperties(type, x, y); return x.Equals(y); }
The above code is quite understandable: first, we check objects for reference equality and
null
equality, then we check types, and then we consider various cases. Moreover, for strings, primitive types, and enumeration types, without further ado, we call the
Equals
method. To check the types of belonging to nullable types and types of collections, we use the
IsNullable
and
IsImplementIEnumerable
extension methods, the source code of which can be found below.
Extension method code public static bool IsImplementIEnumerable(this Type type) => type.GetInterface("IEnumerable`1") != null; public static Type GetIEnumerableInterface(this Type type) => type.GetInterface("IEnumerable`1"); public static bool IsNullable(this Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof (Nullable<>);
In the case of classes, interfaces, and structures, we compare all the relevant properties. Note that indexers in .NET are also properties, but we will limit ourselves to simple (parameterless) properties that have getters, and the comparison of collections will be processed separately. To check whether a property is an indexer, we can use the
PropertyInfo.GetIndexParameters method. If the method returned an array of nonzero length, then we are dealing with an indexer.
private bool CompareAllProperties(Type type, object x, object y) { var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); var readableNonIndexers = properties.Where(p => p.CanRead && p.GetIndexParameters().Length == 0); foreach (PropertyInfo propertyInfo in readableNonIndexers) { var a = propertyInfo.GetValue(x, null); var b = propertyInfo.GetValue(y, null); if (!CompareObjectsInternal(propertyInfo.PropertyType, a, b)) return false; } return true; }
The next case is a comparison of nullable objects. If the underlying type is primitive, then we can compare the values using the
Equals
method. If there are structures inside objects, then it is necessary to first extract the inner signs and then compare them for all public properties. We can safely access the
Value
property, since equality tests have already been performed earlier in the
CompareObjectsInternal
method.
private bool CompareNullables(Type type, object x, object y) { Type underlyingTypeOfNullableType = Nullable.GetUnderlyingType(type); if (underlyingTypeOfNullableType.IsPrimitive) { return x.Equals(y); } var valueProperty = type.GetProperty("Value"); var a = valueProperty.GetValue(x, null); var b = valueProperty.GetValue(y, null); return CompareAllProperties(underlyingTypeOfNullableType, a, b); }
Let us turn to the comparison of collections. You can implement the implementation in at least two ways: write your non-generic method that works with IEnumerable, or use the
Enumerable.SequenceEqual <TSource> extension method. For collections whose elements are type-values, it is obvious that the non-generic method will work more slowly, since he will constantly have to do the packing / unpacking of the values. When using the same LINQ method, we first need to substitute the type of the collection's elements into the TSource type parameter using the
MakeGenericMethod method), and then call the method by passing the two compared collections. Moreover, if the collection contains elements of non-primitive types, then we can pass an additional argument — the comparer — the current instance of our
ReflectionComparer
class (not for nothing, we implemented the
IEqualityComparer<object>
interface
IEqualityComparer<object>
!). Therefore, to compare collections, select LINQ:
private static MethodInfo GenericSequenceEqualWithoutComparer = typeof(Enumerable) .GetMethods(BindingFlags.Public | BindingFlags.Static) .First(m => m.Name == "SequenceEqual" && m.GetParameters().Length == 2); private static MethodInfo GenericSequenceEqualWithComparer = typeof(Enumerable) .GetMethods(BindingFlags.Public | BindingFlags.Static) .First(m => m.Name == "SequenceEqual" && m.GetParameters().Length == 3); private bool CompareEnumerables(Type collectionType, object x, object y) { Type enumerableInterface = collectionType.GetIEnumerableInterface(); Type elementType = enumerableInterface.GetGenericArguments()[0]; MethodInfo sequenceEqual; object[] arguments; if (elementType.IsPrimitive) { sequenceEqual = GenericSequenceEqualWithoutComparer; arguments = new[] {x, y}; } else { sequenceEqual = GenericSequenceEqualWithComparer; arguments = new[] {x, y, this}; } var sequenceEqualMethod = sequenceEqual.MakeGenericMethod(elementType); return (bool)sequenceEqualMethod.Invoke(null, arguments); }
The last case - comparison of arrays - is in many ways similar to the comparison of collections. The difference is that instead of using the standard LINQ method, we use the samopisny generalized method. The implementation of array comparison is presented below:
Comparing arrays private static MethodInfo GenericCompareArraysMethod = typeof(ReflectionComparer).GetMethod("GenericCompareArrays", BindingFlags.NonPublic | BindingFlags.Static); private static bool GenericCompareArrays<T>(T[] x, T[] y, IEqualityComparer<T> comparer) { var comp = comparer ?? EqualityComparer<T>.Default; for (int i = 0; i < x.Length; ++i) { if (!comp.Equals(x[i], y[i])) return false; } return true; } private bool CompareArrays(Type type, object x, object y) { var elementType = type.GetElementType(); int xLength, yLength; if (elementType.IsValueType) { // - object, Array xLength = ((Array) x).Length; yLength = ((Array) y).Length; } else { xLength = ((object[]) x).Length; yLength = ((object[]) y).Length; } if (xLength != yLength) return false; var compareArraysPrimitive = GenericCompareArraysMethod.MakeGenericMethod(elementType); var arguments = elementType.IsPrimitive ? new[] {x, y, null} : new[] {x, y, this}; return (bool) compareArraysPrimitive.Invoke(null, arguments); }
So, all the pieces of the puzzle are assembled - our reflective comparer is ready. Below are the results of a comparison of the performance of a reflexive comparer compared with the manual implementation on “test” data. Obviously, the execution time of the comparison
is very dependent on the type of objects being compared. As an example, two types of objects were compared here - conditionally “more complicated” and “simpler”. To estimate the runtime, the
BenchmarkDotNet library was used, for which I want to express special thanks to Andrey Akinshin
DreamWalker .
Code here public struct Struct { private int m_a; private double m_b; private string m_c; public int A => m_a; public double B => m_b; public string C => m_c; public Struct(int a, double b, string c) { m_a = a; m_b = b; m_c = c; } } public class SimpleClass { public int A { get; set; } public Struct B { get; set; } } public class ComplexClass { public int A { get; set; } public IntPtr B { get; set; } public UIntPtr C { get; set; } public string D { get; set; } public SimpleClass E { get; set; } public int? F { get; set; } public int[] G { get; set; } public List<int> H { get; set; } public double I { get; set; } public float J { get; set; } } [BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)] public class ComparisonTest { private static int[] MakeArray(int count) { var array = new int[count]; for (int i = 0; i < array.Length; ++i) array[i] = i; return array; } private static List<int> MakeList(int count) { var list = new List<int>(count); for (int i = 0; i < list.Count; ++i) list.Add(i); return list; } private ComplexClass x = new ComplexClass { A = 2, B = new IntPtr(2), C = new UIntPtr(2), D = "abc", E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }, F = 1, G = MakeArray(100), H = MakeList(100), I = double.MaxValue, J = float.MaxValue }; private ComplexClass y = new ComplexClass { A = 2, B = new IntPtr(2), C = new UIntPtr(2), D = "abc", E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }, F = 1, G = MakeArray(100), H = MakeList(100), I = double.MaxValue, J = float.MaxValue }; private ReflectionComparer comparer = new ReflectionComparer(); [Benchmark] public void ReflectionCompare() { var _ = comparer.Equals(x, y); } [Benchmark] public void ManualCompare() { var _ = CompareComplexObjects(); } private bool CompareComplexObjects() { if (x == y) return true; if (xA != yA) return false; if (xB != yB) return false; if (xC != yC) return false; if (xD != yD) return false; if (xE != yE) { if (xEA != yEA) return false; var s1 = xEB; var s2 = yEB; if (s1.A != s2.A) return false; if (!s1.B.Equals(s2.B)) return false; if (s1.C != s2.C) return false; } if (xF != yF) return false; if (xG != yG) { if (xG?.Length != yG?.Length) return false; int[] a = xG, b = yG; for (int i = 0; i < a.Length; ++i) { if (a[i] != b[i]) return false; } } if (xH != yH) { if (!xHSequenceEqual(yH)) return false; } if (!xIEquals(yI)) return false; if (!xJEquals(yJ)) return false; return true; } } [BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)] public class SimpleComparisonTest { private SimpleClass x = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }; private SimpleClass y = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }; private ReflectionComparer comparer = new ReflectionComparer(); [Benchmark] public void ReflectionCompare() { var _ = comparer.Equals(x, y); } [Benchmark] public void ManualCompare() { var _ = CompareSimpleObjects(); } private bool CompareSimpleObjects() { if (x == y) return true; if (xA != yA) return false; var s1 = xB; var s2 = yB; if (s1.A != s2.A) return false; if (!s1.B.Equals(s2.B)) return false; if (s1.C != s2.C) return false; return true; } }
Results hereBenchmarkDotNet = v0.7.8.0
OS = Microsoft Windows NT 6.2.9200.0
Processor = Intel® Core (TM) i5-2410M CPU @ 2.30GHz, ProcessorCount = 4
HostCLR = MS .NET 4.0.30319.42000, Arch = 64-bit [RyuJIT]
Results of comparison of ComplexClass objectsMethod | Platform | Jit | Avrtime | Stddev | op / s |
---|
ManualCompare | X64 | Legacyjit | 1,364.3835 ns | 47.6975 ns | 732,941.68 |
ReflectionCompare | X64 | Legacyjit | 36,779.9097 ns | 3,080.9738 ns | 27,188.92 |
ManualCompare | X64 | Ryujit | 930.8761 ns | 43.6018 ns | 1,074,294.12 |
ReflectionCompare | X64 | Ryujit | 36,909.7334 ns | 3,762.0698 ns | 27,093.98 |
ManualCompare | X86 | Legacyjit | 936.3367 ns | 38.3831 ns | 1,067,992.54 |
ReflectionCompare | X86 | Legacyjit | 32,446.6969 ns | 1,687.8442 ns | 30,819.81 |
SimpleClass object comparison resultsMethod | Platform | Jit | Avrtime | Stddev | op / s |
---|
Handwritten | X64 | Legacyjit | 131.5205 ns | 4.9045 ns | 7,603,376.64 |
ReflectionComparer | X64 | Legacyjit | 3,859.7102 ns | 269.8845 ns | 259,087.15 |
Handwritten | X64 | Ryujit | 61.2438 ns | 1.9025 ns | 16,328,222.24 |
ReflectionComparer | X64 | Ryujit | 3,841.4645 ns | 374.0006 ns | 260,317.46 |
Handwritten | X86 | Legacyjit | 71.5982 ns | 5.4304 ns | 13,966,823.95 |
ReflectionComparer | X86 | Legacyjit | 3,636.7963 ns | 241.3940 ns | 274,967.76 |
Naturally, the performance of the reflexive comparer is lower than the self-written one. Reflection is slow and its API works with
object
, which immediately affects performance, if the compared objects contain value types, because of the need to perform boxing / unboxing. And here it is necessary to proceed from their own needs. If reflection is not used often, then you can live with it. But in my particular case, the reflexive comparer was not fast enough. “Come on new
, Mish ,” I said to myself and began the second option.
Part 2. The emit game

You can live like this, but better speed up
Group Leningrad, "I would be in heaven"
Neo: And you read it?
Cypher: It comes. Over time you get used to it. I don't even notice the code. I see a blonde, brunette, redhead.
The film "The Matrix" (The Matrix)
Reflection allows us to extract all the necessary information about the types of objects being compared. But we cannot use these types directly to get the properties we need or call the required method, which leads us to use the slow reflection API (PropertyInfo.GetValue, MethodInfo.Invoke, etc.). And what if we could, using type information, once generate the code to compare objects and call it every time, without resorting to reflection anymore? And, fortunately, we can do it! The namespace
System.Reflection.Emit provides us with the means to create dynamic methods -
DynamicMethod using the
IL -
ILGenerator generator . The same approach is used, for example, to compile regular expressions in the .NET Framework itself (you can see the implementation
here ).
About generation IL on Habré already
wrote . Therefore, only briefly recall the features of IL.
Intermediate Language (IL) is an object-oriented assembly language used by the .NET and Mono platforms. High-level languages, such as C #, VB.NET, F #, are compiled into IL, and IL, in turn, is compiled by JIT into machine code. Because of its intermediate position in this chain, language has its name. IL uses a stack-based computational model, that is, all input and output data is transmitted through the stack, and not through registers, as in many processor architectures. IL supports loading and saving local variables and arguments, type conversion, creating and manipulating objects, passing control, calling methods, exceptions, and many others.
For example, the increment of an integer variable on IL is written as follows:
ldloc.0 // ldc.i4.1 // add // stloc.0 //
You can view the result of the C # complation in IL by various means, for example, ILDasm (the utility that comes with Visual Studio) or
ILSpy . But my favorite way is through the great
Try Roslyn web application, written by Andrey Shchekin
ashmind .
Let's return to our task. We will again do the implementation of the
IEqualityComparer<object>
interface:
public class DynamicCodeComparer : IEqualityComparer<object> { // private delegate bool Comparer(object x, object y); // private static Dictionary<Type, Comparer> ComparerCache = new Dictionary<Type, Comparer>(); public new bool Equals(object x, object y) { // if (ReferenceEquals(x, y)) return true; // null if (ReferenceEquals(x, null) != ReferenceEquals(y, null)) return false; Type xType = x.GetType(); // if (xType != y.GetType()) return false; // // . , // Comparer comparer; if (!ComparerCache.TryGetValue(xType, out comparer)) { ComparerCache[xType] = comparer = new ComparerDelegateGenerator().Generate(xType); } return comparer(x, y); } public int GetHashCode(object obj) { return obj.GetHashCode(); } }
The
Equals
method uses a dictionary to maintain a cache of generated delegates — comparers that perform object comparisons. If the comparer for a particular type is not yet in the dictionary, then we will generate a new delegate. All logic for dynamic delegate generation will be concentrated in the
ComparerDelegateGenerator
class.
class ComparerDelegateGenerator { // private ILGenerator il; public Comparer Generate(Type type) { // , var dynamicMethod = new DynamicMethod("__DynamicCompare", typeof(bool), new[] { typeof(object), typeof(object) }, type.Module); il = dynamicMethod.GetILGenerator(); // // // il.LoadFirstArg(); var arg0 = il.CastToType(type); il.LoadSecondArg(); var arg1 = il.CastToType(type); // CompareObjectsInternal(type, arg0, arg1); // , il.ReturnTrue(); // return (Comparer)dynamicMethod.CreateDelegate(typeof(Comparer)); } }
In the
Generate
method above there is one small nuance. The nuance is to create a dynamic method in the same module as the type of objects being compared. Otherwise, the dynamic method code will not be able to access the type and will receive a
TypeAccessException
exception. The
ILGenerator
class allows
ILGenerator
to generate instructions using the
Emit (OpCode) method, which takes a command code as an argument. But, in order not to litter our class with such details, we will use extension methods, from whose names it will be clear what they are doing. The code for the Extension methods
LoadFirstArg, LoadSecondArg, CastToType
and
ReturnTrue
is presented below. It should be clarified that the
Generate
method
CompareObjectsInternal
will generate
return false
as soon as it encounters different values. Therefore, the last dynamic method
return true
will
return true
to handle the situation when objects are equal.
Extension method code // public static void LoadFirstArg(this ILGenerator il) => il.Emit(OpCodes.Ldarg_0); // public static void LoadSecondArg(this ILGenerator il) => il.Emit(OpCodes.Ldarg_1); // public static LocalBuilder CastToType(this ILGenerator il, Type type) { var x = il.DeclareLocal(type); // - if (type.IsValueType || type.IsPrimitive) { il.Emit(OpCodes.Unbox_Any, type); } // else { il.Emit(OpCodes.Castclass, type); } il.SetLocal(x); return x; } // ( false) public static void LoadZero(this ILGenerator il) => il.Emit(OpCodes.Ldc_I4_0); // ( true) public static void LoadOne(this ILGenerator il) => il.Emit(OpCodes.Ldc_I4_1); // false public static void ReturnFalse(this ILGenerator il) { il.LoadZero(); il.Emit(OpCodes.Ret); } // true public static void ReturnTrue(this ILGenerator il) { il.LoadOne(); il.Emit(OpCodes.Ret); }
Next, consider the
CompareObjectsInternal
method, which will generate different comparison code depending on the type of objects:
private void CompareObjectsInternal(Type type, LocalBuilder x, LocalBuilder y) { // , , var whenEqual = il.DefineLabel(); // - if (!type.IsValueType) { // true, JumpIfReferenceEquals(x, y, whenEqual); // null, false ReturnFalseIfOneIsNull(x, y); // if (type.IsArray) { CompareArrays(type.GetElementType(), x, y); } // else if (type.IsClass || type.IsInterface) { // if (Type.GetTypeCode(type) == TypeCode.String) { CompareStrings(x, y, whenEqual); } // else if (type.IsImplementIEnumerable()) { CompareEnumerables(type, x, y); } // else { CompareAllProperties(type, x, y); } } } // else if (type.IsNullable()) { CompareNullableValues(type, x, y, whenEqual); } // else if (type.IsPrimitive || type.IsEnum) { ComparePrimitives(type, x, y, whenEqual); } // else { CompareAllProperties(type, x, y); } // , , il.MarkLabel(whenEqual); }
As can be seen from the code, we handle the same situations as in the reflexive comparator, but in a slightly different order. ,
null
, .
HasValue
.
, . , , .
private void CompareArrays(Type elementType, LocalBuilder x, LocalBuilder y) { var loop = il.DefineLabel(); // il.LoadArrayLength(x); // il.LoadArrayLength(y); // il.JumpWhenEqual(loop); // , il.ReturnFalse(); // false il.MarkLabel(loop); // var index = il.DeclareLocal(typeof(int)); // - var loopCondition = il.DefineLabel(); // var loopBody = il.DefineLabel(); // il.LoadZero(); // il.SetLocal(index); // il.Jump(loopCondition); // il.MarkLabel(loopBody); // { var xElement = il.GetArrayElement(elementType, x, index); // var yElement = il.GetArrayElement(elementType, y, index); // CompareObjectsInternal(elementType, xElement, yElement); // il.Increment(index); // } il.MarkLabel(loopCondition); // { il.LoadLocal(index); // il.LoadArrayLength(x); // il.JumpWhenLess(loopBody); // , } }
,
, . .
CompareArrays
, . , IL , , : , . (
Jump, JumpWhenLess
)
loopBody, loopCondition
.
// public static void LoadLocal(this ILGenerator il, LocalBuilder x) => il.Emit(OpCodes.Ldloc, x); // public static void SetLocal(this ILGenerator il, LocalBuilder x) => il.Emit(OpCodes.Stloc, x); // public static void LoadArrayLength(this ILGenerator il, LocalBuilder array) { il.LoadLocal(array); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); } // , public static void LoadArrayElement(this ILGenerator il, Type type) { if (type.IsEnum) { type = Enum.GetUnderlyingType(type); } if (type.IsPrimitive) { if (type == typeof (IntPtr) || type == typeof (UIntPtr)) { il.Emit(OpCodes.Ldelem_I); } else { OpCode opCode; switch (Type.GetTypeCode(type)) { case TypeCode.Boolean: case TypeCode.Int32: opCode = OpCodes.Ldelem_I4; break; case TypeCode.Char: case TypeCode.UInt16: opCode = OpCodes.Ldelem_U2; break; case TypeCode.SByte: opCode = OpCodes.Ldelem_I1; break; case TypeCode.Byte: opCode = OpCodes.Ldelem_U1; break; case TypeCode.Int16: opCode = OpCodes.Ldelem_I2; break; case TypeCode.UInt32: opCode = OpCodes.Ldelem_U4; break; case TypeCode.Int64: case TypeCode.UInt64: opCode = OpCodes.Ldelem_I8; break; case TypeCode.Single: opCode = OpCodes.Ldelem_R4; break; case TypeCode.Double: opCode = OpCodes.Ldelem_R8; break; default: throw new ArgumentOutOfRangeException(); } il.Emit(opCode); } } else if (type.IsValueType) { il.Emit(OpCodes.Ldelema, type); } else { il.Emit(OpCodes.Ldelem_Ref); } } // , public static LocalBuilder GetArrayElement(this ILGenerator il, Type elementType, LocalBuilder array, LocalBuilder index) { var x = il.DeclareLocal(elementType); il.LoadLocal(array); il.LoadLocal(index); il.LoadArrayElement(elementType); il.SetLocal(x); return x; } // public static void Increment(this ILGenerator il, LocalBuilder x) { il.LoadLocal(x); il.LoadOne(); il.Emit(OpCodes.Add); il.SetLocal(x); }
DynamicCodeComparer
SimpleClass
, :
- IL .method public static bool __DynamicCompare ( object '', object '' ) cil managed { // Method begins at RVA 0x2050 // Code size 215 (0xd7) .maxstack 15 .locals init ( [0] class SimpleClass, [1] class SimpleClass, [2] int32, [3] int32, [4] valuetype Struct, [5] valuetype Struct, [6] int32, [7] int32, [8] float64, [9] float64, [10] string, [11] string ) IL_0000: ldarg.0 IL_0001: castclass SimpleClass IL_0006: stloc.0 IL_0007: ldarg.1 IL_0008: castclass SimpleClass IL_000d: stloc.1 IL_000e: ldloc.0 IL_000f: ldloc.1 IL_0010: beq IL_00d5 IL_0015: ldloc.0 IL_0016: ldnull IL_0017: ceq IL_0019: ldloc.1 IL_001a: ldnull IL_001b: ceq IL_001d: beq IL_0024 IL_0022: ldc.i4.0 IL_0023: ret IL_0024: ldloc.0 IL_0025: callvirt instance int32 SimpleClass::get_A() IL_002a: stloc.2 IL_002b: ldloc.1 IL_002c: callvirt instance int32 SimpleClass::get_A() IL_0031: stloc.3 IL_0032: ldloc.2 IL_0033: ldloc.3 IL_0034: beq IL_003b IL_0039: ldc.i4.0 IL_003a: ret IL_003b: ldloc.0 IL_003c: callvirt instance valuetype Struct SimpleClass::get_B() IL_0041: stloc.s 4 IL_0043: ldloc.1 IL_0044: callvirt instance valuetype Struct SimpleClass::get_B() IL_0049: stloc.s 5 IL_004b: ldloca.s 4 IL_004d: call instance int32 Struct::get_A() IL_0052: stloc.s 6 IL_0054: ldloca.s 5 IL_0056: call instance int32 Struct::get_A() IL_005b: stloc.s 7 IL_005d: ldloc.s 6 IL_005f: ldloc.s 7 IL_0061: beq IL_0068 IL_0066: ldc.i4.0 IL_0067: ret IL_0068: ldloca.s 4 IL_006a: call instance float64 Struct::get_B() IL_006f: stloc.s 8 IL_0071: ldloca.s 5 IL_0073: call instance float64 Struct::get_B() IL_0078: stloc.s 9 IL_007a: ldloc.s 8 IL_007c: call bool [mscorlib]System.Double::IsNaN(float64) IL_0081: ldloc.s 9 IL_0083: call bool [mscorlib]System.Double::IsNaN(float64) IL_0088: and IL_0089: brtrue IL_0099 IL_008e: ldloc.s 8 IL_0090: ldloc.s 9 IL_0092: beq IL_0099 IL_0097: ldc.i4.0 IL_0098: ret IL_0099: ldloca.s 4 IL_009b: call instance string Struct::get_C() IL_00a0: stloc.s 10 IL_00a2: ldloca.s 5 IL_00a4: call instance string Struct::get_C() IL_00a9: stloc.s 11 IL_00ab: ldloc.s 10 IL_00ad: ldloc.s 11 IL_00af: beq IL_00d5 IL_00b4: ldloc.s 10 IL_00b6: ldnull IL_00b7: ceq IL_00b9: ldloc.s 11 IL_00bb: ldnull IL_00bc: ceq IL_00be: beq IL_00c5 IL_00c3: ldc.i4.0 IL_00c4: ret IL_00c5: ldloc.s 10 IL_00c7: ldloc.s 11 IL_00c9: call instance bool [mscorlib]System.String::Equals(string) IL_00ce: brtrue IL_00d5 IL_00d3: ldc.i4.0 IL_00d4: ret IL_00d5: ldc.i4.1 IL_00d6: ret } // end of method Test::__DynamicCompare
- C# ( ILSpy) public static bool __DynamicCompare(object obj, object obj2) { SimpleClass simpleClass = (SimpleClass)obj; SimpleClass simpleClass2 = (SimpleClass)obj2; if (simpleClass != simpleClass2) { if (simpleClass == null != (simpleClass2 == null)) { return false; } int a = simpleClass.A; int a2 = simpleClass2.A; if (a != a2) { return false; } Struct b = simpleClass.B; Struct b2 = simpleClass2.B; int a3 = b.get_A(); int a4 = b2.get_A(); if (a3 != a4) { return false; } double b3 = b.get_B(); double b4 = b2.get_B(); if (!(double.IsNaN(b3) & double.IsNaN(b4)) && b3 != b4) { return false; } string c = b.get_C(); string c2 = b2.get_C(); if (c != c2) { if (c == null != (c2 == null)) { return false; } if (!c.Equals(c2)) { return false; } } } return true; }
, , . , , , , .
results
DynamicCodeComparer, ReflectionComparer
, . IL .
public struct Struct { private int m_a; private double m_b; private string m_c; public int A => m_a; public double B => m_b; public string C => m_c; public Struct(int a, double b, string c) { m_a = a; m_b = b; m_c = c; } } public class SimpleClass { public int A { get; set; } public Struct B { get; set; } } public class ComplexClass { public int A { get; set; } public IntPtr B { get; set; } public UIntPtr C { get; set; } public string D { get; set; } public SimpleClass E { get; set; } public int? F { get; set; } public int[] G { get; set; } public List<int> H { get; set; } public double I { get; set; } public float J { get; set; } } [BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)] public class ComplexComparisonTest { private static int[] MakeArray(int count) { var array = new int[count]; for (int i = 0; i < array.Length; ++i) array[i] = i; return array; } private static List<int> MakeList(int count) { var list = new List<int>(count); for (int i = 0; i < list.Count; ++i) list.Add(i); return list; } private ComplexClass x = new ComplexClass { A = 2, B = new IntPtr(2), C = new UIntPtr(2), D = "Habrahabr!", E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }, F = 1, G = MakeArray(100), H = MakeList(100), I = double.MaxValue, J = float.MaxValue }; private ComplexClass y = new ComplexClass { A = 2, B = new IntPtr(2), C = new UIntPtr(2), D = "Habrahabr!", E = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }, F = 1, G = MakeArray(100), H = MakeList(100), I = double.MaxValue, J = float.MaxValue }; private ReflectionComparer reflectionComparer = new ReflectionComparer(); private DynamicCodeComparer dynamicCodeComparer = new DynamicCodeComparer(); [Benchmark] public void ReflectionCompare() { var _ = reflectionComparer.Equals(x, y); } [Benchmark] public void DynamicCodeCompare() { var _ = dynamicCodeComparer.Equals(x, y); } [Benchmark] public void ManualCompare() { var _ = CompareComplexObjects(); } private bool CompareComplexObjects() { if (x == y) return true; if (xA != yA) return false; if (xB != yB) return false; if (xC != yC) return false; if (xD != yD) return false; if (xE != yE) { if (xEA != yEA) return false; var s1 = xEB; var s2 = yEB; if (s1.A != s2.A) return false; if (!s1.B.Equals(s2.B)) return false; if (s1.C != s2.C) return false; } if (xF != yF) return false; if (xG != yG) { if (xG?.Length != yG?.Length) return false; int[] a = xG, b = yG; for (int i = 0; i < a.Length; ++i) { if (a[i] != b[i]) return false; } } if (xH != yH) { if (!xHSequenceEqual(yH)) return false; } if (!xIEquals(yI)) return false; if (!xJEquals(yJ)) return false; return true; } } [BenchmarkTask(platform: BenchmarkPlatform.X86, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.LegacyJit)] [BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit)] public class SimpleComparisonTest { private SimpleClass x = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }; private SimpleClass y = new SimpleClass { A = 42, B = new Struct(42, 3.14, "meow") }; private ReflectionComparer reflectionComparer = new ReflectionComparer(); private DynamicCodeComparer dynamicCodeComparer = new DynamicCodeComparer(); [Benchmark] public void ReflectionCompare() { var _ = reflectionComparer.Equals(x, y); } [Benchmark] public void DynamicCodeCompare() { var _ = dynamicCodeComparer.Equals(x, y); } [Benchmark] public void ManualCompare() { var _ = CompareSimpleObjects(); } private bool CompareSimpleObjects() { if (x == y) return true; if (xA != yA) return false; var s1 = xB; var s2 = yB; if (s1.A != s2.A) return false; if (!s1.B.Equals(s2.B)) return false; if (s1.C != s2.C) return false; return true; } }
ComplexClassMethod | Platform | Jit | AvrTime | StdDev | op/s |
---|
DynamicCodeComparer | X64 | LegacyJit | 1,104.7155 ns | 32.9474 ns | 905,210.51 |
Handwritten | X64 | LegacyJit | 1,360.3273 ns | 39.9703 ns | 735,117.32 |
ReflectionComparer | X64 | LegacyJit | 38,043.3600 ns | 2,261.3159 ns | 26,290.11 |
DynamicCodeComparer | X64 | RyuJit | 834.8742 ns | 58.1986 ns | 1,197,785.93 |
Handwritten | X64 | RyuJit | 968.3789 ns | 33.1622 ns | 1,032,653.82 |
ReflectionComparer | X64 | RyuJit | 37,751.3104 ns | 1,763.3172 ns | 26,489.20 |
DynamicCodeComparer | X86 | LegacyJit | 776.0265 ns | 22.8038 ns | 1,288,615.79 |
Handwritten | X86 | LegacyJit | 915.5713 ns | 26.0536 ns | 1,092,214.32 |
ReflectionComparer | X86 | LegacyJit | 32,382.2746 ns | 1,748.4016 ns | 30,881.10 |
SimpleClassMethod | Platform | Jit | AvrTime | StdDev | op/s |
---|
DynamicCodeComparer | X64 | LegacyJit | 215.7626 ns | 8.2063 ns | 4,634,725.08 |
Handwritten | X64 | LegacyJit | 160.4945 ns | 6.8949 ns | 6,230,741.94 |
ReflectionComparer | X64 | LegacyJit | 6,654.3290 ns | 380.7790 ns | 150,278.15 |
DynamicCodeComparer | X64 | RyuJit | 168.4194 ns | 9.4654 ns | 5,937,569.56 |
Handwritten | X64 | RyuJit | 87.8513 ns | 3.3118 ns | 11,382,874.20 |
ReflectionComparer | X64 | RyuJit | 6,954.6437 ns | 387.1803 ns | 143,789.85 |
DynamicCodeComparer | X86 | LegacyJit | 180.4105 ns | 6.5036 ns | 5,542,914.59 |
Handwritten | X86 | LegacyJit | 93.0846 ns | 4.0584 ns | 10,742,923.17 |
ReflectionComparer | X86 | LegacyJit | 6,431.5783 ns | 314.5633 ns | 155,483.09 |
Conclusion
«No silver bullet» (accidental complexity) (essential complexity), . , - , - . , . ,
IEquatable<T>
. , - .
, . Have a nice programming!
: