Can anyone explain this strange behavior with signed floats in C#?

C#.NetFloating Point

C# 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...

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlexander EfimovView Question on Stackoverflow
Solution 1 - C#SLaksView Answer on Stackoverflow
Solution 2 - C#Ben MView Answer on Stackoverflow
Solution 3 - C#Eric LippertView Answer on Stackoverflow
Solution 4 - C#Vilx-View Answer on Stackoverflow
Solution 5 - C#Matthew FlaschenView Answer on Stackoverflow
Solution 6 - C#SLaksView Answer on Stackoverflow
Solution 7 - C#João AngeloView Answer on Stackoverflow
Solution 8 - C#Viacheslav IvanovView Answer on Stackoverflow
Solution 9 - C#Jim MaView Answer on Stackoverflow
Solution 10 - C#Morten AndersonView Answer on Stackoverflow
Solution 11 - C#user243357View Answer on Stackoverflow