未经检查的 uint 的 C# 溢出行为

C# overflow behavior for unchecked uint

我一直在 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

为什么会这样?

我的结论是错误的。查看更新了解更多详情。

看起来像是您使用的第一个编译器中的错误。 在这种情况下,零是正确的结果 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 + 7 完全一样 IEEE 754 中的编号。因此 scale * scale 将为您提供相同的值 floatscale * scale + 7a = b,所以第二个操作基本上是空操作。

Roslyn 编译器在编译时执行(一些)const 操作,并将整个表达式优化为 0同样,这是正确的结果,并且允许编译器执行任何优化,这些优化将导致与没有它们的代码完全相同的行为。

我的 猜测 是您使用的 .NET 4.7.2 编译器也试图对此进行优化,但有一个错误导致它在错误的位置评估转换。当然,如果您首先将 scale 转换为 uint 然后执行操作,您会得到 7,因为 scale * scale 往返于 0 然后您添加 7。但是这与在运行时逐步计算表达式时得到的结果不一致。同样,根本原因只是在查看生成的行为时的猜测,但考虑到我上面所说的一切,我确信这是第一个编译器方面的规范违规。

更新:

我搞砸了。有 this bit of the C# specification 是我在写上面的答案时不知道的:

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an "extended" or "long double" floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations with less precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations. Other than delivering more precise results, this rarely has any measurable effects. However, in expressions of the form x * y / z, where the multiplication produces a result that is outside the double range, but the subsequent division brings the temporary result back into the double range, the fact that the expression is evaluated in a higher range format may cause a finite result to be produced instead of an infinity.

C# 保证操作提供的精度级别至少 达到 IEEE 754 级别,但不一定完全。这不是错误,这是规范功能。 Roslyn 编译器有权完全按照 IEEE 754 的规定评估表达式,而另一个编译器有权在将 uint 放入 2^32 + 7 时推断出 7

对于我误导的第一个回答,我深表歉意,但至少我们今天都学到了一些东西。

首先,您使用的是未经检查的上下文,这是针对编译器的指令,作为开发人员,您确信结果不会溢出类型,并且您希望看到没有编译错误。在您的场景中,您实际上是故意溢出类型并期望在三个不同的编译器之间保持一致的行为,与新的 Roslyn 和 .NET Core 相比,其中一个可能向后兼容远远超过历史。

第二件事是您混合了隐式和显式转换。我不确定 Roslyn 编译器,但 .NET Framework 和 .NET Core 编译器肯定会对这些操作使用不同的优化。

这里的问题是你的代码第一行只使用浮点数values/types,但第二行是浮点数values/types和整数value/type.[=22的组合=]

如果您直接创建整数浮点类型 (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 的回答相反,那就是 "The bug (if it really is a bug) is most likely in Roslyn and .NET Core compilers"。

相信的另一个原因是第一个未经检查的计算结果对所有人都是相同的,并且它是溢出 UInt32 类型的最大值的值。

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

减一是因为我们从零开始,这是一个很难减去自己的值。如果我对溢出的数学理解是正确的,我们从最大值后的下一个数字开始。

更新

根据 jalsh 评论

7.0 is a double, not a float, try 7.0f, it'll still give you a 0

他的评论是正确的。如果我们使用 float,Roslyn 和 .NET Core 仍然会得到 0,但另一方面,在 7 中使用双精度结果。

我做了一些额外的测试,事情变得更奇怪了,但最后一切都有意义(至少有一点)。

我假设 .NET Framework 4.7.2 编译器(2018 年年中发布)确实使用了与 .NET Core 3.1 和 Roslyn 3.4 编译器(2019 年底发布)不同的优化。这些不同的 optimizations/computations 纯粹用于编译时已知的常量值。这就是为什么需要使用 unchecked 关键字的原因,因为编译器已经知道发生了溢出,但使用了不同的计算来优化最终 IL。

相同的源代码和几乎相同的 IL,除了 IL_000a 指令。一个编译器计算 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
    }
}

两个编译器生成 "exactly" 相同的 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

所以,最后我认为不同行为的原因只是不同版本的框架 and/or 编译器对常量表达式使用不同的 optimizations/computation,但在其他情况下行为非常一样。

这里的要点是(正如您在 docs 上看到的那样)float 值的基数最多只能达到 2^24 。因此,当您分配值 2^32 (64 * 2014 * 164 * 1024 = 2^6 * 2^10 * 2^6 * 2^10 = 2^32) 实际上变成 2^24 * 2^8,也就是 4294967000。添加 7 只会添加到转换为 ulong.

的部分 t运行

如果您更改为 double,它的基数为 2^53,它将满足您的需求。

这可能是 运行 时的问题,但在本例中,它是编译时的问题,因为所有值都是常量,将由编译器计算。