Testowałem ten kod na 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)));
}
}
Jeśli skompiluję z .NET 4.7.2, dostanę
859091763
7
Ale jeśli robię Roslyn lub .NET Core, dostaję
859091763
0
Dlaczego to się dzieje?
ulong
drugim przypadku rzutowanie jest ignorowane, więc dzieje się to podczas konwersjifloat
->int
.ulong
rzutuje na wynik.Odpowiedzi:
Moje wnioski były błędne. Zobacz aktualizację, aby uzyskać więcej informacji.
Wygląda jak błąd w pierwszym kompilatorze, którego użyłeś. Zero jest poprawnym wynikiem w tym przypadku .Kolejność operacji podanych w specyfikacji C # jest następująca:scale
przezscale
, dająca
a + 7
, ustępującb
b
doulong
otrzymującc
c
douint
otrzymującd
Pierwsze dwie operacje pozostawiają wartość zmiennoprzecinkową równą
b = 4.2949673E+09f
. Zgodnie ze standardową arytmetyką zmiennoprzecinkową jest to4294967296
( możesz to sprawdzić tutaj ). To pasuje doulong
porządku, więcc = 4294967296
, ale jest dokładnie o jeden więcejuint.MaxValue
, więc w obie strony0
, więcd = 0
. Teraz, tu niespodzianka, bo arytmetyka zmiennoprzecinkowa jest funky,4.2949673E+09f
i4.2949673E+09f + 7
jest dokładnie taka sama liczba w IEEE 754. Więcscale * scale
daje taką samą wartośćfloat
jakscale * scale + 7
,a = b
tak drugi operacji jest w zasadzie nie-op.Kompilator Roslyn wykonuje (niektóre) operacje const w czasie kompilacji i optymalizuje całe to wyrażenie
0
.Ponownie, jest to poprawny wynik , akompilator może wykonywać wszelkie optymalizacje, które spowodują takie same zachowanie, jak kod bez nich.Moje przypuszczenie jest, że .NET 4.7.2 kompilator użyty również stara się zoptymalizować to daleko, ale ma błąd, który powoduje, że ocena oddanych w niewłaściwym miejscu. Oczywiście, jeśli najpierw rzuciszscale
na,uint
a następnie wykonasz operację, otrzymasz7
, ponieważ wscale * scale
obie strony do,0
a następnie dodajesz7
. Jest to jednak niespójne z wynikiem, który można uzyskać, oceniając wyrażenia krok po kroku w czasie wykonywania . Ponownie, podstawowa przyczyna jest tylko zgadywaniem, gdy patrzy się na wytworzone zachowanie, ale biorąc pod uwagę wszystko, co powiedziałem powyżej, jestem przekonany, że jest to naruszenie specyfikacji po stronie pierwszego kompilatora.AKTUALIZACJA:
Zrobiłem głupek. Jest trochę specyfikacji C # , o której istnieniu nie wiedziałem, pisząc powyższą odpowiedź:
C # gwarantuje operacje zapewniające poziom precyzji przynajmniej na poziomie IEEE 754, ale niekoniecznie dokładnie to. To nie jest błąd, to funkcja specyfikacji. Kompilator Roslyn ma prawo do oceny wyrażenia dokładnie tak, jak określa to IEEE 754, a drugi kompilator ma prawo wydedukować
2^32 + 7
to7
po włożeniuuint
.Przykro mi z powodu mojej wprowadzającej w błąd pierwszej odpowiedzi, ale przynajmniej dzisiaj wszyscy się czegoś nauczyliśmy.
źródło
scale
wartością zmiennoprzecinkową, a następnie ocenił wszystko inne w czasie wykonywania, wynik byłby taki sam. Czy możesz rozwinąć?Chodzi tutaj o to (jak widać w dokumentach ), że wartości zmiennoprzecinkowe mogą mieć podstawę tylko do 2 ^ 24 . Tak więc, gdy przypisujesz wartość 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ), staje się w rzeczywistości 2 ^ 24 * 2 ^ 8 , czyli 4294967000 . Dodanie 7 spowoduje dodanie tylko do części obciętej przez konwersję na ulong .
Jeśli zmienisz na double , który ma podstawę 2 ^ 53 , będzie działać na to, czego chcesz.
Może to być problem z czasem wykonywania, ale w tym przypadku jest to problem z czasem kompilacji, ponieważ wszystkie wartości są stałymi i zostaną ocenione przez kompilator.
źródło
Przede wszystkim używasz niezaznaczonego kontekstu, który jest instrukcją dla kompilatora, na pewno jesteś programistą, że wynik nie zostanie przepełniony i nie zobaczysz żadnego błędu kompilacji. W twoim scenariuszu jesteś celowo przepełniony i spodziewasz się spójnego zachowania w trzech różnych kompilatorach, z których jeden prawdopodobnie jest kompatybilny z historią w porównaniu do Roslyn i .NET Core, które są nowymi.
Po drugie, łączysz konwersje niejawne i jawne. Nie jestem pewien co do kompilatora Roslyn, ale zdecydowanie .NET Framework i kompilatory .NET Core mogą używać różnych optymalizacji dla tych operacji.
Problem polega na tym, że pierwszy wiersz kodu używa tylko wartości / typów zmiennoprzecinkowych, ale drugi wiersz jest kombinacją wartości / typów zmiennoprzecinkowych i wartości / typu całkowego.
Jeśli od razu utworzysz zmiennoprzecinkowe liczby całkowite (7> 7.0), uzyskasz taki sam wynik dla wszystkich trzech skompilowanych źródeł.
Powiedziałbym więc odwrotnie niż na to, co odpowiedział V0ldek: „Błąd (jeśli to naprawdę błąd) najprawdopodobniej występuje w kompilatorach Roslyn i .NET Core”.
Kolejny powód, by sądzić, że wynik pierwszego niezaznaczonego wyniku obliczeń jest taki sam dla wszystkich i jest to wartość przekraczająca maksymalną wartość
UInt32
typu.Jest jeden minus, gdy zaczynamy od zera, czyli wartości, którą trudno jest odjąć. Jeśli moje matematyczne rozumienie przepełnienia jest prawidłowe, zaczynamy od następnej liczby po wartości maksymalnej.
AKTUALIZACJA
Zgodnie z komentarzem Jalsh
Jego komentarz jest poprawny. W przypadku użycia liczby zmiennoprzecinkowej nadal otrzymujesz 0 dla Roslyn i .NET Core, ale z drugiej strony przy podwójnych wynikach w 7.
Zrobiłem kilka dodatkowych testów i wszystko staje się jeszcze dziwniejsze, ale ostatecznie wszystko ma sens (przynajmniej trochę).
Zakładam, że kompilator .NET Framework 4.7.2 (wydany w połowie 2018 r.) Naprawdę używa innych optymalizacji niż kompilatory .NET Core 3.1 i Roslyn 3.4 (wydany pod koniec 2019 r.). Te różne optymalizacje / obliczenia są wykorzystywane wyłącznie dla stałych wartości znanych w czasie kompilacji. Dlatego konieczne było użycie
unchecked
słowa kluczowego, ponieważ kompilator już wie, że dzieje się przepełnienie, ale do optymalizacji końcowej IL zastosowano inne obliczenia.Ten sam kod źródłowy i prawie taka sama IL oprócz instrukcji IL_000a. Jeden kompilator oblicza 7, a drugi 0.
Kod źródłowy
.NET Framework (x64) IL
Oddział kompilatora Roslyn (wrzesień 2019) IL
Zaczyna iść właściwą drogą, gdy dodajesz wyrażenia niestałe (domyślnie są
unchecked
), jak poniżej.Który generuje „dokładnie” tę samą IL przez oba kompilatory.
.NET Framework (x64) IL
Oddział kompilatora Roslyn (wrzesień 2019) IL
Tak więc ostatecznie uważam, że powodem różnych zachowań jest po prostu inna wersja frameworka i / lub kompilatora, które używają różnych optymalizacji / obliczeń dla stałych wyrażeń, ale w innych przypadkach zachowanie jest bardzo takie samo.
źródło