Adakah yang bisa menjelaskan perilaku aneh ini dengan floats yang ditandatangani di C #?

247

Ini adalah contoh dengan komentar:

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

Jadi, apa pendapat Anda tentang ini?

Alexander Efimov
sumber
2
Untuk membuat hal-hal yang asing c.d.Equals(d.d)dievaluasi trueseperti halnyac.f.Equals(d.f)
Justin Niessner
2
Jangan bandingkan pelampung dengan perbandingan persis seperti .Equals. Itu hanya ide yang buruk.
Thorsten79
6
@ Thorsten79: Bagaimana itu relevan di sini?
Ben M
2
Ini yang paling aneh. Menggunakan panjang, bukan ganda, untuk f memperkenalkan perilaku yang sama. Dan menambahkan bidang pendek lainnya memperbaikinya lagi ...
Jens
1
Aneh - sepertinya hanya terjadi ketika keduanya memiliki tipe yang sama (float atau double). Ubah satu menjadi float (atau desimal) dan D2 berfungsi sama dengan D1.
tvanfosson

Jawaban:

387

Bug ada dalam dua baris berikut System.ValueType: (Saya masuk ke sumber referensi)

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

(Kedua metode tersebut adalah [MethodImpl(MethodImplOptions.InternalCall)])

Ketika semua bidang adalah 8 byte lebar, CanCompareBitssecara keliru mengembalikan nilai true, menghasilkan perbandingan bitwise dari dua nilai yang berbeda, tetapi identik secara semantik.

Ketika setidaknya satu bidang tidak lebar 8 byte, CanCompareBitsmengembalikan false, dan kode mulai menggunakan refleksi untuk mengulangi bidang dan memanggil Equalssetiap nilai, yang memperlakukan dengan benar -0.0sama dengan 0.0.

Berikut adalah sumber untuk CanCompareBitsdari 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
Slaks
sumber
158
Melangkah ke System.ValueType? Itu teman yang cukup hardcore.
Pierreten
2
Anda tidak menjelaskan apa pentingnya "lebar 8 byte". Apakah struct dengan semua bidang 4-byte tidak memiliki hasil yang sama? Saya menduga bahwa memiliki bidang 4-byte tunggal dan bidang 8-byte baru saja memicu IsNotTightlyPacked.
Gabe
1
@ Gabe saya menulis sebelumnya bahwaThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks
1
Dengan .NET sebagai perangkat lunak open source sekarang, berikut adalah tautan ke implementasi Core CLR dari ValueTypeHelper :: CanCompareBits . Tidak ingin memperbarui jawaban Anda karena implementasinya sedikit berubah dari sumber referensi yang Anda posting.
IInspectable
59

Saya menemukan jawabannya di http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

Bagian inti adalah sumber komentar CanCompareBits, yang ValueType.Equalsdigunakan untuk menentukan apakah akan menggunakan memcmpperbandingan-gaya:

Komentar dari CanCompareBits mengatakan "Return true jika valuetype tidak mengandung pointer dan sudah penuh". Dan FastEqualsCheck menggunakan "memcmp" untuk mempercepat perbandingan.

Penulis selanjutnya menyatakan dengan tepat masalah yang dijelaskan oleh OP:

Bayangkan Anda memiliki struktur yang hanya berisi pelampung. Apa yang akan terjadi jika satu berisi +0.0, dan yang lainnya berisi -0.0? Mereka harus sama, tetapi representasi biner yang mendasarinya berbeda. Jika Anda membuat struktur lain yang menimpa metode Persamaan, optimasi itu juga akan gagal.

Ben M
sumber
Aku ingin tahu apakah perilaku Equals(Object)untuk double, floatdan Decimalberubah selama draft awal .net; Saya akan berpikir bahwa lebih penting untuk X.Equals((Object)Y)hanya mengembalikan virtual trueketika Xdan Ytidak dapat dibedakan, daripada memiliki metode yang sesuai dengan perilaku kelebihan beban lainnya (terutama mengingat bahwa, karena paksaan tipe implisit, Equalsmetode kelebihan beban bahkan tidak mendefinisikan hubungan kesetaraan !, mis. 1.0f.Equals(1.0)menghasilkan false, tetapi 1.0.Equals(1.0f)menghasilkan true!) Masalah sebenarnya IMHO tidak dengan cara struktur dibandingkan ...
supercat
1
... tetapi dengan cara tipe-tipe nilai tersebut menimpa Equalsberarti sesuatu selain kesetaraan. Misalkan, misalnya, seseorang ingin menulis sebuah metode yang mengambil objek abadi dan, jika belum di-cache, melakukan ToStringdi atasnya dan cache hasilnya; jika telah di-cache, cukup kembalikan string yang di-cache. Bukan hal yang tidak masuk akal untuk dilakukan, tetapi akan gagal dengan Decimalkarena dua nilai mungkin membandingkan sama tetapi menghasilkan string yang berbeda.
supercat
52

Dugaan Vilx benar. Apa yang "CanCompareBits" lakukan adalah memeriksa untuk melihat apakah tipe nilai yang dimaksud "penuh sesak" dalam memori. Struct yang padat dibandingkan dengan hanya membandingkan bit biner yang membentuk struktur; struktur yang longgar dibandingkan dengan memanggil Equals pada semua anggota.

Ini menjelaskan pengamatan SLaks bahwa repros dengan struct yang semuanya berlipat ganda; struct seperti itu selalu penuh sesak.

Sayangnya seperti yang telah kita lihat di sini, itu memperkenalkan perbedaan semantik karena perbandingan bitwise dari double dan Equals perbandingan dari double memberikan hasil yang berbeda.

Eric Lippert
sumber
3
Lalu mengapa itu bukan bug? Meskipun MS merekomendasikan untuk mengesampingkan Persamaan pada tipe nilai selalu.
Alexander Efimov
14
Mengalahkan saya. Saya bukan ahli internal CLR.
Eric Lippert
4
... kamu tidak? Tentunya pengetahuan Anda tentang C # internal akan mengarah pada pengetahuan yang cukup tentang cara kerja CLR.
CaptainCasey
37
@ KaptenCasey: Saya telah menghabiskan lima tahun mempelajari internal dari kompiler C # dan mungkin total beberapa jam mempelajari internal CLR. Ingat, saya adalah konsumen CLR; Saya memahami area permukaan publiknya dengan cukup baik, tetapi bagian dalamnya adalah kotak hitam bagi saya.
Eric Lippert
1
Kesalahan saya, saya pikir kompiler CLR dan VB / C # lebih erat ... jadi C # / VB -> CIL -> CLR
CaptainCasey
22

Setengah jawaban:

Reflector memberi tahu kami bahwa ValueType.Equals()melakukan sesuatu seperti ini:

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

Sayangnya keduanya CanCompareBits()dan FastEquals()(keduanya metode statis) adalah eksternal ( [MethodImpl(MethodImplOptions.InternalCall)]) dan tidak memiliki sumber yang tersedia.

Kembali ke menebak mengapa satu kasus dapat dibandingkan dengan bit, dan yang lainnya tidak dapat (masalah pelurusan mungkin?)

Vilx-
sumber
17

Itu memang benar bagi saya, dengan Monm gmcs 2.4.2.3.

Matthew Flaschen
sumber
5
Ya, saya juga sudah mencobanya di Mono, dan itu memberi saya benar juga. Sepertinya MS melakukan beberapa keajaiban di dalam :)
Alexander Efimov
3
menarik, kita semua mengirim ke Mono?
WeNeedAnswers
14

Test case yang lebih sederhana:

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 : Bug ini juga terjadi dengan float, tetapi hanya terjadi jika bidang di struct menambahkan hingga kelipatan 8 byte.

Slaks
sumber
Sepertinya aturan pengoptimal yang berlaku: jika semua berlipat ganda daripada membandingkan sedikit, yang lain lakukan panggilan ganda yang terpisah. Panggilan yang sama
Henk Holterman
Saya tidak berpikir ini adalah kasus uji yang sama dengan apa masalah yang disajikan di sini adalah bahwa nilai default untuk Bad.f bukan 0, sedangkan kasus lain tampaknya menjadi masalah Int vs Double.
Driss Zouak
6
@ Drop: Nilai default untuk double adalah 0 . Anda salah.
SLaks
10

Itu harus terkait dengan perbandingan sedikit demi sedikit, karena 0.0harus berbeda -0.0hanya dari sedikit sinyal.

João Angelo
sumber
5

... apa pendapatmu tentang ini?

Selalu menimpa Equals dan GetHashCode pada tipe nilai. Ini akan cepat dan benar.

Viacheslav Ivanov
sumber
Selain peringatan bahwa ini hanya diperlukan ketika kesetaraan itu relevan, inilah yang saya pikirkan. Sama menyenangkannya dengan melihat keanehan dari perilaku kesetaraan tipe nilai nilai standar seperti yang dilakukan oleh jawaban tertinggi, ada alasan mengapa CA1815 ada.
Joe Amenta
@ JoAmenta Maaf atas jawaban yang terlambat. Dalam pandangan saya (tentu saja dalam pandangan saya), kesetaraan selalu ( ) relevan untuk jenis nilai. Implementasi kesetaraan standar tidak dapat diterima dalam kasus-kasus umum. ( ) Kecuali kasus yang sangat istimewa. Sangat. Sangat spesial. Ketika Anda tahu persis apa yang Anda lakukan dan mengapa.
Viacheslav Ivanov
Saya pikir kami setuju bahwa mengesampingkan pemeriksaan kesetaraan untuk jenis nilai hampir selalu mungkin dan bermakna dengan sangat sedikit pengecualian, dan biasanya akan membuatnya lebih benar. Poin yang saya coba sampaikan dengan kata "relevan" adalah bahwa ada beberapa tipe nilai yang instansnya tidak akan pernah dibandingkan dengan instances lain untuk kesetaraan, sehingga mengesampingkan akan menghasilkan kode mati yang perlu dipertahankan. Itu (dan kasus-kasus khusus yang aneh yang Anda singgung) akan menjadi satu-satunya tempat yang saya lewatkan.
Joe Amenta
4

Hanya pembaruan untuk bug berusia 10 tahun ini: telah diperbaiki ( Penafian : Saya penulis PR ini) dalam .NET Core yang mungkin akan dirilis dalam .NET Core 2.1.0.

The posting blog menjelaskan bug dan bagaimana saya tetap itu.

Jim Ma
sumber
2

Jika Anda membuat D2 seperti ini

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

itu benar.

jika kamu membuatnya seperti ini

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

Itu masih salah.

i t tampaknya seperti itu false jika struct hanya memegang ganda.

Morten Anderson
sumber
1

Itu harus nol terkait, karena mengubah garis

dd = -0.0

untuk:

dd = 0,0

menghasilkan perbandingan yang benar ...

pengguna243357
sumber
Sebaliknya, NaN dapat membandingkan satu sama lain untuk suatu perubahan, ketika mereka benar-benar menggunakan pola bit yang sama.
Harold