Comportamento de estouro de c # para uint não verificado

10

Eu tenho testado este código em 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 eu compilar com o .NET 4.7.2, recebo

859091763

7

Mas se eu fizer o Roslyn ou o .NET Core, recebo

859091763

0 0

Por que isso acontece?

Lukas
fonte
O elenco para ulongestá sendo ignorado no último caso, então está acontecendo na conversão float-> int.
madreflection 19/01
Estou mais surpreso com a mudança de comportamento, que parece ser uma grande diferença. Eu não esperaria que "0" fosse uma resposta válida com essa cadeia de elencos tbh.
Lukas
Compreensível. Várias coisas nas especificações foram corrigidas no compilador quando eles construíram o Roslyn, para que isso pudesse fazer parte dele. Confira a saída JIT nesta versão no SharpLab. Isso mostra como o elenco ulongafeta o resultado.
madreflection 20/01
É fascinante, com seu exemplo no dotnetfiddle, o último WriteLine produz 0 em Roslyn 3.4 e 7 no .NET Core 3.1
Lukas
Também confirmei na minha área de trabalho. O código JIT nem parece muito próximo, eu recebo resultados diferentes entre o .NET Core e o .NET Framework. Trippy
Lukas

Respostas:

1

Minhas conclusões estavam incorretas. Veja a atualização para mais detalhes.

Parece um bug no primeiro compilador que você usou. Zero é o resultado correto neste caso . A ordem das operações ditada pela especificação C # é a seguinte:

  1. multiplicar scalepor scale, produzindoa
  2. executar a + 7, produzindob
  3. lançado bpara ulong, produzindoc
  4. lançado cpara uint, produzindod

As duas primeiras operações deixam você com um valor flutuante de b = 4.2949673E+09f. Sob a aritmética padrão de ponto flutuante, é isso 4294967296( você pode vê-lo aqui ). Isso se encaixa ulongmuito bem, então c = 4294967296, mas é exatamente um a mais do que uint.MaxValue, portanto, viagens de ida e volta para 0, portanto d = 0. Agora, surpresa surpresa, já que aritmética de ponto flutuante é funky, 4.2949673E+09fe 4.2949673E+09f + 7é exatamente o mesmo número em IEEE 754. Então scale * scalelhe dará o mesmo valor de um float, como scale * scale + 7, a = b, então o segundo operações é basicamente um não-op.

O compilador Roslyn executa (algumas) operações const em tempo de compilação e otimiza toda essa expressão para 0. Novamente, esse é o resultado correto , e o compilador pode executar qualquer otimização que resultará no mesmo comportamento exato do código sem elas.

Meu palpite é que o compilador .NET 4.7.2 que você usou também tenta otimizar isso, mas tem um erro que faz com que ele avalie a conversão em um lugar errado. Naturalmente, se você primeiro converter scaleem uinte depois executar a operação, obterá 7, porque scale * scaleida e volta 0e depois adiciona 7. Mas isso é inconsistente com o resultado que você obteria ao avaliar as expressões passo a passo no tempo de execução . Novamente, a causa raiz é apenas um palpite ao analisar o comportamento produzido, mas, considerando tudo o que afirmei acima, estou convencido de que essa é uma violação de especificação do lado do primeiro compilador.

ATUALIZAR:

Eu fiz uma brincadeira. Existe um pouco da especificação C # que eu não sabia que existia ao escrever a resposta acima:

As operações de ponto flutuante podem ser executadas com maior precisão do que o tipo de resultado da operação. Por exemplo, algumas arquiteturas de hardware suportam um tipo de ponto flutuante "estendido" ou "duplo longo" com maior alcance e precisão do que o tipo duplo e executam implicitamente todas as operações de ponto flutuante usando esse tipo de precisão mais alta. Somente com um custo excessivo no desempenho, essas arquiteturas de hardware podem ser feitas para executar operações de ponto flutuante com menos precisão e, em vez de exigir uma implementação para perder desempenho e precisão, o C # permite que um tipo de precisão mais alta seja usado para todas as operações de ponto flutuante . Além de fornecer resultados mais precisos, isso raramente tem efeitos mensuráveis. No entanto, nas expressões do formato x * y / z,

O C # garante que as operações forneçam um nível de precisão pelo menos no nível da IEEE 754, mas não necessariamente exatamente isso. Não é um bug, é um recurso de especificação. O compilador Roslyn está em seu direito de avaliar a expressão exatamente como IEEE 754 especifica, eo outro compilador está em seu direito de deduzir que 2^32 + 7é 7quando colocado em uint.

Sinto muito pela minha primeira resposta enganosa, mas pelo menos todos aprendemos algo hoje.

V0ldek
fonte
Então, acho que temos um bug no compilador atual do .NET Framework (tentei no VS 2019 apenas para ter certeza) :) Acho que vou tentar ver se há algum lugar para registrar um bug, embora consertar algo assim provavelmente tem muitos efeitos colaterais indesejados e provavelmente será ignorado ...
Lukas
Eu não acho que seja prematuramente convertido para int, o que teria causado problemas muito mais claros em MUITOS casos, acho que o caso aqui é que, na operação const, não está avaliando o valor e convertendo-o até o último, ou seja, é que, em vez de armazenar os valores intermediários em carros alegóricos, é apenas pular isso e substituí-lo em cada expressão pela própria expressão
jalsh 20/01
@ Jalsh Acho que não entendo seu palpite. Se o compilador simplesmente substituísse cada um scalepelo valor flutuante e avaliasse todo o resto em tempo de execução, o resultado seria o mesmo. Você pode elaborar?
V0ldek 20/01
@ V0ldek, o voto negativo foi um erro, editei sua resposta para que eu pudesse removê-la :)
jalsh 20/01
meu palpite é que ele realmente não armazenou os valores intermediários em floats, apenas substituiu f pela expressão que avalia f sem lançá-lo para flutuar
jalsh 20/01
0

O ponto aqui é (como você pode ver nos documentos ) que os valores flutuantes podem ter apenas uma base de até 2 ^ 24 . Portanto, quando você atribui um valor de 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ), ele se torna, na verdade, 2 ^ 24 * 2 ^ 8 , que é 4294967000 . Adicionar 7 só será adicionado à parte truncada pela conversão para ulong .

Se você mudar para o dobro , que tem uma base de 2 ^ 53 , funcionará para o que você deseja.

Isso pode ser um problema de tempo de execução, mas, nesse caso, é um problema de tempo de compilação, porque todos os valores são constantes e serão avaliados pelo compilador.

Paulo Morgado
fonte
-2

Antes de tudo, você está usando um contexto não verificado, que é uma instrução para o compilador. Você tem certeza, como desenvolvedor, de que o resultado não excederá o tipo e que você não verá nenhum erro de compilação. No seu cenário, você realmente está transbordando de propósito e espera um comportamento consistente em três compiladores diferentes, um dos quais provavelmente é compatível com versões anteriores ao histórico em comparação com o Roslyn e o .NET Core, que são novos.

A segunda coisa é que você está misturando conversões implícitas e explícitas. Não tenho certeza sobre o compilador Roslyn, mas definitivamente os compiladores .NET Framework e .NET Core podem usar otimizações diferentes para essas operações.

O problema aqui é que a primeira linha do seu código usa apenas valores / tipos de ponto flutuante, mas a segunda linha é uma combinação de valores / tipos de ponto flutuante e valor / tipo integral.

Caso você faça um tipo de ponto flutuante inteiro imediatamente (7> 7.0), obterá o mesmo resultado para todas as três fontes 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
    }
}

Então, eu diria o contrário do que o V0ldek respondeu e que é "O bug (se realmente é um bug) é mais provável nos compiladores Roslyn e .NET Core".

Outro motivo para acreditar que é o resultado dos primeiros resultados de computação não verificados são iguais para todos e é o valor que excede o valor máximo do UInt32tipo.

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

Menos um está lá quando começamos do zero, que é um valor difícil de subtrair. Se meu entendimento matemático de estouro estiver correto, começaremos do próximo número após o valor máximo.

ATUALIZAR

De acordo com o comentário jalsh

7.0 é um dobro, não um flutuador, tente 7.0f, ele ainda dará um 0

O comentário dele está correto. No caso de usarmos float, você ainda recebe 0 para Roslyn e .NET Core, mas, por outro lado, usa resultados duplos em 7.

Fiz alguns testes adicionais e as coisas ficaram ainda mais estranhas, mas no final tudo faz sentido (pelo menos um pouco).

O que eu assumo é que o compilador .NET Framework 4.7.2 (lançado em meados de 2018) realmente usa otimizações diferentes dos compiladores .NET Core 3.1 e Roslyn 3.4 (lançado no final de 2019). Essas diferentes otimizações / cálculos são puramente usadas para valores constantes conhecidos em tempo de compilação. É por isso que houve a necessidade de usar a uncheckedpalavra-chave como compilador já sabe que está ocorrendo um estouro, mas computações diferentes foram usadas para otimizar a IL final.

O mesmo código fonte e quase o mesmo IL, exceto a instrução IL_000a. Um compilador calcula 7 e outro 0.

Código fonte

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

IL do .NET Framework (x64)

.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

Filial do compilador Roslyn (set 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

Começa a seguir o caminho certo quando você adiciona expressões não constantes (por padrão são unchecked) como abaixo.

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

O que gera "exatamente" a mesma IL pelos dois compiladores.

IL do .NET Framework (x64)

.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

Filial do compilador Roslyn (set 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

Portanto, no final, acredito que a razão para um comportamento diferente é apenas uma versão diferente da estrutura e / ou compilador que usa otimizações / computação diferentes para expressões constantes, mas em outros casos o comportamento é o mesmo.

dropoutcoder
fonte
7.0 é um dobro, não é um flutuador, tente 7.0f, ele ainda dará um 0
jalsh 20/01
Sim, deve ser do tipo ponto flutuante, não flutuante. Obrigado pela correção.
dropoutcoder 20/01
Isso muda toda a perspectiva do problema. Quando se lida com o dobro da precisão obtida, é muito maior e o resultado explicado na resposta do V0ldek muda drasticamente. Você pode simplesmente mudar de escala para dobrar e verificar novamente, os resultados serão os mesmos. ..
jalsh 20/01
No final, é uma questão mais complexa.
dropoutcoder 20/01
11
@ jalsh Sim, mas há um sinalizador de compilador que transforma o contexto verificado em todos os lugares. Você pode querer verificar tudo quanto à segurança, exceto um determinado caminho quente que precisa de todos os ciclos de CPU que pode obter.
V0ldek 20/01