The bug is in the following two lines of System.ValueType
: (I stepped into the reference source)
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
(Both methods are [MethodImpl(MethodImplOptions.InternalCall)]
)
When all of the fields are 8 bytes wide, CanCompareBits
mistakenly returns true, resulting in a bitwise comparison of two different, but semantically identical, values.
When at least one field is not 8 bytes wide, CanCompareBits
returns false, and the code proceeds to use reflection to loop over the fields and call Equals
for each value, which correctly treats -0.0
as equal to 0.0
.
Here is the source for CanCompareBits
from SSCLI:
FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj != NULL);
MethodTable* mt = obj->GetMethodTable();
FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND
I found the answer at http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx.
The core piece is the source comment on CanCompareBits
, which ValueType.Equals
uses to determine whether to use memcmp
-style comparison:
The comment of CanCompareBits says "Return true if the valuetype does not contain pointer and is tightly packed". And FastEqualsCheck use "memcmp" to speed up the comparison.
The author goes on to state exactly the problem described by the OP:
Imagine you have a structure which only contains a float. What will occur if one contains +0.0, and the other contains -0.0? They should be the same, but the underlying binary representation are different. If you nest other structure which override the Equals method, that optimization will also fail.
Vilx's conjecture is correct. What "CanCompareBits" does is checks to see whether the value type in question is "tightly packed" in memory. A tightly packed struct is compared by simply comparing the binary bits that make up the structure; a loosely packed structure is compared by calling Equals on all the members.
This explains SLaks' observation that it repros with structs that are all doubles; such structs are always tightly packed.
Unfortunately as we've seen here, that introduces a semantic difference because bitwise comparison of doubles and Equals comparison of doubles gives different results.
Half an answer:
Reflector tells us that ValueType.Equals()
does something like this:
if (CanCompareBits(this))
return FastEqualsCheck(this, obj);
else
// Use reflection to step through each member and call .Equals() on each one.
Unfortunately both CanCompareBits()
and FastEquals()
(both static methods) are extern ([MethodImpl(MethodImplOptions.InternalCall)]
) and have no source available.
Back to guessing why one case can be compared by bits, and the other cannot (alignment issues maybe?)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With