【问题标题】:Can anyone explain this strange behavior with signed floats in C#?任何人都可以用 C# 中的签名浮点数来解释这种奇怪的行为吗?
【发布时间】:2011-01-31 07:47:07
【问题描述】:

以下是 cmets 的示例:

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        
    }
}

那么,您对此有何看法?

【问题讨论】:

  • 为了让事情变得陌生,c.d.Equals(d.d)c.f.Equals(d.f) 一样计算为 true
  • 不要将浮点数与 .Equals 等精确比较进行比较。这简直是​​个坏主意。
  • @Thorsten79:这与这里有什么关系?
  • 这是最奇怪的。对 f 使用 long 而不是 double 会引入相同的行为。并添加另一个短字段再次更正它......
  • 奇怪——它似乎只发生在两者都是相同类型(浮点或双精度)时。将一个更改为浮点数(或小数),D2 的工作方式与 D1 相同。

标签: c# .net floating-point


【解决方案1】:

bug在System.ValueType下面两行:(我踩到了参考源)

if (CanCompareBits(this)) 
    return FastEqualsCheck(thisObj, obj);

(两种方法都是[MethodImpl(MethodImplOptions.InternalCall)]

当所有字段都是 8 字节宽时,CanCompareBits 错误地返回 true,从而导致两个不同但语义相同的值的按位比较。

当至少一个字段不是 8 字节宽时,CanCompareBits 返回 false,代码继续使用反射循环遍历字段并为每个值调用 Equals,它正确地将 -0.0 视为等于0.0.

这是来自 SSCLI 的 CanCompareBits 的来源:

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

【讨论】:

  • 进入 System.ValueType?这真是太铁杆了兄弟。
  • 你没有解释“8字节宽”的意义是什么。具有所有 4 字节字段的结构不会有相同的结果吗?我猜有一个 4 字节字段和一个 8 字节字段只会触发 IsNotTightlyPacked
  • @Gabe 我之前写过The bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
  • 随着 .NET 现在是开源软件,这里是 ValueTypeHelper::CanCompareBits 的核心 CLR 实现的链接。不想更新您的答案,因为实施与您发布的参考源略有不同。
【解决方案2】:

我在http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx找到了答案。

核心部分是CanCompareBits的源评论,ValueType.Equals用来判断是否使用memcmp-style比较:

CanCompareBits 的评论说 "如果值类型不返回真 包含指针并且紧密 打包”。和 FastEqualsCheck 使用 "memcmp" 加快比较速度。

作者继续准确说明 OP 描述的问题:

想象你有一个结构 只包含一个浮点数。会发生什么 如果一个包含 +0.0,另一个 包含-0.0?他们应该是 相同,但底层二进制文件 表示方式不同。如果你 嵌套其他覆盖的结构 Equals 方法,即优化 也会失败。

【讨论】:

  • 我想知道Equals(Object)doublefloatDecimal 的行为是否在.net 的早期草稿中发生了变化;我认为让虚拟X.Equals((Object)Y) 仅在XY 无法区分时才返回true 比让该方法与其他重载的行为相匹配更重要(特别是考虑到,因为隐式类型强制,重载的Equals 方法甚至没有定义等价关系!例如1.0f.Equals(1.0) 产生错误,但1.0.Equals(1.0f) 产生正确!)恕我直言,真正的问题不在于比较结构的方式......
  • ...但是这些值类型会覆盖Equals 以表示等价以外的含义。例如,假设一个人想要编写一个方法,该方法接受一个不可变对象,如果它还没有被缓存,则对其执行ToString 并缓存结果;如果它已被缓存,则只需返回缓存的字符串。这不是一件不合理的事情,但使用Decimal 会失败,因为两个值可能比较相等但产生不同的字符串。
【解决方案3】:

Vilx 的猜想是正确的。 “CanCompareBits”所做的是检查所讨论的值类型是否在内存中“紧密包装”。通过简单地比较构成结构的二进制位来比较紧凑的结构;通过对所有成员调用 Equals 来比较松散的结构。

这解释了 SLaks 的观察,即它使用所有双精度的结构进行复制;这样的结构总是紧凑的。

不幸的是,正如我们在这里看到的那样,这引入了语义差异,因为双精度数的按位比较和双精度数的相等比较会产生不同的结果。

【讨论】:

  • 那为什么不是bug呢?尽管 MS 建议始终在值类型上覆盖 Equals。
  • 让我大吃一惊。我不是 CLR 内部的专家。
  • ...你不是吗?当然,您对 C# 内部结构的了解会导致您对 CLR 的工作原理有相当多的了解。
  • @CaptainCasey:我花了五年时间研究 C# 编译器的内部结构,可能总共花了几个小时研究 CLR 的内部结构。请记住,我是 CLR 的消费者;我相当了解它的公共表面区域,但它的内部对我来说是一个黑匣子。
  • 我的错误,我认为 CLR 和 VB/C# 编译器耦合更紧密......所以 C#/VB -> CIL -> CLR
【解决方案4】:

半个答案:

Reflector 告诉我们ValueType.Equals() 做了这样的事情:

if (CanCompareBits(this))
    return FastEqualsCheck(this, obj);
else
    // Use reflection to step through each member and call .Equals() on each one.

很遗憾,CanCompareBits()FastEquals()(都是静态方法)都是外部 ([MethodImpl(MethodImplOptions.InternalCall)]) 并且没有可用的源。

回到猜测为什么一种情况可以按位比较,而另一种不能(可能是对齐问题?)

【讨论】:

    【解决方案5】:

    使用 Mono 的 gmcs 2.4.2.3,它确实对我来说是正确的。

    【讨论】:

    • 是的,我也在 Mono 中尝试过,它也给了我真实的感觉。看起来 MS 在里面做了一些魔法:)
    • 有趣,我们都运送到 Mono?
    【解决方案6】:

    更简单的测试用例:

    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;
    }
    

    编辑:该错误也发生在浮点数中,但仅当结构中的字段加起来为 8 个字节的倍数时才会发生。

    【讨论】:

    • 看起来像一个优化器规则:如果它的全部翻倍而不是位比较,否则做单独的 double.Equal 调用
    • 我不认为这与这里提出的问题似乎是相同的测试用例 Bad.f 的默认值不是 0,而另一种情况似乎是 Int与双重问题。
    • @Driss:double 的默认值 0。你错了。
    【解决方案7】:

    它必须与逐位比较相关,因为0.0-0.0的区别仅在于信号位。

    【讨论】:

      【解决方案8】:

      ……你怎么看?

      始终覆盖值类型上的 Equals 和 GetHashCode。它将快速且正确。

      【讨论】:

      • 除了需要注意的是只有在平等相关时才需要这样做,这正是我的想法。看看默认值类型相等行为的怪癖就像最高投票的答案一样有趣,CA1815 存在是有原因的。
      • @JoeAmenta 抱歉,回复晚了。在我看来(当然只是在我看来),等式总是与值类型相关()。在常见情况下,默认相等实现是不可接受的。 () 非常特殊的情况除外。非常。非常特别。当你确切地知道你在做什么以及为什么。
      • 我认为我们同意重写值类型的相等性检查几乎总是可行且有意义的,只有极少数例外,并且通常会使它更严格地更正确。我试图用“相关”这个词表达的观点是,有些值类型的实例永远不会与其他实例进行比较以获得相等性,因此覆盖将导致需要维护的死代码。那些(以及你提到的奇怪的特殊情况)将是我唯一会跳过它的地方。
      【解决方案9】:

      只是这个 10 年前的 bug 的更新:它 has been fixed免责声明:我是这个 PR 的作者)在 .NET Core 中可能会在 .NET Core 2.1 中发布.0.

      blog post 解释了这个错误以及我如何修复它。

      【讨论】:

        【解决方案10】:

        如果你像这样制作 D2

        public struct D2
        {
            public double d;
            public double f;
            public string s;
        }
        

        这是真的。

        如果你这样做

        public struct D2
        {
            public double d;
            public double f;
            public double u;
        }
        

        还是假的。

        it 如果结构只包含双精度,这似乎是错误的。

        【讨论】:

          【解决方案11】:

          它必须是零相关的,因为改变了行

          d.d = -0.0

          到:

          d.d = 0.0

          结果比较为真...

          【讨论】:

          • 相反,当 NaN 实际使用相同的位模式时,它们可以相互比较以进行更改。
          猜你喜欢
          • 2011-12-01
          • 1970-01-01
          • 2023-03-26
          • 2019-06-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-03-08
          • 1970-01-01
          相关资源
          最近更新 更多