Comportement de débordement C # pour uint non contrôlé

10

J'ai testé ce code sur 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)));
    }
}

Si je compile avec .NET 4.7.2, j'obtiens

859091763

7

Mais si je fais Roslyn ou .NET Core, je reçois

859091763

0

Pourquoi cela arrive-t-il?

Lukas
la source
La conversion en ulongest ignorée dans ce dernier cas, donc cela se produit dans la conversion float-> int.
madreflection
Je suis plus surpris par le changement de comportement, cela semble être une assez grosse différence. Je ne m'attendrais pas à ce que "0" soit une réponse valide non plus avec cette chaîne de transtypages tbh.
Lukas
Compréhensible. Plusieurs choses dans la spécification ont été corrigées dans le compilateur lors de la construction de Roslyn, ce qui pourrait en faire partie. Découvrez la sortie JIT sur cette version sur SharpLab. Cela montre comment la conversion en ulongaffecte le résultat.
madreflection
C'est fascinant, avec votre exemple sur dotnetfiddle, le dernier WriteLine affiche 0 dans Roslyn 3.4 et 7 sur .NET Core 3.1
Lukas
J'ai également confirmé sur mon bureau. Le code JIT ne semble même pas du tout proche, j'obtiens des résultats différents entre .NET Core et .NET Framework. Trippy
Lukas

Réponses:

1

Mes conclusions étaient incorrectes. Voir la mise à jour pour plus de détails.

Ressemble à un bogue dans le premier compilateur que vous avez utilisé. Zéro est le résultat correct dans ce cas . L'ordre des opérations dicté par la spécification C # est le suivant:

  1. multiplier scalepar scale, donnanta
  2. effectuer a + 7, céderb
  3. coulé bsur ulong, donnantc
  4. coulé csur uint, donnantd

Les deux premières opérations vous laissent avec une valeur flottante de b = 4.2949673E+09f. Sous l'arithmétique à virgule flottante standard, c'est 4294967296( vous pouvez le vérifier ici ). Cela s'intègre ulongtrès bien, donc c = 4294967296, mais c'est exactement un de plus uint.MaxValue, donc il y a des allers-retours 0, par conséquent d = 0. Maintenant, surprise surprise, puisque l' arithmétique à virgule flottante est génial, 4.2949673E+09fet 4.2949673E+09f + 7est exactement le même nombre dans la norme IEEE 754. Alors scale * scalevous donnera la même valeur d'un floatque scale * scale + 7, a = b, de sorte que les deuxièmes opérations est essentiellement un no-op.

Le compilateur Roslyn effectue (certaines) opérations const au moment de la compilation et optimise cette expression entière pour 0. Encore une fois, c'est le résultat correct , et le compilateur est autorisé à effectuer toutes les optimisations qui entraîneront le même comportement exact que le code sans elles.

Je suppose que le compilateur .NET 4.7.2 que vous avez utilisé essaie également d'optimiser cela, mais a un bogue qui lui fait évaluer la distribution au mauvais endroit. Naturellement, si vous effectuez d'abord un cast scalevers un uint, puis effectuez l'opération, vous obtenez 7, car des scale * scaleallers-retours vers 0et ensuite vous ajoutez 7. Mais cela n'est pas cohérent avec le résultat que vous obtiendriez lors de l'évaluation des expressions étape par étape lors de l'exécution . Encore une fois, la cause première est juste une supposition en regardant le comportement produit, mais étant donné tout ce que j'ai dit ci-dessus, je suis convaincu que c'est une violation des spécifications du côté du premier compilateur.

MISE À JOUR:

J'ai fait une gaffe. Il y a ce morceau de la spécification C # que je ne connaissais pas lors de l'écriture de la réponse ci-dessus:

Les opérations en virgule flottante peuvent être effectuées avec une précision supérieure au type de résultat de l'opération. Par exemple, certaines architectures matérielles prennent en charge un type à virgule flottante "étendu" ou "long double" avec une portée et une précision supérieures à celles du type double, et effectuent implicitement toutes les opérations à virgule flottante à l'aide de ce type de précision supérieure. Ce n'est qu'à un coût excessif en performances que ces architectures matérielles peuvent être réalisées pour effectuer des opérations en virgule flottante avec moins de précision, et plutôt que d'exiger une implémentation pour perdre à la fois les performances et la précision, C # permet d'utiliser un type de précision plus élevée pour toutes les opérations en virgule flottante . En plus de fournir des résultats plus précis, cela a rarement des effets mesurables. Cependant, dans les expressions de la forme x * y / z,

C # garantit des opérations pour fournir un niveau de précision au moins au niveau de l'IEEE 754, mais pas nécessairement exactement cela. Ce n'est pas un bug, c'est une fonctionnalité spécifique. Le compilateur Roslyn est en droit d'évaluer l'expression exactement comme le spécifie IEEE 754, et l'autre compilateur est en droit de déduire que 2^32 + 7c'est 7lorsqu'il est placé dans uint.

Je suis désolé pour ma première réponse trompeuse, mais au moins nous avons tous appris quelque chose aujourd'hui.

V0ldek
la source
Ensuite, je suppose que nous avons un bogue dans le compilateur .NET Framework actuel (j'ai juste essayé dans VS 2019 juste pour être sûr) :) Je suppose que j'essaierai de voir s'il y a un endroit où enregistrer un bogue, bien que corriger quelque chose comme ça ont probablement beaucoup d'effets secondaires indésirables et sont probablement ignorés ...
Lukas
Je ne pense pas que ce soit prématurément casté en int, cela aurait causé des problèmes beaucoup plus clairs dans BEAUCOUP de cas, je suppose que le cas ici est que dans l'opération const, il n'évalue pas la valeur et ne la jette que jusqu'au dernier, ce qui signifie est qu'au lieu de stocker les valeurs intermédiaires dans des flottants, il suffit de sauter cela et de le remplacer dans chaque expression par l'expression elle
jalsh
@jalsh Je ne pense pas comprendre votre supposition. Si le compilateur remplaçait simplement chacun scalepar la valeur flottante et évaluait ensuite tout le reste à l'exécution, le résultat serait le même. Peux-tu élaborer?
V0ldek
@ V0ldek, le downvote était une erreur, j'ai modifié votre réponse pour que je puisse la supprimer :)
jalsh
je suppose qu'il n'a pas réellement stocké les valeurs intermédiaires dans des flottants, il a juste remplacé f par l'expression qui évalue f sans le lancer pour flotter
jalsh
0

Le point ici est (comme vous pouvez le voir sur les documents ) que les valeurs flottantes ne peuvent avoir qu'une base jusqu'à 2 ^ 24 . Ainsi, lorsque vous affectez une valeur de 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ), cela devient en fait 2 ^ 24 * 2 ^ 8 , qui est 4294967000 . Ajouter 7 ne fera qu'ajouter à la partie tronquée par conversion en ulong .

Si vous changez en double , qui a une base de 2 ^ 53 , cela fonctionnera pour ce que vous voulez.

Cela peut être un problème au moment de l'exécution mais, dans ce cas, c'est un problème au moment de la compilation, car toutes les valeurs sont des constantes et seront évaluées par le compilateur.

Paulo Morgado
la source
-2

Tout d'abord, vous utilisez un contexte non contrôlé qui est une instruction pour le compilateur, vous êtes sûr, en tant que développeur, que le résultat ne débordera pas de type et vous ne souhaitez voir aucune erreur de compilation. Dans votre scénario, vous êtes en fait de type débordant et attendez-vous à un comportement cohérent entre trois compilateurs différents, dont l'un est probablement rétrocompatible loin de l'historique par rapport à Roslyn et .NET Core qui sont nouveaux.

La deuxième chose est que vous mélangez des conversions implicites et explicites. Je ne suis pas sûr du compilateur Roslyn, mais les compilateurs .NET Framework et .NET Core peuvent certainement utiliser des optimisations différentes pour ces opérations.

Le problème ici est que la première ligne de votre code utilise uniquement des valeurs / types à virgule flottante, mais la deuxième ligne est une combinaison de valeurs / types à virgule flottante et de valeur / type intégrale.

Si vous créez immédiatement un type à virgule flottante entier (7> 7.0), vous obtiendrez le même résultat pour les trois sources compilées.

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

Donc, je dirais à l'opposé de ce que V0ldek a répondu et c'est "Le bogue (s'il s'agit vraiment d'un bogue) est très probablement dans les compilateurs Roslyn et .NET Core".

Une autre raison de croire que le résultat du premier calcul non contrôlé est le même pour tous et que la valeur dépasse la valeur maximale de UInt32type.

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

Moins un est là car nous partons de zéro, ce qui est une valeur difficile à soustraire. Si ma compréhension mathématique du débordement est correcte, nous commençons par le nombre suivant après la valeur maximale.

MISE À JOUR

Selon le commentaire de Jalsh

7.0 est un double, pas un flotteur, essayez 7.0f, il vous donnera toujours un 0

Son commentaire est correct. Dans le cas où nous utilisons float, vous obtenez toujours 0 pour Roslyn et .NET Core, mais en revanche, en utilisant des résultats doubles en 7.

J'ai fait des tests supplémentaires et les choses deviennent encore plus bizarres, mais au final tout a du sens (au moins un peu).

Ce que je suppose, c'est que le compilateur .NET Framework 4.7.2 (publié mi-2018) utilise vraiment des optimisations différentes de celles des compilateurs .NET Core 3.1 et Roslyn 3.4 (publiées fin 2019). Ces différentes optimisations / calculs sont purement utilisés pour des valeurs constantes connues au moment de la compilation. C’est pourquoi il fallait utiliserunchecked mots clés car le compilateur sait déjà qu'il y a un débordement, mais différents calculs ont été utilisés pour optimiser l'IL final.

Même code source et presque même IL sauf instruction IL_000a. Un compilateur calcule 7 et les autres 0.

Code source

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

Branche du compilateur Roslyn (sept. 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

Cela commence à aller dans le bon sens lorsque vous ajoutez des expressions non constantes (par défaut sont unchecked) comme ci-dessous.

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

Ce qui génère "exactement" le même IL par les deux compilateurs.

.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

Branche du compilateur Roslyn (sept. 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

Donc, à la fin, je crois que la raison d'un comportement différent est simplement une version différente du framework et / ou du compilateur qui utilise différentes optimisations / calculs pour des expressions constantes, mais dans d'autres cas, le comportement est très identique.

dropoutcoder
la source
7.0 est un double, pas un flotteur, essayez 7.0f, il vous donnera toujours un 0
jalsh
Oui, il doit être de type virgule flottante, et non flottant. Merci pour la correction.
dropoutcoder
Cela change toute la perspective du problème, lorsque vous traitez avec un double de la précision que vous obtenez est beaucoup plus élevée et le résultat expliqué dans la réponse de V0ldek change radicalement, vous pouvez plutôt changer l'échelle pour doubler et vérifier à nouveau, les résultats seraient les mêmes. ..
jalsh
En fin de compte, c'est une question plus complexe.
dropoutcoder
1
@jalsh Oui, mais il existe un indicateur de compilation qui transforme le contexte vérifié partout. Vous voudrez peut-être que tout soit vérifié pour la sécurité, à l'exception d'un certain chemin chaud qui a besoin de tous les cycles CPU qu'il peut obtenir.
V0ldek