📜 ⬆️ ⬇️

Compare me completely. Reflection in the service of .NET developer



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:


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 here
BenchmarkDotNet = 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 objects
MethodPlatformJitAvrtimeStddevop / s
ManualCompareX64Legacyjit1,364.3835 ns47.6975 ns732,941.68
ReflectionCompareX64Legacyjit36,779.9097 ns3,080.9738 ns27,188.92
ManualCompareX64Ryujit930.8761 ns43.6018 ns1,074,294.12
ReflectionCompareX64Ryujit36,909.7334 ns3,762.0698 ns27,093.98
ManualCompareX86Legacyjit936.3367 ns38.3831 ns1,067,992.54
ReflectionCompareX86Legacyjit32,446.6969 ns1,687.8442 ns30,819.81

SimpleClass object comparison results
MethodPlatformJitAvrtimeStddevop / s
HandwrittenX64Legacyjit131.5205 ns4.9045 ns7,603,376.64
ReflectionComparerX64Legacyjit3,859.7102 ns269.8845 ns259,087.15
HandwrittenX64Ryujit61.2438 ns1.9025 ns16,328,222.24
ReflectionComparerX64Ryujit3,841.4645 ns374.0006 ns260,317.46
HandwrittenX86Legacyjit71.5982 ns5.4304 ns13,966,823.95
ReflectionComparerX86Legacyjit3,636.7963 ns241.3940 ns274,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; } } 

ComplexClass
MethodPlatformJitAvrTimeStdDevop/s
DynamicCodeComparerX64LegacyJit1,104.7155 ns32.9474 ns905,210.51
HandwrittenX64LegacyJit1,360.3273 ns39.9703 ns735,117.32
ReflectionComparerX64LegacyJit38,043.3600 ns2,261.3159 ns26,290.11
DynamicCodeComparerX64RyuJit834.8742 ns58.1986 ns1,197,785.93
HandwrittenX64RyuJit968.3789 ns33.1622 ns1,032,653.82
ReflectionComparerX64RyuJit37,751.3104 ns1,763.3172 ns26,489.20
DynamicCodeComparerX86LegacyJit776.0265 ns22.8038 ns1,288,615.79
HandwrittenX86LegacyJit915.5713 ns26.0536 ns1,092,214.32
ReflectionComparerX86LegacyJit32,382.2746 ns1,748.4016 ns30,881.10

SimpleClass
MethodPlatformJitAvrTimeStdDevop/s
DynamicCodeComparerX64LegacyJit215.7626 ns8.2063 ns4,634,725.08
HandwrittenX64LegacyJit160.4945 ns6.8949 ns6,230,741.94
ReflectionComparerX64LegacyJit6,654.3290 ns380.7790 ns150,278.15
DynamicCodeComparerX64RyuJit168.4194 ns9.4654 ns5,937,569.56
HandwrittenX64RyuJit87.8513 ns3.3118 ns11,382,874.20
ReflectionComparerX64RyuJit6,954.6437 ns387.1803 ns143,789.85
DynamicCodeComparerX86LegacyJit180.4105 ns6.5036 ns5,542,914.59
HandwrittenX86LegacyJit93.0846 ns4.0584 ns10,742,923.17
ReflectionComparerX86LegacyJit6,431.5783 ns314.5633 ns155,483.09

Conclusion

«No silver bullet» (accidental complexity) (essential complexity), . , - , - . , . , IEquatable<T> . , - .

, . Have a nice programming!

:

Source: https://habr.com/ru/post/269699/


All Articles