Saya telah menguji kode ini di https://dotnetfiddle.net/ :
using System;
public class Program
{
const float scale = 64 * 1024;
public static void Main()
{
Console.WriteLine(unchecked((uint)(ulong)(1.2 * scale * scale + 1.5 * scale)));
Console.WriteLine(unchecked((uint)(ulong)(scale* scale + 7)));
}
}
Jika saya kompilasi dengan .NET 4.7.2 saya dapatkan
859091763
7
Tetapi jika saya melakukan Roslyn atau .NET Core, saya mengerti
859091763
0
Mengapa ini terjadi?
ulong
sedang diabaikan dalam kasus terakhir sehingga terjadi dalamfloat
->int
konversi.ulong
mempengaruhi hasil.Jawaban:
Kesimpulan saya salah. Lihat pembaruan untuk detail lebih lanjut.
Sepertinya ada bug di kompiler pertama yang Anda gunakan. Nol adalah hasil yang benar dalam kasus ini .Urutan operasi yang ditentukan oleh spesifikasi C # adalah sebagai berikut:scale
denganscale
, menghasilkana
a + 7
, menghasilkanb
b
keulong
, menghasilkanc
c
keuint
, menghasilkand
Dua operasi pertama memberi Anda nilai float
b = 4.2949673E+09f
. Di bawah aritmatika floating-point standar, ini adalah4294967296
( Anda dapat memeriksanya di sini ). Itu cocok denganulong
baik-baik saja, jadic = 4294967296
, tapi itu persis satu lebih dariuint.MaxValue
, jadi pulang pergi0
, karenanyad = 0
. Sekarang, kejutan kejutan, karena aritmatika floating-point funky,4.2949673E+09f
dan4.2949673E+09f + 7
adalah nomor yang sama di IEEE 754. Jadiscale * scale
akan memberikan nilai yang sama darifloat
sepertiscale * scale + 7
,a = b
sehingga operasi kedua pada dasarnya adalah no-op.Kompilator Roslyn melakukan (beberapa) operasi const pada waktu kompilasi, dan mengoptimalkan keseluruhan ekspresi ini
0
.Sekali lagi, itulah hasil yang benar , dankompiler diizinkan untuk melakukan optimasi apa pun yang akan menghasilkan perilaku yang sama persis seperti kode tanpa mereka.Saya Dugaan adalah .NET 4.7.2 compiler yang Anda gunakan juga mencoba untuk mengoptimalkan ini, tetapi memiliki bug yang menyebabkannya untuk mengevaluasi para pemain di tempat yang salah. Tentu, jika Anda pertama kaliscale
melakukanuint
dan kemudian melakukan operasi, Anda mendapatkannya7
, karenascale * scale
pulang pergi ke0
dan kemudian Anda tambahkan7
. Tapi itu tidak konsisten dengan hasil yang akan Anda dapatkan ketika mengevaluasi ekspresi langkah demi langkah saat runtime . Sekali lagi, akar penyebabnya hanyalah dugaan ketika melihat perilaku yang dihasilkan, tetapi mengingat semua yang saya nyatakan di atas, saya yakin ini adalah pelanggaran spesifikasi di sisi kompiler pertama.MEMPERBARUI:
Saya telah melakukan kesalahan. Ada sedikit spesifikasi C # yang saya tidak tahu ada ketika menulis jawaban di atas:
C # menjamin operasi untuk memberikan tingkat presisi setidaknya pada tingkat IEEE 754, tetapi tidak harus tepat itu. Ini bukan bug, ini fitur spek. The Roslyn compiler dalam haknya untuk mengevaluasi ekspresi persis seperti IEEE 754 menspesifikasikan, dan compiler lain dalam haknya untuk menyimpulkan bahwa
2^32 + 7
adalah7
ketika dimasukkan ke dalamuint
.Saya minta maaf atas jawaban pertama saya yang menyesatkan, tetapi setidaknya kita semua belajar sesuatu hari ini.
sumber
scale
- masing dengan nilai float dan kemudian mengevaluasi semuanya saat runtime, hasilnya akan sama. Bisakah Anda menguraikan?Intinya di sini adalah (seperti yang Anda lihat pada dokumen ) bahwa nilai float hanya dapat memiliki basis hingga 2 ^ 24 . Jadi, ketika Anda menetapkan nilai 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ) itu menjadi, sebenarnya 2 ^ 24 * 2 ^ 8 , yaitu 4294967000 . Menambahkan 7 hanya akan menambah bagian yang terpotong oleh konversi ke ulong .
Jika Anda berubah menjadi dua kali lipat , yang memiliki basis 2 ^ 53 , itu akan bekerja untuk apa yang Anda inginkan.
Ini bisa menjadi masalah run-time tetapi, dalam kasus ini, ini adalah masalah waktu kompilasi, karena semua nilai adalah konstanta dan akan dievaluasi oleh kompiler.
sumber
Pertama-tama Anda menggunakan konteks tidak dicentang yang merupakan instruksi untuk kompiler yang Anda yakin, sebagai pengembang, bahwa hasilnya tidak akan meluap jenis dan Anda ingin melihat tidak ada kesalahan kompilasi. Dalam skenario Anda, Anda sebenarnya tipe sengaja meluap dan mengharapkan perilaku yang konsisten di tiga kompiler berbeda yang salah satunya mungkin kompatibel jauh ke belakang dalam sejarah dibandingkan dengan Roslyn dan .NET Core yang baru.
Yang kedua adalah Anda mencampurkan konversi implisit dan eksplisit. Saya tidak yakin tentang kompiler Roslyn, tapi jelas .NET Framework dan .NET Core kompiler dapat menggunakan optimasi yang berbeda untuk operasi tersebut.
Masalahnya di sini adalah bahwa baris pertama dari kode Anda hanya menggunakan nilai / tipe floating point, tetapi baris kedua adalah kombinasi dari nilai / tipe floating point dan nilai / tipe integral.
Jika Anda membuat integer tipe floating point langsung (7> 7.0) Anda akan mendapatkan hasil yang sama untuk ketiga sumber yang dikompilasi.
Jadi, saya akan mengatakan berlawanan dengan apa yang dijawab V0ldek dan itu adalah "Bug (jika itu benar-benar bug) kemungkinan besar dalam Roslyn dan .NET Core kompiler".
Alasan lain untuk meyakini bahwa adalah hasil dari hasil perhitungan pertama yang tidak dicentang sama untuk semua dan itu adalah nilai yang melebihi nilai maksimal
UInt32
tipe.Minus satu ada di sana saat kita mulai dari nol yang merupakan nilai yang sulit dikurangkan sendiri. Jika pemahaman matematika saya tentang overflow sudah benar, kita mulai dari angka berikutnya setelah nilai maksimal.
MEMPERBARUI
Menurut komentar jalsh
Komentarnya benar. Jika kita menggunakan float, Anda masih mendapatkan 0 untuk Roslyn dan .NET Core, tetapi di sisi lain menggunakan hasil ganda dalam 7.
Saya membuat beberapa tes tambahan dan segalanya menjadi lebih aneh, tetapi pada akhirnya semuanya masuk akal (setidaknya sedikit).
Apa yang saya asumsikan adalah .NET Framework 4.7.2 compiler (dirilis pada pertengahan 2018) benar-benar menggunakan optimasi yang berbeda dari .NET Core 3.1 dan Roslyn 3.4 compiler (dirilis pada akhir 2019). Optimalisasi / perhitungan yang berbeda ini murni digunakan untuk nilai konstan yang diketahui pada waktu kompilasi. Itu sebabnya ada kebutuhan untuk digunakan
unchecked
kata kunci karena kompiler sudah tahu ada overflow terjadi, tetapi perhitungan yang berbeda digunakan untuk mengoptimalkan IL akhir.Kode sumber yang sama dan IL yang hampir sama kecuali instruksi IL_000a. Satu kompiler menghitung 7 dan 0 lainnya.
Kode sumber
.NET Framework (x64) IL
Cabang penyusun Roslyn (Sep 2019) IL
Itu mulai berjalan dengan benar ketika Anda menambahkan ekspresi tidak konstan (secara default
unchecked
) seperti di bawah ini.Yang menghasilkan "persis" IL yang sama oleh kedua kompiler.
.NET Framework (x64) IL
Cabang penyusun Roslyn (Sep 2019) IL
Jadi, pada akhirnya saya percaya alasan untuk perilaku yang berbeda hanyalah versi kerangka kerja yang berbeda dan / atau kompiler yang menggunakan optimasi / perhitungan berbeda untuk ekspresi konstan, tetapi dalam kasus lain perilaku sangat sama.
sumber