10

I've been testing this code at 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)));
    }
}

If I compile with .NET 4.7.2 I get

859091763

7

But if I do Roslyn or .NET Core, I get

859091763

0

Why does this happen?

Community
  • 1
  • 1
Lukas
  • 103
  • 3
  • The cast to `ulong` is being ignored in the latter case so it's happening in the `float`->`int` conversion. – madreflection Jan 19 '20 at 23:57
  • I am more surprised by the change of behavior, that seems like a pretty big difference. I wouldn't expect "0" to be a valid answer either with that chain of casts tbh. – Lukas Jan 20 '20 at 00:05
  • Understandable. Several things in the spec were fixed in the compiler when they built Roslyn, so that could be part of it. Check out the JIT output on [this version](https://sharplab.io/#v2:EYLgHgbALANALiAhgZwLYwCYgNQB8ACATAIwCwAUBUQMwUDeFABM4wMYD2AdsnIwGYAbdol7JWiAQFNGAXkbRGAKkbEADISgBuCkxZFCu5g3ItTjAK6dWAC0msA1pIyGzxs+/5CR/WYzETpZX8pRmxGAHZtEw9TcyFOAHNGMF8ACjiuBIBKPiiY2IBLTl4AT2I08yK4LNSUsPCszWYAembGAFVkaXEeRjh2C3iElxjK4sYSwgqqmr5QiMaWtoBlewKABzYUXn7BzJGPA/d8YgBOVL5Go7MT87Ar6PzGW9Syh6fmF8n3jwBfF3+5EBQA=) on SharpLab. That shows how the cast to `ulong` affects the result. – madreflection Jan 20 '20 at 00:08
  • It's fascinating, with your example back on dotnetfiddle, the last WriteLine outputs 0 in Roslyn 3.4 and 7 on .NET Core 3.1 – Lukas Jan 20 '20 at 00:15
  • I also confirmed on my Desktop. The JIT code doesn't even look close at all, I do get different results between .NET Core and .NET Framework. Trippy – Lukas Jan 20 '20 at 00:29
  • @madreflection That's not equivalent code. In the "Use cast to ulong" you add `7` twice, once in the `f` expression and then again after the cast, which eliminates the couse of the bug - inacurracy of big numbers in fl. The cast to `ulong` actually does not affect the result, you can skip it just fine since the value is way, way below `ulong.MaxValue`. – V0ldek Jan 20 '20 at 00:31
  • @V0ldek The code that madreflection gave is actually still interesting, as in that it still shows varying behaviors across .NET runtimes. For all intent and purposes, I thought that the behavior of arithmetic overflows would be the same across .NET runtime as opposed to C++ which leaves a lot of undefined behavior. – Lukas Jan 20 '20 at 00:35
  • @V0ldek: Darn, you're right. I wasn't paying attention. And yet if you remove the addition from the first line, SharpLab gives the same result as with it. – madreflection Jan 20 '20 at 00:37
  • Comparison fiddles: [.Net Framework](https://dotnetfiddle.net/5q41RY) and [.Net Core](https://dotnetfiddle.net/qhPluU). The divergence actually happens in the evaluation of `unchecked((ulong)(scale* scale + 7))`, the final cast to `uint` simply reflects the divergence. – dbc Jan 20 '20 at 00:44
  • Plot twist: C++ answers with this: Hello World! , 4.29497e+09 , 4294967296 , 7 , 4294967295 – Lukas Jan 20 '20 at 00:46
  • @MicrosoftCorp... care to comment? – jalsh Jan 20 '20 at 02:05
  • Actually, it doesn't have to be in an unchecked context to cause this kinda behavior see: [.net framework 4.7](https://dotnetfiddle.net/uOPODc) vs [.net Core 3.1](https://dotnetfiddle.net/M2xjip) – jalsh Jan 20 '20 at 04:07
  • My original answer was incorrect. Check the update for rectification. – V0ldek Jan 20 '20 at 08:22

3 Answers3

2

My conclusions were incorrect. See the update for more details.

Looks like a bug in the first compiler you used. Zero is the correct result in this case. The order of operations dictated by the C# specification is as follows:

  1. multiply scale by scale, yielding a
  2. perform a + 7, yielding b
  3. cast b to ulong, yielding c
  4. cast c to uint, yielding d

The first two operations leave you with a float value of b = 4.2949673E+09f. Under standard floating-point arithmetic, this is 4294967296 (you can check it here). That fits into ulong just fine, so c = 4294967296, but it's exactly one more than uint.MaxValue, so it round-trips to 0, hence d = 0. Now, surprise surprise, since floating-point arithmetic is funky, 4.2949673E+09f and 4.2949673E+09f + 7 is the exact same number in IEEE 754. So scale * scale will give you the same value of a float as scale * scale + 7, a = b, so the second operations is basically a no-op.

The Roslyn compiler performs (some) const operations at compile-time, and optimises this entire expression to 0. Again, that's the correct result, and the compiler is allowed to perform any optimisations that will result in the exact same behaviour as the code without them.

My guess is that the .NET 4.7.2 compiler you used also tries to optimise this away, but has a bug that causes it to evaluate the cast in a wrong place. Naturally, if you first cast scale to an uint and then perform the operation, you get 7, because scale * scale round-trips to 0 and then you add 7. But that is inconsistent with the result you would get when evaluating the expressions step-by-step at runtime. Again, the root cause is just a guess when looking at the produced behaviour, but given everything I've stated above I'm convinced this is a spec violation on the side of the first compiler.

UPDATE:

I have done a goof. There's this bit of the C# specification that I didn't know existed when writing the above answer:

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# guarantees operations to provide a level of precision at least on the level of IEEE 754, but not necessarily exactly that. It's not a bug, it's a spec feature. The Roslyn compiler is in its right to evaluate the expression exactly as IEEE 754 specifies, and the other compiler is in its right to deduce that 2^32 + 7 is 7 when put into uint.

I'm sorry for my misleading first answer, but at least we've all learned something today.

V0ldek
  • 9,623
  • 1
  • 26
  • 57
  • Then I guess we have a bug in the current .NET Framework compiler (I just tried in VS 2019 just to be sure) :) I guess I'll try seeing if there's somewhere to log a bug to, though fixing something like that would probably have a lot of unwanted side effects and probably be ignored ... – Lukas Jan 20 '20 at 00:41
  • I don't think it's prematurely casting to int, that would've caused much clearer issues in A LOT of cases, I guess the case here is that in the const operation it's not evaluating the value and casting it until the very last, meaning is that instead of storing the intermediate values in floats, it's just skipping that and replacing it in each expression with the expression itself – jalsh Jan 20 '20 at 02:22
  • @jalsh I don't think I understand your guess. If the compiler simply replaced each `scale` with the float value and then evaluated everything else at runtime, the result would be the same. Can you elaborate? – V0ldek Jan 20 '20 at 07:22
  • @V0ldek, the downvote was a mistake, I edited your answer so I could remove it :) – jalsh Jan 20 '20 at 07:32
  • my guess is that it didn't actually store the intermediate values in floats, it just replaced f with the expression that evaluates f without casting it to float – jalsh Jan 20 '20 at 07:39
  • @V0ldek check [this](https://dotnetfiddle.net/1Pe33h) and [this](https://dotnetfiddle.net/RbKNTb) both in .net 4.7.1 – jalsh Jan 20 '20 at 07:40
  • My original answer was incorrect. Check the update for rectification. – V0ldek Jan 20 '20 at 08:22
  • @V0ldek, Great dig to find that answer, anyways, it still is strange that the two compilers generate different result – jalsh Jan 20 '20 at 08:26
  • and especially that on my latest example above, visual studio would tell me that the (float) cast is redundant but it still affects the result somehow – jalsh Jan 20 '20 at 08:28
0

The point here is (as you can see on the docs) that float values can only have a base up to 2^24. So, when you assign a value of 2^32 (64 * 2014 * 164 * 1024 = 2^6 * 2^10 * 2^6 * 2^10 = 2^32) it becomes, actually 2^24 * 2^8, which is 4294967000. Adding 7 will only be adding to the part truncated by conversion to ulong.

If you change to double, which has a base of 2^53, it will work for what you want.

This could be a run-time issue but, in this case, it is a compile-time issue, because all the values are constants and will be evaluated by the compiler.

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
-2

First of all you are using unchecked context which is an instruction for the compiler you are sure, as a developer, that the result will not overflow type and you'd like to see no compilation error. In your scenario you are actually on purpose overflowing type and expecting consistent behavior across three different compilers which one of them probably is backward compatible far to the history in comparison to Roslyn and .NET Core which are new ones.

Second thing is you are mixing implicit and explicit conversions. I am not sure about Roslyn compiler, but definitely .NET Framework and .NET Core compilers may use different optimizations for those operations.

The problem here is that first line of your code uses only floating point values/types, but the second line is combination of floating point values/types and integral value/type.

In case you make integer floating point type straight away (7 > 7.0) you'll get very same result for all three compiled sources.

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

So, I would say opposite to what V0ldek answered and that is "The bug (if it really is a bug) is most likely in Roslyn and .NET Core compilers".

Another reason to believe that is that result of first unchecked computation results are same for all and it is value that overflows maximal value of UInt32 type.

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

Minus one is there as we starts from zero which is value that is hard to subtract itself. If my math understanding of overflow is correct we start from next number after the maximal value.

UPDATE

According to the jalsh comment

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

His comment is correct. In case we use float you still get 0 for Roslyn and .NET Core, but on the other hand using double results in 7.

I made some additional tests and things get even weirder, but in the end everything makes sense(at least a bit).

What I assume is that .NET Framework 4.7.2 compiler(released in mid 2018) really uses different optimizations than .NET Core 3.1 and Roslyn 3.4 compilers(released at the end of 2019). These different optimizations/computations are purely used for constant values known at compile-time. That is why there was a need to use unchecked keyword as compiler already knows there is overflow happening, but different computation was used to optimize final IL.

Same source code and almost same IL except IL_000a instruction. One compiler computes 7 and other 0.

Source code

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

It starts to go the right way when you add non-constant expressions(by default are unchecked) like below.

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

Which generates "exactly" same IL by both compilers.

.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 compiler 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

So, in the end I believe the reason for different behavior is just a different version of framework and/or compiler that are using different optimizations/computation for constant expressions, but in other cases behavior is very same.

dropoutcoder
  • 2,627
  • 2
  • 14
  • 32
  • 7.0 is a double, not a float, try 7.0f, it'll still give you a 0 – jalsh Jan 20 '20 at 03:18
  • Yes, it should be floating point type, not float. Thanks for correction. – dropoutcoder Jan 20 '20 at 03:50
  • That changes the whole perspective of the issue, when dealing with a double the precision you get is much higher and the result explained in V0ldek's answer change drastically, you might rather just change scale to double and check again, the results would be the same... – jalsh Jan 20 '20 at 04:00
  • In the end it is more complex issue. – dropoutcoder Jan 20 '20 at 05:53
  • `unchecked` context doesn't mean that you're sure that nothing will overflow, but that you don't care if it does. It can either be that it indeed never happens and you want to save performance by skipping the check, or that it does happen, but it's the desired outcome. For example, `GetHashCode` implementations often are `unchecked`, because overflows there are expected and harmless. – V0ldek Jan 20 '20 at 07:27
  • @V0ldek [MSDN](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/unchecked) clearly states that _Expressions that contain non-constant terms are unchecked by default at compile time and run time_. The effect of ```unchecked``` on performance would only be seen when inside another [checked](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/checked) context – jalsh Jan 20 '20 at 08:14
  • 1
    @jalsh Yes, but there's a compiler flag that turns the checked context everywhere. You might want to have everything checked for safety, except for a certain hot path that needs all the CPU cycles it can get. – V0ldek Jan 20 '20 at 08:21
  • @V0ldek: I am not truly with you with your `unchecked` and `GetHashCode` example. For performance reasons it should be `unchecked`, but it definitely is not something where you should expect any overflow. Your answer states that result of overflow should be 0 and in that case every overflow inside `unchecked` implementation of `GetHashCode` will result in 0 too. Which I believe is not desired result. I believe XOR between multiple hashcodes should be used and that is safe for sure or you can use `HashCode.Combine` methods. – dropoutcoder Jan 20 '20 at 11:53
  • So far I still believe it is just different optimization used per compiler that makes the difference. – dropoutcoder Jan 20 '20 at 11:55
  • @dropoutcoder "Overflow" means that the operations on unsigned values are effectively a commutative ring. Or, in other words, every operation on a 32-bit unsigned value is calculated modulo 2^32. So 2^32 - 1 is 2^32 - 1, 2^32 overflows to 0, 2^32 + 7 overflows to 7. You might enjoy reading [this](https://ericlippert.com/2015/04/09/what-is-the-unchecked-keyword-good-for-part-one/) and its second part, maybe that gets the point across better. – V0ldek Jan 20 '20 at 14:01
  • Btw just to be clear, it wasn't trying to go for perf or anything of the like. I wrote a little hasher for some game entity, and found the result to be unstable and started digging deeper. The real issue for me here is the behavior change across .NET Framework. I incorrectly assumed all .NET Frameworks from Microsoft would have identical rules regarding overflows at least on the same platform, but this was evidently wrong. – Lukas Jan 21 '20 at 07:36
  • @Lukas: It is okay to play with things. I do it either. As I said the problem is most likely in compilers optimizations and not framework related. – dropoutcoder Jan 23 '20 at 22:34
  • @V0ldek: You probably didn't get that math of mine, but it is okay. I'll explain. In case you have bigger number than max number you definitely need to subtract max number and one to get into an overflow state to get the correct overflow value, right? Thanks for your link. It is nice read. – dropoutcoder Jan 23 '20 at 22:41