Comportamento di overflow C # per uint non selezionato


10

Ho testato questo codice su 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)));
    }
}

Se compilo con .NET 4.7.2 ottengo

859091763

7

Ma se lo faccio Roslyn o .NET Core, ottengo

859091763

0

Perché succede?


Il cast di ulongviene ignorato in quest'ultimo caso, quindi sta accadendo nella conversione float-> int.
madreflection,

Sono più sorpreso dal cambiamento di comportamento, che sembra una differenza abbastanza grande. Non mi aspetto che "0" sia una risposta valida neanche con quella catena di cast tbh.
Lukas,

Comprensibile. Diverse cose nelle specifiche sono state risolte nel compilatore quando hanno costruito Roslyn, in modo che potesse farne parte. Scopri l'output JIT su questa versione su SharpLab. Ciò mostra in che modo il cast ulonginfluisce sul risultato.
madreflection il

È affascinante, con il tuo esempio tornato su dotnetfiddle, l'ultimo WriteLine produce 0 in Roslyn 3.4 e 7 in .NET Core 3.1
Lukas

Ho anche confermato sul mio desktop. Il codice JIT non sembra nemmeno vicino, ottengo risultati diversi tra .NET Core e .NET Framework. Trippy
Lukas

Risposte:


1

Le mie conclusioni erano errate. Vedi l'aggiornamento per maggiori dettagli.

Sembra un bug nel primo compilatore che hai usato. Zero è il risultato corretto in questo caso . L'ordine delle operazioni dettato dalla specifica C # è il seguente:

  1. moltiplicare scaleper scale, cedendoa
  2. esibirsi a + 7, cedereb
  3. cast ba ulong, cedendoc
  4. cast ca uint, cedendod

Le prime due operazioni ti lasciano con un valore float di b = 4.2949673E+09f. In base all'aritmetica standard in virgola mobile, questo è 4294967296( puoi verificarlo qui ). Questo si adatta ulongperfettamente, quindi c = 4294967296, ma è esattamente uno in più di uint.MaxValue, quindi va di andata e ritorno a 0, quindi d = 0. Ora, sorpresa sorpresa, dal momento che aritmetica in virgola mobile è funky, 4.2949673E+09fed 4.2949673E+09f + 7è lo stesso numero esatto in IEEE 754. Così scale * scalevi darà lo stesso valore di un floatcome scale * scale + 7, a = b, quindi la seconda operazioni è fondamentalmente un no-op.

Il compilatore Roslyn esegue (alcune) operazioni const in fase di compilazione e ottimizza l'intera espressione su 0. Ancora una volta, questo è il risultato corretto e al compilatore è consentito eseguire qualsiasi ottimizzazione che comporterà esattamente lo stesso comportamento del codice senza di essi.

La mia ipotesi è che anche il compilatore .NET 4.7.2 che hai usato tenti di ottimizzarlo, ma ha un bug che gli fa valutare il cast in un posto sbagliato. Naturalmente, se prima lanci scalesu un uinte poi esegui l'operazione, ottieni 7, perché scale * scalei viaggi di andata e ritorno 0e poi aggiungi 7. Ma ciò non è coerente con il risultato che otterresti quando valuti le espressioni passo dopo passo in fase di esecuzione . Ancora una volta, la causa principale è solo un'ipotesi quando si guarda al comportamento prodotto, ma dato tutto quello che ho detto sopra sono convinto che si tratti di una violazione delle specifiche sul lato del primo compilatore.

AGGIORNARE:

Ho fatto un cretino. C'è questa parte della specifica C # che non sapevo esistesse quando scrivevo la risposta sopra:

Le operazioni in virgola mobile possono essere eseguite con una precisione maggiore rispetto al tipo di risultato dell'operazione. Ad esempio, alcune architetture hardware supportano un tipo a virgola mobile "esteso" o "doppio lungo" con una portata e una precisione maggiori rispetto al tipo doppio e implicitamente eseguono tutte le operazioni in virgola mobile utilizzando questo tipo di precisione più elevata. Solo a costi eccessivi in ​​termini di prestazioni, è possibile realizzare architetture hardware di questo tipo per eseguire operazioni in virgola mobile con meno precisione e, anziché richiedere un'implementazione per rinunciare a prestazioni e precisione, C # consente di utilizzare un tipo di precisione più elevata per tutte le operazioni in virgola mobile . Oltre a fornire risultati più precisi, ciò raramente ha effetti misurabili. Tuttavia, nelle espressioni della forma x * y / z,

C # garantisce le operazioni per fornire un livello di precisione almeno a livello di IEEE 754, ma non necessariamente esattamente questo. Non è un bug, è una caratteristica specifica. Il compilatore Roslyn ha il diritto di valutare l'espressione esattamente come specificato da IEEE 754 e l'altro compilatore ha il diritto di dedurlo quando 2^32 + 7viene 7inserito uint.

Mi dispiace per la mia prima risposta fuorviante, ma almeno oggi abbiamo imparato qualcosa.


Quindi suppongo che abbiamo un bug nell'attuale compilatore .NET Framework (ho appena provato in VS 2019 solo per essere sicuro) :) Credo che proverò a vedere se c'è un posto dove registrare un bug, anche se risolvere qualcosa del genere sarebbe probabilmente ha molti effetti collaterali indesiderati e probabilmente viene ignorato ...
Lukas

Non penso che sia prematuramente il casting su int, che avrebbe causato problemi molto più chiari in MOLTI casi, immagino che il caso qui sia che nell'operazione const non sta valutando il valore e lanciandolo fino all'ultimo, il che significa è che invece di memorizzare i valori intermedi in float, lo sta semplicemente saltando e sostituendolo in ogni espressione con l'espressione stessa
jalsh

@jalsh Non credo di aver capito la tua ipotesi. Se il compilatore semplicemente sostituisse ciascuno scalecon il valore float e quindi valutasse tutto il resto in fase di esecuzione, il risultato sarebbe lo stesso. Puoi elaborare?
V0ldek,

@ V0ldek, il downvote è stato un errore, ho modificato la tua risposta in modo da poterla rimuovere :)
jalsh

la mia ipotesi è che in realtà non ha archiviato i valori intermedi in float, ha semplicemente sostituito f con l'espressione che valuta f senza lanciarlo in float
jalsh

0

Il punto qui è (come puoi vedere nei documenti ) che i valori float possono avere solo una base fino a 2 ^ 24 . Quindi, quando assegni un valore di 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ) diventa, in realtà 2 ^ 24 * 2 ^ 8 , che è 4294967000 . L'aggiunta di 7 verrà aggiunta solo alla parte troncata dalla conversione in ulong .

Se cambi in doppio , che ha una base di 2 ^ 53 , funzionerà per quello che vuoi.

Questo potrebbe essere un problema di runtime ma, in questo caso, è un problema di tempo di compilazione, perché tutti i valori sono costanti e verranno valutati dal compilatore.


-2

Prima di tutto stai usando un contesto non controllato che è un'istruzione per il compilatore di cui sei sicuro, come sviluppatore, che il risultato non traboccerà e non vorrai vedere alcun errore di compilazione. Nel tuo scenario sei in realtà un tipo di trabocco intenzionale e ti aspetti un comportamento coerente tra tre diversi compilatori, uno dei quali è probabilmente retrocompatibile con la storia rispetto a Roslyn e .NET Core, che sono nuovi.

La seconda cosa è che stai mescolando conversioni implicite ed esplicite. Non sono sicuro del compilatore Roslyn, ma sicuramente i compilatori .NET Framework e .NET Core potrebbero utilizzare diverse ottimizzazioni per tali operazioni.

Il problema qui è che la prima riga del codice utilizza solo valori / tipi in virgola mobile, ma la seconda riga è una combinazione di valori / tipi in virgola mobile e valore / tipo integrale.

Nel caso in cui si realizzi immediatamente un numero intero in virgola mobile (7> 7.0) si otterrà lo stesso risultato per tutte e tre le fonti compilate.

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

Quindi, direi l'opposto di ciò che ha risposto V0ldek e che è "Il bug (se è davvero un bug) è molto probabilmente nei compilatori Roslyn e .NET Core".

Un altro motivo per credere è che il risultato dei primi risultati di calcolo non controllati è lo stesso per tutti ed è il valore che trabocca il valore massimo del UInt32tipo.

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

Meno uno è lì mentre partiamo da zero, che è un valore che è difficile sottrarre se stesso. Se la mia comprensione matematica dell'overflow è corretta, partiamo dal numero successivo dopo il valore massimo.

AGGIORNARE

Secondo il commento di jalsh

7.0 è un doppio, non un float, prova 7.0f, ti darà comunque uno 0

Il suo commento è corretto. Nel caso in cui utilizziamo float, ottieni ancora 0 per Roslyn e .NET Core, ma d'altra parte usando i doppi risultati in 7.

Ho fatto alcuni test aggiuntivi e le cose diventano ancora più strane, ma alla fine tutto ha un senso (almeno un po ').

Quello che presumo è che il compilatore .NET Framework 4.7.2 (rilasciato a metà 2018) utilizzi davvero ottimizzazioni diverse rispetto ai compilatori .NET Core 3.1 e Roslyn 3.4 (rilasciati alla fine del 2019). Queste diverse ottimizzazioni / calcoli sono utilizzate esclusivamente per valori costanti noti in fase di compilazione. Questo è il motivo per cui era necessario utilizzare la uncheckedparola chiave poiché il compilatore sa già che si sta verificando un overflow, ma per ottimizzare l'IL finale è stato utilizzato un calcolo diverso.

Stesso codice sorgente e quasi lo stesso IL tranne l'istruzione IL_000a. Un compilatore calcola 7 e l'altro 0.

Codice sorgente

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

Ramo del compilatore di Roslyn (settembre 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

Inizia ad andare nel modo giusto quando aggiungi espressioni non costanti (per impostazione predefinita unchecked) come di seguito.

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

Il che genera "esattamente" lo stesso IL di entrambi i compilatori.

.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

Ramo del compilatore di Roslyn (settembre 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

Quindi, alla fine, credo che la ragione di un comportamento diverso sia solo una versione diversa di framework e / o compilatore che utilizza ottimizzazioni / calcoli diversi per espressioni costanti, ma in altri casi il comportamento è molto simile.


7.0 è un doppio, non un float, prova 7.0f, ti darà comunque uno 0
jalsh

Sì, dovrebbe essere in virgola mobile, non float. Grazie per la correzione.
dropoutcoder

Ciò cambia l'intera prospettiva del problema, quando si tratta di raddoppiare la precisione che si ottiene è molto più elevata e il risultato spiegato nella risposta di V0ldek cambia drasticamente, si potrebbe semplicemente cambiare scala per raddoppiare e ricontrollare, i risultati sarebbero gli stessi. ..
Jalsh

Alla fine è un problema più complesso.
dropoutcoder

1
@jalsh Sì, ma c'è un flag di compilazione che trasforma il contesto controllato ovunque. Potresti voler controllare tutto per sicurezza, ad eccezione di un certo percorso caldo che necessita di tutti i cicli della CPU che può ottenere.
V0ldek,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.