Here is the example with comments:
class Program
{
// first version of structure
public struct D1
{
public double d;
public int f
It must be related to a bit by bit comparison, since 0.0
should differ from -0.0
only by the signal bit.
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
Simpler test case:
Console.WriteLine("Good: " + new Good().Equals(new Good { d = -.0 }));
Console.WriteLine("Bad: " + new Bad().Equals(new Bad { d = -.0 }));
public struct Good {
public double d;
public int f;
}
public struct Bad {
public double d;
}
EDIT: The bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
If you make D2 like this
public struct D2
{
public double d;
public double f;
public string s;
}
it's true.
if you make it like this
public struct D2
{
public double d;
public double f;
public double u;
}
It's still false.
it seems like it's false if the struct only holds doubles.
It must be zero related, since changing the line
d.d = -0.0
to:
d.d = 0.0
results in the comparison being true...
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.