확인되지 않은 uint에 대한 C # 오버 플로우 동작


10

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

.NET 4.7.2로 컴파일하면

859091763

7

그러나 Roslyn 또는 .NET Core를 수행하면

859091763

0

왜 이런 일이 발생합니까?


캐스트 대상 ulong은 후자의 경우 무시되므로 float-> int변환 에서 발생합니다 .
madreflection

나는 행동의 변화에 ​​더 놀랐다. 그것은 꽤 큰 차이처럼 보인다. 나는 "0"이 그 캐스트 체인 tbh에서도 유효한 대답이라고 기대하지 않을 것입니다.
루카스

이해할 수 있는. 스펙의 일부는 Roslyn을 빌드 할 때 컴파일러에서 수정되었으므로 그 일부가 될 수 있습니다. SharpLab 에서이 버전 의 JIT 출력을 확인하십시오 . 이것은 캐스트가 ulong결과에 어떻게 영향 을 미치는지를 보여줍니다 .
madreflection

그것은 dotnetfiddle에 귀하의 예를 들어 다시로, 매혹적인의 마지막를 WriteLine은 .NET 코어 3.1 로슬린 3.4 0에서 7 출력
루카스

또한 내 데스크탑에서도 확인했습니다. JIT 코드는 전혀 자세히 보지 않으며 .NET Core와 .NET Framework간에 다른 결과를 얻습니다. 트리 피
루카스

답변:


1

나의 결론은 틀렸다. 자세한 내용은 업데이트를 참조하십시오.

사용한 첫 번째 컴파일러의 버그처럼 보입니다. 이 경우 0이 올바른 결과입니다 . C # 사양에 의해 지시 된 작업 순서는 다음과 같습니다.

  1. 곱셈 scale에 의해 scale산출a
  2. 수행 a + 7, 항복b
  3. 에 캐스팅 b하여 ulong산출c
  4. 에 캐스팅 c하여 uint산출d

처음 두 연산은 부동 소수점 값을 남겨 둡니다 b = 4.2949673E+09f. 표준 부동 소수점 산술에서 이것은 4294967296( 여기에서 확인할 수 있습니다 ). 에 맞는지 ulong잘 때문에 c = 4294967296,하지만 정확히 하나의 이상이다 uint.MaxValue에 그것 때문에 왕복, 0따라서 d = 0. 부동 소수점 연산은 펑키 이후 지금 깜짝 놀람, 4.2949673E+09f4.2949673E+09f + 7IEEE 754에서 동일한 번호 그래서는 scale * scale당신에게 동일한 값을 줄 것 float등을 scale * scale + 7, a = b두 번째 작업은 기본적으로 어떠한 조합 없도록.

Roslyn 컴파일러는 컴파일 타임에 (일부) const 연산을 수행하고이 전체 표현식을로 최적화합니다 0. 다시 말하지만, 그것은 올바른 결과 이며 , 컴파일러는 최적화없이 코드와 동일한 동작을 수행하는 최적화를 수행 할 수 있습니다.

생각 에 사용하는 .NET 4.7.2 컴파일러도이를 최적화하려고 시도하지만 잘못된 위치에서 캐스트를 평가하는 버그가 있습니다. 먼저 캐스팅 경우 당연히 scaleuint다음 작업을 수행, 당신은 얻을 수 7있기 때문에, scale * scale에 왕복 0하고 다음 추가 7. 그러나 이는 런타임에 식을 단계별로 평가할 때 얻게되는 결과와 일치하지 않습니다 . 다시 말하지만, 근본 원인은 생성 된 동작을 볼 때 추측 일 뿐이지 만 위에서 언급 한 모든 것을 감안할 때 이것이 첫 번째 컴파일러 측면의 사양 위반이라고 확신합니다.

최신 정보:

나는 바보를했다. 위의 답변을 쓸 때 알지 못하는 C # 사양 의이 비트 가 있습니다 .

부동 소수점 연산은 연산의 결과 유형보다 높은 정밀도로 수행 될 수 있습니다. 예를 들어, 일부 하드웨어 아키텍처는 더블 유형보다 범위와 정밀도가 큰 "확장"또는 "긴 더블"부동 소수점 유형을 지원하며이 고정밀 유형을 사용하여 모든 부동 소수점 연산을 암시 적으로 수행합니다. 과도한 하드웨어 비용으로 정밀도가 떨어지는 부동 소수점 연산을 수행 할 수 있으며 성능과 정밀도를 모두 상실하는 구현이 아니라 C #을 통해 모든 부동 소수점 연산에 고정밀 유형을 사용할 수 있습니다. . 보다 정확한 결과를 제공하는 것 외에는 측정 가능한 효과가 거의 없습니다. 그러나 x * y / z 형식의 표현식에서

C # 은 최소한 IEEE 754 수준 에서 정확한 수준을 제공하는 작업을 보장 하지만 반드시 정확한 것은 아닙니다 . 버그가 아니며 사양 기능입니다. Roslyn 컴파일러는 IEEE 754가 지정한대로 정확하게 식을 평가할 권리가 있으며, 다른 컴파일러는 2^32 + 77를 넣을 때 이를 추론 할 권리가 uint있습니다.

오해의 소지가있는 첫 번째 답변에 대해 유감스럽게 생각하지만 적어도 오늘 우리는 모두 무언가를 배웠습니다.


그런 다음 현재 .NET Framework 컴파일러에 버그가 있다고 생각합니다 (단지 VS 2019에서 확실하게 시도했습니다) :) 버그를 기록 할 수있는 곳이 있는지 확인하려고 노력하지만 아마도 원하지 않는 부작용이 많을 수도 있고 무시 될 수도 있습니다.
Lukas

나는 그것이 조기에 int로 캐스팅한다고 생각하지 않습니다. 많은 경우에 훨씬 더 명확한 문제가 발생했을 것입니다. 여기서는 const 연산에서 값을 평가하지 않고 마지막까지 캐스팅하지 않는 것입니다. 중간 값을 float로 저장하는 대신이를 건너 뛰고 각 표현식에서 표현식 자체로 바꾸는 것입니다.
jalsh

@ jalsh 나는 당신의 추측을 이해하지 못하는 것 같아요. 컴파일러가 단순히 각각 scale을 float 값으로 바꾸고 런타임에 다른 모든 것을 평가하면 결과는 동일합니다. 정교하게 할 수 있습니까?
V0ldek

@ V0ldek, downvote는 실수였습니다. 나는 그것을 제거 할 수 있도록 귀하의 답변을 편집했습니다 :)
jalsh

내 생각에 그것은 실제로 중간 값을 float로 저장하지 않았다는 것입니다. f를 f로 ​​변환하지 않고 f를 평가하는 표현식으로 f를 대체했습니다.
jalsh

0

여기에서 요점은 ( 문서에서 볼 수 있듯이 ) float 값은 최대 2 ^ 24 까지만 가질 수 있다는 것 입니다. 따라서 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ) 값을 할당하면 실제로 2 ^ 24 * 2 ^ 도 84294967000 이다. 7 을 추가하면 ulong 으로 변환하여 잘린 부분에만 추가됩니다 .

밑이 2 ^ 53double로 변경 하면 원하는대로 작동합니다.

이는 런타임 문제 일 수 있지만이 경우 모든 값이 상수이고 컴파일러가 평가하므로 컴파일 타임 문제입니다.


-2

우선 확인되지 않은 컨텍스트를 사용하고 있습니다.이 컴파일러는 개발자에게 결과가 오버플로 유형이 아니며 컴파일 오류가 없음을 확신합니다. 귀하의 시나리오에서는 실제로 오버 플로우 유형을 목표로하고 3 개의 다른 컴파일러에서 일관된 동작을 기대합니다.이 중 하나는 새로운 Roslyn 및 .NET Core와 비교하여 이전 버전과 완전히 호환됩니다.

두 번째는 암시 적 변환과 명시 적 변환을 혼합하는 것입니다. Roslyn 컴파일러는 확실하지 않지만 .NET Framework 및 .NET Core 컴파일러는 해당 작업에 대해 다른 최적화를 사용할 수 있습니다.

여기서 문제는 코드의 첫 번째 줄은 부동 소수점 값 / 유형 만 사용하지만 두 번째 줄은 부동 소수점 값 / 유형과 정수 값 / 유형의 조합입니다.

정수 부동 소수점 유형을 바로 만들 경우 (7> 7.0) 세 가지 컴파일 된 소스 모두에 대해 매우 동일한 결과를 얻을 수 있습니다.

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

따라서 V0ldek이 대답 한 것과는 반대로 "버그 (실제로 버그 인 경우)는 Roslyn 및 .NET Core 컴파일러에서 가장 가능성이 높습니다"입니다.

이를 확인해야 할 또 다른 이유는 첫 번째 확인되지 않은 계산 결과가 모두 동일하며 UInt32유형의 최대 값을 초과하는 값입니다 .

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

빼기 1은 0부터 시작하여 빼기 어려운 값입니다. 오버플로에 대한 수학 이해가 정확하면 최대 값 다음에 다음 숫자부터 시작합니다.

최신 정보

jalsh 의견에 따르면

7.0은 float가 아닌 double입니다. 7.0f를 사용해보십시오. 여전히 0을 줄 것입니다.

그의 의견은 정확하다. float를 사용하는 경우 Roslyn 및 .NET Core에 대해 여전히 0을 얻지 만 다른 한편으로는 double을 사용하면 7이됩니다.

나는 몇 가지 추가 테스트를했고 상황이 더욱 이상해졌지만 결국 모든 것이 의미가 있습니다 (적어도 조금).

내가 생각하는 것은 .NET Framework 4.7.2 컴파일러 (2018 년 중반에 릴리스 됨)는 실제로 .NET Core 3.1 및 Roslyn 3.4 컴파일러 (2019 년 말에 릴리스 됨)와 다른 최적화를 사용한다는 것입니다. 이러한 다른 최적화 / 계산은 컴파일 타임에 알려진 상수 값에 순수하게 사용됩니다. unchecked컴파일러가 이미 오버플로가 발생한다는 것을 알고 있기 때문에 키워드 를 사용해야 할 필요가 있지만 최종 IL을 최적화하기 위해 다른 계산이 사용되었습니다.

IL_000a 명령어를 제외하고 동일한 소스 코드와 거의 동일한 IL. 한 컴파일러는 7과 다른 0을 계산합니다.

소스 코드

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 컴파일러 지점 (2019 년 9 월) 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

unchecked아래와 같이 상수가 아닌 표현식을 추가하면 올바른 방법으로 시작합니다 (기본적으로는 ).

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을 생성합니다.

.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 컴파일러 지점 (2019 년 9 월) 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

따라서 결국 다른 동작의 이유는 상수 식에 대해 다른 최적화 / 계산을 사용하는 다른 버전의 프레임 워크 및 / 또는 컴파일러 일뿐이지만 다른 경우 동작은 매우 동일합니다.


7.0은 실수가 아닌 더블입니다. 7.0f를 시도해보십시오. 3:18에 여전히 0
jalsh

예, 부동 소수점 유형이 아니라 부동 소수점 유형이어야합니다. 정정 주셔서 감사합니다.
dropoutcoder

그것은 문제의 전체 관점을 변경합니다. 배가되는 정밀도를 두 배로 처리 할 때 V0ldek의 답변 변경에 설명 된 결과가 크게 바뀌면 스케일을 두 배로 변경하고 다시 확인하면 결과가 동일합니다. ..
jalsh

결국 더 복잡한 문제입니다.
dropoutcoder

1
@jalsh 예, 그러나 체크 된 컨텍스트를 모든 곳에서 바꾸는 컴파일러 플래그가 있습니다. 얻을 수있는 모든 CPU주기를 필요로하는 특정 핫 경로를 제외하고 모든 것이 안전을 위해 점검되도록 할 수 있습니다.
V0ldek
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.