誰かがこの奇妙な動作をC#の符号付き浮動小数点数で説明できますか?


247

これはコメント付きの例です:

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

それで、これについてどう思いますか?


2
物事を奇妙にc.d.Equals(d.d)評価することは、次のtrueように評価されますc.f.Equals(d.f)
Justin Niessner

2
floatを.Equalsのような正確な比較と比較しないでください。それは単に悪い考えです。
Thorsten79

6
@ Thorsten79:ここでそれはどのように関連していますか?
Ben M

2
これは最も奇妙です。fにdoubleではなくlongを使用すると、同じ動作が発生します。そして、別の短いフィールドを追加すると、再び修正されます...
Jens

1
奇妙です-両方が同じタイプ(floatまたはdouble)の場合にのみ発生するようです。1つを浮動小数点(または10進数)に変更すると、D2はD1と同じように機能します。
tvanfosson

回答:


387

バグは次の2行にありSystem.ValueTypeます:(参照ソースに足を踏み入れました)

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

(両方の方法があります[MethodImpl(MethodImplOptions.InternalCall)]

すべてのフィールドの幅が8バイトのCanCompareBits場合、誤ってtrueが返され、2つの異なる値がビット単位で比較されますが、意味的には同じです。

少なくとも1つのフィールドに広い8つのバイトでない場合、CanCompareBitsfalseを戻し、フィールドをループし、コールに使用する反射のコード進行Equals各値、正しく扱いのため-0.0に等しいとして0.0

CanCompareBitsSSCLI のソースは次のとおりです。

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

158
System.ValueTypeにステップインしますか?それはかなり筋金入りの仲間です。
Pierreten

2
「8バイト幅」の意味が何であるかを説明しません。すべての4バイトフィールドを持つ構造体は同じ結果にならないでしょうか?4バイトのフィールドが1つと8バイトのフィールドが1つあるだけでトリガーされると思いますIsNotTightlyPacked
Gabe

1
@Gabe私が以前に書いたThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks 2013年

1
.NETが現在オープンソースソフトウェアであるため、ValueTypeHelper :: CanCompareBitsのコアCLR実装へのリンクを次に示します。あなたが投稿した参照ソースから実装が少し変更されているため、回答を更新したくありませんでした。
IInspectable 2017

59

答えはhttp://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspxで見つかりました。

コアピースはのソースコメントでありCanCompareBits、をValueType.Equals使用してmemcmpスタイル比較を使用するかどうかを決定します。

CanCompareBitsのコメントには、「valuetypeにポインターが含まれておらず、密にパックされている場合はtrueを返す」と記載されています。また、FastEqualsCheckは「memcmp」を使用して比較を高速化します。

著者は、OPによって記述された問題を正確に述べ続けます。

フロートのみを含む構造があるとします。一方に+0.0が含まれ、もう一方に-0.0が含まれている場合はどうなりますか?それらは同じである必要がありますが、基礎となるバイナリ表現は異なります。Equalsメソッドをオーバーライドする他の構造をネストすると、その最適化も失敗します。


私はの行動かしらEquals(Object)ためdoublefloatDecimal.NETの早期ドラフト中に変更しました。私はそれが仮想持つことがより重要だと思うだろうX.Equals((Object)Y)だけのリターンをtrueするときXY(特に与えられたオーバーロードされた、なぜなら暗黙の型変換の、それそれの方法は、他のオーバーロードの動作に合わせ持っているよりも、区別がつかないEquals方法でも同値関係を定義していません!、たとえば1.0f.Equals(1.0)falseを1.0.Equals(1.0f)生成しますが、trueを生成します!)実際の問題
IMHO

1
...しかし、これらの値タイプがオーバーライドEqualsして、同等以外の何かを意味する方法で。たとえば、不変オブジェクトを受け取るメソッドを作成し、それがまだキャッシュされていない場合は、そのオブジェクトに対して実行ToStringして結果をキャッシュしたいとします。キャッシュされている場合は、キャッシュされた文字列を返します。不合理なことではありませんがDecimal、2つの値は等しいものの、異なる文字列を生成する可能性があるため、失敗します。
スーパーキャット2012

52

Vilxの予想は正しい。「CanCompareBits」が行うことは、問題の値の型がメモリに「密にパック」されているかどうかを確認することです。密にパックされた構造体は、構造を構成するバイナリビットを比較するだけで比較されます。疎にパックされた構造は、すべてのメンバーでEqualsを呼び出すことによって比較されます。

これは、すべてダブルの構造体で再現するというSLaksの見解を説明しています。そのような構造体は常に密に詰め込まれています。

残念ながら、ここで見たように、ダブルのビットごとの比較とダブルのEquals比較は異なる結果をもたらすため、セマンティックの違いが生じます。


3
では、なぜそれがバグではないのですか?MSは常に値型のEqualsをオーバーライドすることを推奨していますが。
Alexander Efimov

14
私の一体を打ち負かす。私はCLRの内部についての専門家ではありません。
Eric Lippert

4
...そうではありませんか?確かに、C#の内部についての知識は、CLRのしくみに関するかなりの知識につながるでしょう。
CaptainCasey 2010年

37
@CaptainCasey:私は5年間C#コンパイラーの内部を研究し、おそらく合計で2時間CLRの内部を研究しました。私はCLRの消費者です。私はそのパブリックサーフェスエリアをかなりよく理解していますが、内部はブラックボックスです。
Eric Lippert

1
私の間違いは、CLRとVB / C#コンパイラの方がより緊密に結合されていると思っていた...だからC#/
VB-

22

答えの半分:

リフレクターは、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()(両方の静的メソッド)はextern([MethodImpl(MethodImplOptions.InternalCall)])であり、利用可能なソースがありません。

なぜ1つのケースをビットで比較できるのか、もう1つのケースは比較できないのかを推測することに戻ってください(アライメントの問題か?)



14

よりシンプルなテストケース:

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バイトの倍数になる場合にのみ発生します。


行くオプティマイザルールのように見えます:すべてがビット比較よりも2倍である場合、それ以外の場合は別々に2倍になります。等しい呼び出し
Henk Holterman

これは、ここで提示されている問題のように、Bad.fのデフォルト値が0ではないのと同じテストケースではないと思いますが、他のケースはInt対Doubleの問題のようです。
Driss Zouak

6
@Driss:のデフォルト値はdouble です 0。あなたが間違っている。
SLaks


5

…これについてどう思う?

常に値の型のEqualsとGetHashCodeをオーバーライドします。それは速くて正しいでしょう。


これは、平等が関係する場合にのみ必要であるという警告を除いて、まさに私が考えていたものです。最高投票数の回答のように、デフォルト値タイプの等価動作の癖を見るのと同じくらい楽しいですが、CA1815が存在する理由があります。
Joe Amenta

@JoeAmenta返事が遅くなってすみません。私の見解では(もちろん、私の見解では)、等価性は常に()値型に関連しています。一般的なケースでは、デフォルトの等価実装は受け入れられません。()非常に特殊な場合を除きます。非常に。とても特別な。あなたが何をしていて、その理由を正確に知っているとき。
Viacheslav Ivanov

値型の等価性チェックをオーバーライドすることは事実上常に可能であり、非常に少数の例外を除いて意味があり、通常は厳密により正確になることに同意します。「関連」という言葉で伝えようとしていた点は、インスタンスが他のインスタンスと等しいかどうか比較されることのない値の型がいくつかあるため、オーバーライドするとデッドコードが発生し、維持する必要があるということです。それら(およびあなたが暗示する奇妙な特殊ケース)は、私がスキップする唯一の場所でしょう。
Joe Amenta


2

このように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;
}

それはまだ誤りです。

構造体のみがダブルスを保持している場合、それは偽だようにtは思えます。


1

行を変更するので、ゼロに関連している必要があります

dd = -0.0

に:

dd = 0.0

比較は真になります...


逆に、実際に同じビットパターンを使用している場合、NaNは変更について互いに等しいと比較できます。
ハロルド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.