Can anyone explain this strange behavior with signed floats in C#?
C#.NetFloating PointC# Problem Overview
Here is the example with comments:
class Program
{
// first version of structure
public struct D1
{
public double d;
public int f;
}
// during some changes in code then we got D2 from D1
// Field f type became double while it was int before
public struct D2
{
public double d;
public double f;
}
static void Main(string[] args)
{
// Scenario with the first version
D1 a = new D1();
D1 b = new D1();
a.f = b.f = 1;
a.d = 0.0;
b.d = -0.0;
bool r1 = a.Equals(b); // gives true, all is ok
// The same scenario with the new one
D2 c = new D2();
D2 d = new D2();
c.f = d.f = 1;
c.d = 0.0;
d.d = -0.0;
bool r2 = c.Equals(d); // false! this is not the expected result
}
}
So, what do you think about this?
C# Solutions
Solution 1 - C#
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
Solution 2 - C#
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.
Solution 3 - C#
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.
Solution 4 - C#
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?)
Solution 5 - C#
It does give true for me, with Mono's gmcs 2.4.2.3.
Solution 6 - C#
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.
Solution 7 - C#
It must be related to a bit by bit comparison, since 0.0
should differ from -0.0
only by the signal bit.
Solution 8 - C#
> …what do you think about this?
Always override Equals and GetHashCode on value types. It will be fast and correct.
Solution 9 - C#
Just an update for this 10 years old bug: it has been fixed (Disclaimer: I'm the author of this PR) in .NET Core which would be probably released in .NET Core 2.1.0.
The blog post explained the bug and how I fixed it.
Solution 10 - C#
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.
Solution 11 - C#
It must be zero related, since changing the line
> d.d = -0.0
to: > d.d = 0.0
results in the comparison being true...