views:

1008

answers:

2

I just came across something pretty weird to me : when you use the Equals() method on a value type (and if this method has not been overriden, of course) you get something very very slow -- fields are compared one to one using reflection ! As in :

public struct MyStruct{
   int i;
}

   (...)

   MyStruct s, t;
   s.i = 0;
   t.i = 1;
   if ( s.Equals( t ))   <= s.i will be compared to t.i via reflection here.
      (...)

My question : why does the C# compiler do not generate a simple method to compare value types ? Something like (in MyStruct's definition) :

   public override bool Equals( Object o ){
      if ( this.i == o.i )
         return true;
      else
         return false;
   }

The compiler knows what are the fields of MyStruct at compile time, why does it wait until runtime to enumerate MyStruct fields ?

Very strange to me.

Thanks :)

ADDED : Sorry, I just realize that, of course, Equals is not a language keyword but a runtime method... The compiler is completely unaware of this method. So it make sens here to use reflection.

+4  A: 

It doesn't use reflection when it doesn't need to. It just compare values bit by bit in case the struct if it can do so. However, there if any of the struct members (or members of members, any descendants) override object.Equals and provide its own implementation, obviously, it can't rely on bit-by-bit comparison to calculate the return value.

The reason it's slow is that the parameter to Equals is of type object and value types have to be boxed to be treated as an object. Boxing involves allocating memory on the heap and memory copying the value type to that location.

You could manually provide an overload for the Equals method that takes your own struct as parameter to prevent boxing:

public bool Equals(MyStruct obj) {
     return obj.i == i;
}
Mehrdad Afshari
It uses reflection in some cases. If it detects it can just blit the results, it does so - but if there are reference types (or types containing reference types) in the fields, it has to do a more painful process.
Jon Skeet
While I was writing this, I read around and I found that some .Net frameworks authors (Cwalina, Abrams) just confirm that Equals is using reflection on value types. But maybe just in Framework 2.0 ?
Sylvain
Sylvain: They are right. As Jon said, if the struct contains reference types as members, it has to call Equals on that fields. I updated the answer to reflect that. The point I was trying to make is that it doesn't use reflection when it doesn't need so (like your example).
Mehrdad Afshari
I have to say, I don't understand why a bit-wise comparison can't be made when there are references. If two references point to the same object, when would the pointers not be exactly equal?
Snarfblam
Snarfblam - I'm still wondering about the same thing.
Sylvain
@Snarfblam: From the code Mehrdad posted, it seems as if it calls Equals on each of the referenced objects contained within the struct - almost recursively. If two objects are equal but their references aren't, a bit-wise comparison will fail.
Samir Talwar
+3  A: 

The following is the decompiled ValueType.Equals method from mscorlib:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    RuntimeType type = (RuntimeType) base.GetType();
    RuntimeType type2 = (RuntimeType) obj.GetType();
    if (type2 != type)
    {
        return false;
    }
    object a = this;
    if (CanCompareBits(this))
    {
        return FastEqualsCheck(a, obj);
    }
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    for (int i = 0; i < fields.Length; i++)
    {
        object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false);
        object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false);
        if (obj3 == null)
        {
            if (obj4 != null)
            {
                return false;
            }
        }
        else if (!obj3.Equals(obj4))
        {
            return false;
        }
    }
    return true;
}

When possible, a bit-wise comparison will be done (note the CanCompareBits and FastEqualsCheck, both of which are defined as InternalCall. The JIT would presumably inject the appropriate code here. As to why it is so slow, I couldn't tell you.

Snarfblam
Just test this. You are right. :)
Sylvain