Przepełnienie C # dla niesprawdzonego uint

10

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?

Lukas
źródło
W ulongdrugim przypadku rzutowanie jest ignorowane, więc dzieje się to podczas konwersji float-> int.
madreflection
Jestem bardziej zaskoczony zmianą zachowania, która wydaje się dość dużą różnicą. Nie spodziewałbym się, że „0” będzie poprawną odpowiedzią w przypadku tego łańcucha rzutów tbh.
Lukas
Zrozumiale. Kilka rzeczy w specyfikacji zostało naprawionych w kompilatorze podczas budowania Roslyn, więc mogło to być jego częścią. Sprawdź dane wyjściowe JIT w tej wersji na SharpLab. To pokazuje, jak ulongrzutuje na wynik.
madreflection
To fascynujące, z twoim przykładem z powrotem na dotnetfiddle, ostatnie WriteLine wyświetla 0 w Roslyn 3.4 i 7 w .NET Core 3.1
Lukas
Potwierdziłem również na moim pulpicie. Kod JIT wcale nie wygląda na bliski, otrzymuję różne wyniki między .NET Core i .NET Framework. Trippy
Lukas

Odpowiedzi:

1

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:

  1. pomnóż scaleprzez scale, dająca
  2. wykonać a + 7, ustępującb
  3. oddać bdo ulongotrzymującc
  4. oddać cdo uintotrzymującd

Pierwsze dwie operacje pozostawiają wartość zmiennoprzecinkową równą b = 4.2949673E+09f. Zgodnie ze standardową arytmetyką zmiennoprzecinkową jest to 4294967296( możesz to sprawdzić tutaj ). To pasuje do ulongporządku, więc c = 4294967296, ale jest dokładnie o jeden więcej uint.MaxValue, więc w obie strony 0, więc d = 0. Teraz, tu niespodzianka, bo arytmetyka zmiennoprzecinkowa jest funky, 4.2949673E+09fi 4.2949673E+09f + 7jest dokładnie taka sama liczba w IEEE 754. Więc scale * scaledaje taką samą wartość floatjak scale * scale + 7, a = btak 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 , a kompilator 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 rzucisz scalena, uinta następnie wykonasz operację, otrzymasz 7, ponieważ w scale * scaleobie strony do, 0a następnie dodajesz 7. 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ź:

Operacje zmiennoprzecinkowe mogą być wykonywane z większą precyzją niż typ wyniku operacji. Na przykład niektóre architektury sprzętowe obsługują zmiennoprzecinkowy „rozszerzony” lub „długi podwójny” o większym zakresie i precyzji niż typ podwójny i domyślnie wykonują wszystkie operacje zmiennoprzecinkowe przy użyciu tego typu o wyższej precyzji. Tylko przy nadmiernym koszcie wydajności można stworzyć taką architekturę sprzętową, aby wykonywać operacje zmiennoprzecinkowe z mniejszą precyzją i zamiast wymagać implementacji, aby utracić zarówno wydajność, jak i precyzję, C # pozwala na zastosowanie typu o wyższej precyzji dla wszystkich operacji zmiennoprzecinkowych . Poza dostarczaniem bardziej precyzyjnych wyników, rzadko ma to wymierne efekty. Jednak w wyrażeniach postaci x * y / z

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 + 7to 7po włożeniu uint.

Przykro mi z powodu mojej wprowadzającej w błąd pierwszej odpowiedzi, ale przynajmniej dzisiaj wszyscy się czegoś nauczyliśmy.

V0ldek
źródło
Potem chyba mam błąd w bieżącym kompilatorze .NET Framework (próbowałem tylko w VS 2019, aby się upewnić) :) Myślę, że spróbuję sprawdzić, czy jest gdzieś, aby zalogować się do błędu, chociaż naprawienie czegoś takiego prawdopodobnie mają wiele niepożądanych efektów ubocznych i prawdopodobnie zostaną zignorowani ...
Lukas
Nie sądzę, że przedwcześnie rzuca się na int, co spowodowałoby o wiele wyraźniejsze problemy w WIELU przypadkach, wydaje mi się, że w tym przypadku operacja const nie ocenia wartości i nie rzuca jej do końca, co oznacza jest to, że zamiast przechowywać wartości pośrednie w
liczbach zmiennoprzecinkowych
@jalsh Nie sądzę, że rozumiem twoje przypuszczenia. Jeśli kompilator po prostu zastąpiłby każdą scalewartością zmiennoprzecinkową, a następnie ocenił wszystko inne w czasie wykonywania, wynik byłby taki sam. Czy możesz rozwinąć?
V0ldek
@ V0ldek, głosowanie było błędem, zredagowałem twoją odpowiedź, więc mogłem ją usunąć :)
jalsh
zgaduję, że tak naprawdę nie przechowywał wartości pośrednich w
liczbach
0

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.

Paulo Morgado
źródło
-2

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ł.

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))); // 859091763
        Console.WriteLine(unchecked((uint)(ulong)(scale * scale + 7.0))); // 7
    }
}

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ść UInt32typu.

Console.WriteLine(unchecked((uint)(ulong)(1.2 * scale * scale + 1.5 * scale) - UInt32.MaxValue - 1)); // 859091763

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

7.0 jest podwójnym, nie zmiennoprzecinkowym, spróbuj 7.0f, wciąż da ci 0

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 uncheckedsł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

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)(scale * scale + 7.0)));
    }
}

.NET Framework (x64) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    // Fields
    .field private static literal float32 scale = float32(65536)

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 17 (0x11)
        .maxstack 8

        IL_0000: ldc.i4 859091763
        IL_0005: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_000a: ldc.i4.7
        IL_000b: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0010: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2062
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

} // end of class Program

Oddział kompilatora Roslyn (wrzesień 2019) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [System.Private.CoreLib]System.Object
{
    // Fields
    .field private static literal float32 scale = float32(65536)

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 17 (0x11)
        .maxstack 8

        IL_0000: ldc.i4 859091763
        IL_0005: call void [System.Console]System.Console::WriteLine(uint32)
        IL_000a: ldc.i4.0
        IL_000b: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0010: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2062
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Private.CoreLib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

} // end of class Program

Zaczyna iść właściwą drogą, gdy dodajesz wyrażenia niestałe (domyślnie są unchecked), jak poniżej.

using System;

public class Program
{
    static Random random = new Random();

    public static void Main()
    {
        var scale = 64 * random.Next(1024, 1025);       
        uint f = (uint)(ulong)(scale * scale + 7f);
        uint d = (uint)(ulong)(scale * scale + 7d);
        uint i = (uint)(ulong)(scale * scale + 7);

        Console.WriteLine((uint)(ulong)(1.2 * scale * scale + 1.5 * scale)); // 859091763
        Console.WriteLine((uint)(ulong)(scale * scale + 7f)); // 7
        Console.WriteLine(f); // 7
        Console.WriteLine((uint)(ulong)(scale * scale + 7d)); // 7
        Console.WriteLine(d); // 7
        Console.WriteLine((uint)(ulong)(scale * scale + 7)); // 7
        Console.WriteLine(i); // 7
    }
}

Który generuje „dokładnie” tę samą IL przez oba kompilatory.

.NET Framework (x64) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    // Fields
    .field private static class [mscorlib]System.Random random

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 164 (0xa4)
        .maxstack 4
        .locals init (
            [0] int32,
            [1] uint32,
            [2] uint32
        )

        IL_0000: ldc.i4.s 64
        IL_0002: ldsfld class [mscorlib]System.Random Program::random
        IL_0007: ldc.i4 1024
        IL_000c: ldc.i4 1025
        IL_0011: callvirt instance int32 [mscorlib]System.Random::Next(int32, int32)
        IL_0016: mul
        IL_0017: stloc.0
        IL_0018: ldloc.0
        IL_0019: ldloc.0
        IL_001a: mul
        IL_001b: conv.r4
        IL_001c: ldc.r4 7
        IL_0021: add
        IL_0022: conv.u8
        IL_0023: conv.u4
        IL_0024: ldloc.0
        IL_0025: ldloc.0
        IL_0026: mul
        IL_0027: conv.r8
        IL_0028: ldc.r8 7
        IL_0031: add
        IL_0032: conv.u8
        IL_0033: conv.u4
        IL_0034: stloc.1
        IL_0035: ldloc.0
        IL_0036: ldloc.0
        IL_0037: mul
        IL_0038: ldc.i4.7
        IL_0039: add
        IL_003a: conv.i8
        IL_003b: conv.u4
        IL_003c: stloc.2
        IL_003d: ldc.r8 1.2
        IL_0046: ldloc.0
        IL_0047: conv.r8
        IL_0048: mul
        IL_0049: ldloc.0
        IL_004a: conv.r8
        IL_004b: mul
        IL_004c: ldc.r8 1.5
        IL_0055: ldloc.0
        IL_0056: conv.r8
        IL_0057: mul
        IL_0058: add
        IL_0059: conv.u8
        IL_005a: conv.u4
        IL_005b: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0060: ldloc.0
        IL_0061: ldloc.0
        IL_0062: mul
        IL_0063: conv.r4
        IL_0064: ldc.r4 7
        IL_0069: add
        IL_006a: conv.u8
        IL_006b: conv.u4
        IL_006c: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0071: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0076: ldloc.0
        IL_0077: ldloc.0
        IL_0078: mul
        IL_0079: conv.r8
        IL_007a: ldc.r8 7
        IL_0083: add
        IL_0084: conv.u8
        IL_0085: conv.u4
        IL_0086: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_008b: ldloc.1
        IL_008c: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0091: ldloc.0
        IL_0092: ldloc.0
        IL_0093: mul
        IL_0094: ldc.i4.7
        IL_0095: add
        IL_0096: conv.i8
        IL_0097: conv.u4
        IL_0098: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_009d: ldloc.2
        IL_009e: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_00a3: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2100
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

    .method private hidebysig specialname rtspecialname static 
        void .cctor () cil managed 
    {
        // Method begins at RVA 0x2108
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: newobj instance void [mscorlib]System.Random::.ctor()
        IL_0005: stsfld class [mscorlib]System.Random Program::random
        IL_000a: ret
    } // end of method Program::.cctor

} // end of class Program

Oddział kompilatora Roslyn (wrzesień 2019) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [System.Private.CoreLib]System.Object
{
    // Fields
    .field private static class [System.Private.CoreLib]System.Random random

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 164 (0xa4)
        .maxstack 4
        .locals init (
            [0] int32,
            [1] uint32,
            [2] uint32
        )

        IL_0000: ldc.i4.s 64
        IL_0002: ldsfld class [System.Private.CoreLib]System.Random Program::random
        IL_0007: ldc.i4 1024
        IL_000c: ldc.i4 1025
        IL_0011: callvirt instance int32 [System.Private.CoreLib]System.Random::Next(int32, int32)
        IL_0016: mul
        IL_0017: stloc.0
        IL_0018: ldloc.0
        IL_0019: ldloc.0
        IL_001a: mul
        IL_001b: conv.r4
        IL_001c: ldc.r4 7
        IL_0021: add
        IL_0022: conv.u8
        IL_0023: conv.u4
        IL_0024: ldloc.0
        IL_0025: ldloc.0
        IL_0026: mul
        IL_0027: conv.r8
        IL_0028: ldc.r8 7
        IL_0031: add
        IL_0032: conv.u8
        IL_0033: conv.u4
        IL_0034: stloc.1
        IL_0035: ldloc.0
        IL_0036: ldloc.0
        IL_0037: mul
        IL_0038: ldc.i4.7
        IL_0039: add
        IL_003a: conv.i8
        IL_003b: conv.u4
        IL_003c: stloc.2
        IL_003d: ldc.r8 1.2
        IL_0046: ldloc.0
        IL_0047: conv.r8
        IL_0048: mul
        IL_0049: ldloc.0
        IL_004a: conv.r8
        IL_004b: mul
        IL_004c: ldc.r8 1.5
        IL_0055: ldloc.0
        IL_0056: conv.r8
        IL_0057: mul
        IL_0058: add
        IL_0059: conv.u8
        IL_005a: conv.u4
        IL_005b: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0060: ldloc.0
        IL_0061: ldloc.0
        IL_0062: mul
        IL_0063: conv.r4
        IL_0064: ldc.r4 7
        IL_0069: add
        IL_006a: conv.u8
        IL_006b: conv.u4
        IL_006c: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0071: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0076: ldloc.0
        IL_0077: ldloc.0
        IL_0078: mul
        IL_0079: conv.r8
        IL_007a: ldc.r8 7
        IL_0083: add
        IL_0084: conv.u8
        IL_0085: conv.u4
        IL_0086: call void [System.Console]System.Console::WriteLine(uint32)
        IL_008b: ldloc.1
        IL_008c: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0091: ldloc.0
        IL_0092: ldloc.0
        IL_0093: mul
        IL_0094: ldc.i4.7
        IL_0095: add
        IL_0096: conv.i8
        IL_0097: conv.u4
        IL_0098: call void [System.Console]System.Console::WriteLine(uint32)
        IL_009d: ldloc.2
        IL_009e: call void [System.Console]System.Console::WriteLine(uint32)
        IL_00a3: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2100
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Private.CoreLib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

    .method private hidebysig specialname rtspecialname static 
        void .cctor () cil managed 
    {
        // Method begins at RVA 0x2108
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: newobj instance void [System.Private.CoreLib]System.Random::.ctor()
        IL_0005: stsfld class [System.Private.CoreLib]System.Random Program::random
        IL_000a: ret
    } // end of method Program::.cctor

} // end of class Program

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.

dropoutcoder
źródło
7.0 to double, a nie float, spróbuj 7.0f, nadal da ci 0
jalsh
Tak, powinien być typu zmiennoprzecinkowego, a nie zmiennoprzecinkowego. Dzięki za korektę.
dropoutcoder
To zmienia całą perspektywę problemu, gdy masz do czynienia z podwójną precyzją, którą otrzymujesz, jest znacznie wyższa, a wynik wyjaśniony w odpowiedzi V0ldka zmienia się drastycznie, możesz po prostu zmienić skalę na podwójną i sprawdzić ponownie, wyniki byłyby takie same. ..
jalsh
W końcu jest to bardziej złożony problem.
dropoutcoder
1
@jalsh Tak, ale istnieje flaga kompilatora, która wszędzie zmienia sprawdzony kontekst. Możesz sprawdzić wszystko pod kątem bezpieczeństwa, z wyjątkiem pewnej gorącej ścieżki, która wymaga wszystkich możliwych cykli procesora.
V0ldek