Comportamiento de desbordamiento de C # para uint sin marcar

10

He estado probando este código en 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 compilo con .NET 4.7.2 obtengo

859091763

7 7

Pero si hago Roslyn o .NET Core, obtengo

859091763

0 0

¿Por qué pasó esto?

Lukas
fuente
La conversión a ulongse ignora en el último caso, por lo que sucede en la conversión float-> int.
madreflection
Estoy más sorprendido por el cambio de comportamiento, que parece una gran diferencia. Tampoco esperaría que "0" fuera una respuesta válida con esa cadena de lanzamientos tbh.
Lukas
Comprensible. Se corrigieron varias cosas en la especificación en el compilador cuando construyeron Roslyn, por lo que podría ser parte de eso. Consulte la salida JIT de esta versión en SharpLab. Eso muestra cómo el elenco ulongafecta el resultado.
madreflection
Es fascinante, con su ejemplo de vuelta en dotnetfiddle, el último WriteLine sale 0 en Roslyn 3.4 y 7 en .NET Core 3.1
Lukas
También lo confirmé en mi escritorio. El código JIT ni siquiera se parece en absoluto, obtengo resultados diferentes entre .NET Core y .NET Framework. Trippy
Lukas

Respuestas:

1

Mis conclusiones fueron incorrectas. Vea la actualización para más detalles.

Parece un error en el primer compilador que usaste. Cero es el resultado correcto en este caso . El orden de operaciones dictado por la especificación de C # es el siguiente:

  1. multiplicar scalepor scale, produciendoa
  2. realizar a + 7, cediendob
  3. emitido ba ulong, cediendoc
  4. emitido ca uint, cediendod

Las dos primeras operaciones te dejan con un valor flotante de b = 4.2949673E+09f. Bajo la aritmética de coma flotante estándar, esto es 4294967296( puede verificarlo aquí ). Eso encaja ulongbien, así que c = 4294967296, pero es exactamente uno más que uint.MaxValue, por lo que es de ida y vuelta a0 tanto d = 0. Ahora, sorpresa sorpresa, ya aritmética de punto flotante es muy de moda, 4.2949673E+09fy 4.2949673E+09f + 7es exactamente el mismo número en el estándar IEEE 754. Así que scale * scalele dan el mismo valor de una floatcomo scale * scale + 7, a = b, por lo que las segundas operaciones es básicamente un no-op.

El compilador de Roslyn realiza (algunas) operaciones constantes en tiempo de compilación y optimiza toda esta expresión para 0. Nuevamente, ese es el resultado correcto , y el compilador puede realizar cualquier optimización que dé como resultado el mismo comportamiento que el código sin ellos.

Mi conjetura es que el compilador .NET 4.7.2 ha utilizado también trata de optimizar esta distancia, pero tiene un error que hace que se evalúe el molde en un lugar equivocado. Naturalmente, si primero lanzas scalea una uinty luego realizas la operación, obtienes7 , porque scale * scalelos viajes de ida y vuelta 0añaden 7. Pero eso es inconsistente con el resultado que obtendría al evaluar las expresiones paso a paso en tiempo de ejecución . Nuevamente, la causa raíz es solo una suposición cuando se observa el comportamiento producido, pero dado todo lo que he dicho anteriormente, estoy convencido de que se trata de una violación de las especificaciones del lado del primer compilador.

ACTUALIZAR:

He hecho una tontería. Hay poco de la especificación de C # que no sabía que existía al escribir la respuesta anterior:

Las operaciones de punto flotante se pueden realizar con mayor precisión que el tipo de resultado de la operación. Por ejemplo, algunas arquitecturas de hardware admiten un tipo de punto flotante "extendido" o "doble largo" con mayor rango y precisión que el tipo doble, y realizan implícitamente todas las operaciones de punto flotante utilizando este tipo de mayor precisión. Solo a un costo excesivo en rendimiento se puede hacer que tales arquitecturas de hardware realicen operaciones de punto flotante con menos precisión, y en lugar de requerir una implementación para perder tanto el rendimiento como la precisión, C # permite que se use un tipo de mayor precisión para todas las operaciones de punto flotante . Aparte de entregar resultados más precisos, esto rara vez tiene efectos medibles. Sin embargo, en expresiones de la forma x * y / z,

C # garantiza operaciones para proporcionar un nivel de precisión al menos en el nivel de IEEE 754, pero no necesariamente eso exactamente . No es un error, es una característica específica. El compilador Roslyn está en su derecho de evaluar la expresión exactamente como IEEE 754 especifica, y el otro compilador está en su derecho a deducir que 2^32 + 7es 7cuando se pone en uint.

Lamento mi primera respuesta engañosa, pero al menos todos hemos aprendido algo hoy.

V0ldek
fuente
Entonces supongo que tenemos un error en el compilador actual de .NET Framework (solo lo intenté en VS 2019 solo para estar seguro) :) Creo que intentaré ver si hay algún lugar para registrar un error, aunque arreglar algo como eso probablemente tenga muchos efectos secundarios no deseados y probablemente sea ignorado ...
Lukas
No creo que sea una conversión prematura a int, eso habría causado problemas mucho más claros en MUCHOS casos, supongo que el caso aquí es que en la operación constante no está evaluando el valor y emitiéndolo hasta el final, lo que significa es que en lugar de almacenar los valores intermedios en flotantes, simplemente se salta eso y lo reemplaza en cada expresión con la expresión en sí misma
jalsh
@ jalsh No creo que entienda tu suposición. Si el compilador simplemente reemplazara cada uno scalecon el valor flotante y luego evaluara todo lo demás en tiempo de ejecución, el resultado sería el mismo. ¿Puedes elaborar?
V0ldek
@ V0ldek, el voto negativo fue un error, edité tu respuesta para poder eliminarla :)
jalsh
Supongo que en realidad no almacenó los valores intermedios en flotantes, simplemente reemplazó f con la expresión que evalúa f sin convertirlo en flotante
jalsh
0

El punto aquí es (como puede ver en los documentos ) que los valores flotantes solo pueden tener una base de hasta 2 ^ 24 . Entonces, cuando asigna un valor de 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ) se convierte, en realidad, en 2 ^ 24 * 2 ^ 8 , que es 4294967000 . Agregar 7 solo se agregará a la parte truncada por la conversión a ulong .

Si cambia a doble , que tiene una base de 2 ^ 53 , funcionará para lo que desee.

Esto podría ser un problema de tiempo de ejecución pero, en este caso, es un problema de tiempo de compilación, porque todos los valores son constantes y serán evaluados por el compilador.

Paulo Morgado
fuente
-2

En primer lugar, está utilizando un contexto no comprobado, que es una instrucción para el compilador, está seguro, como desarrollador, de que el resultado no se desbordará y no le gustaría ver ningún error de compilación. En su escenario, en realidad está desbordando el tipo y esperando un comportamiento consistente en tres compiladores diferentes, uno de los cuales probablemente sea compatible con versiones anteriores en comparación con Roslyn y .NET Core, que son nuevos.

Lo segundo es que estás mezclando conversiones implícitas y explícitas. No estoy seguro sobre el compilador de Roslyn, pero definitivamente .NET Framework y .NET Core pueden usar diferentes optimizaciones para esas operaciones.

El problema aquí es que la primera línea de su código usa solo valores / tipos de punto flotante, pero la segunda línea es una combinación de valores / tipos de punto flotante y tipo / valor integral.

En caso de que haga un tipo entero de coma flotante de inmediato (7> 7.0) obtendrá el mismo resultado para las tres fuentes compiladas.

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

Entonces, diría lo contrario de lo que respondió V0ldek y que es "El error (si realmente es un error) es muy probable en los compiladores Roslyn y .NET Core".

Otra razón para creer eso es que el resultado de los primeros resultados de cálculo no verificados son los mismos para todos y es un valor que desborda el valor máximo de UInt32tipo.

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

Menos uno está allí cuando comenzamos desde cero, que es un valor que es difícil de restar. Si mi comprensión matemática del desbordamiento es correcta, comenzamos desde el siguiente número después del valor máximo.

ACTUALIZAR

Según el comentario de jalsh

7.0 es un doble, no un flotador, prueba 7.0f, todavía te dará un 0

Su comentario es correcto. En caso de que usemos flotante, todavía obtendrá 0 para Roslyn y .NET Core, pero por otro lado, usará resultados dobles en 7.

Hice algunas pruebas adicionales y las cosas se ponen aún más extrañas, pero al final todo tiene sentido (al menos un poco).

Lo que supongo es que el compilador .NET Framework 4.7.2 (lanzado a mediados de 2018) realmente usa diferentes optimizaciones que los compiladores .NET Core 3.1 y Roslyn 3.4 (lanzado a fines de 2019). Estas diferentes optimizaciones / cálculos se utilizan exclusivamente para valores constantes conocidos en tiempo de compilación. Por eso era necesario usarunchecked palabra clave ya que el compilador ya sabe que está ocurriendo un desbordamiento, pero se utilizó un cálculo diferente para optimizar la IL final.

El mismo código fuente y casi la misma IL, excepto la instrucción IL_000a. Un compilador calcula 7 y otro 0.

Código fuente

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

Roslyn compilador branch (Sep 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

Comienza a ir por el camino correcto cuando agrega expresiones no constantes (por defecto son unchecked) como a continuación.

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

Lo que genera "exactamente" la misma IL por ambos compiladores.

.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

Roslyn compilador branch (Sep 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

Entonces, al final, creo que la razón de un comportamiento diferente es solo una versión diferente del framework y / o compilador que está usando diferentes optimizaciones / cálculos para expresiones constantes, pero en otros casos el comportamiento es muy similar.

codificador
fuente
7.0 es doble, no flotante, prueba 7.0f, todavía te dará un 0
jalsh
Sí, debe ser de tipo coma flotante, no flotante. Gracias por la corrección.
Dropoutcoder
Eso cambia toda la perspectiva del problema, cuando se trata de un doble, la precisión que obtienes es mucho mayor y el resultado explicado en la respuesta de V0ldek cambia drásticamente, podrías simplemente cambiar la escala al doble y verificar nuevamente, los resultados serían los mismos. ..
jalsh
Al final es un tema más complejo.
Dropoutcoder
1
@jalsh Sí, pero hay un indicador de compilador que convierte el contexto marcado en todas partes. Es posible que desee tener todo comprobado por seguridad, a excepción de un cierto camino caliente que necesita todos los ciclos de CPU que puede obtener.
V0ldek