Why is the enumeration value from a multi dimensional array not equal to itself?
Asked Answered
B

2

152

Consider:

using System;

public class Test
{
    enum State : sbyte { OK = 0, BUG = -1 }

    static void Main(string[] args)
    {
        var s = new State[1, 1];
        s[0, 0] = State.BUG;
        State a = s[0, 0];
        Console.WriteLine(a == s[0, 0]); // False
    }
}

How can this be explained? It occurs in debug builds in Visual Studio 2015 when running in the x86 JIT. A release build or running in the x64 JIT prints True as expected.

To reproduce from the command line:

csc Test.cs /platform:x86 /debug

(/debug:pdbonly, /debug:portable and /debug:full also reproduce.)

Borkowski answered 13/4, 2016 at 4:4 Comment(14)
ideone.com/li3EzY it's true. add more information about .net version, IDE, compilerLathery
I actually see this return false under a debug build, but not a release build, targeting .Net 4.6.2 in VS2015.Dayan
Same here. But after fiddling with project settings I figured out that unchecking "Prefer 32 bit" checkbox in "Build" tab makes it work as intended - returning true. So, it looks like a WoW64 issue to me.Archimage
It seems that you pointed a bug in the framework.Bowrah
I think the problem has to do with sbyte, because when I change the type of State to int it works. I also noticed that when cast to int, arr[0,0] becomes 255 and State.Wrong becomes -1.Kuykendall
@Kuykendall That sounds more like a symptom than a cause.Dayan
@Preston Guillot, It is, but, I am still not sure what is causing it. I agree with @Fabien that it may have to do with the framework, because replacing -1 with -x causes (int)arr[0,0] to evaluate to 256-xKuykendall
I'm getting True result in VS 2015 and .Net 4.0Huskamp
@PrestonGuillot Thanks, the problem just happens in debug mode.Borkowski
Interestingly, running the broken code through ildasm and then ilasm "fixes" it...Disciplinant
If you compare two values using CompareTo method State.Wrong.CompareTo(arr[0, 0])) it gives correct value 0Microdot
Ah - rebuilding it with ilasm /debug leaves it broken...Disciplinant
The /debug=IMPL flag leaves it broken; /debug=OPT "fixes" it.Disciplinant
It works well from the Immediate Window but not from debug mode...Labrecque
K
164

You found a code generation bug in the .NET 4 x86 jitter. It is a very unusual one, it only fails when the code is not optimized. The machine code looks like this:

        State a = s[0, 0];
013F04A9  push        0                            ; index 2 = 0
013F04AB  mov         ecx,dword ptr [ebp-40h]      ; s[] reference
013F04AE  xor         edx,edx                      ; index 1 = 0
013F04B0  call        013F0058                     ; eax = s[0, 0]
013F04B5  mov         dword ptr [ebp-4Ch],eax      ; $temp1 = eax 
013F04B8  movsx       eax,byte ptr [ebp-4Ch]       ; convert sbyte to int
013F04BC  mov         dword ptr [ebp-44h],eax      ; a = s[0, 0]
        Console.WriteLine(a == s[0, 0]); // False
013F04BF  mov         eax,dword ptr [ebp-44h]      ; a
013F04C2  mov         dword ptr [ebp-50h],eax      ; $temp2 = a
013F04C5  push        0                            ; index 2 = 0
013F04C7  mov         ecx,dword ptr [ebp-40h]      ; s[] reference 
013F04CA  xor         edx,edx                      ; index 1 = 0
013F04CC  call        013F0058                     ; eax = s[0, 0]
013F04D1  mov         dword ptr [ebp-54h],eax      ; $temp3 = eax 
                                               ; <=== Bug here!
013F04D4  mov         eax,dword ptr [ebp-50h]      ; a == s[0, 0] 
013F04D7  cmp         eax,dword ptr [ebp-54h]  
013F04DA  sete        cl  
013F04DD  movzx       ecx,cl  
013F04E0  call        731C28F4  

A plodding affair with lots of temporaries and code duplication, that's normal for unoptimized code. The instruction at 013F04B8 is notable, that is where the necessary conversion from sbyte to a 32-bit integer occurs. The array getter helper function returned 0x0000000FF, equal to State.BUG, and that needs to be converted to -1 (0xFFFFFFFF) before the value can be compared. The MOVSX instruction is a Sign eXtension instruction.

Same thing happens again at 013F04CC, but this time there is no MOVSX instruction to make the same conversion. That's where the chips fall down, the CMP instruction compares 0xFFFFFFFF with 0x000000FF and that is false. So this is an error of omission, the code generator failed to emit MOVSX again to perform the same sbyte to int conversion.

What is particularly unusual about this bug is that this works correctly when you enable the optimizer, it now knows to use MOVSX in both cases.

The probable reason that this bug went undetected for so long is the usage of sbyte as the base type of the enum. Quite rare to do. Using a multi-dimensional array is instrumental as well, the combination is fatal.

Otherwise a pretty critical bug I'd say. How widespread it might be is hard to guess, I only have the 4.6.1 x86 jitter to test. The x64 and the 3.5 x86 jitter generate very different code and avoid this bug. The temporary workaround to keep going is to remove sbyte as the enum base type and let it be the default, int, so no sign extension is necessary.

You can file the bug at connect.microsoft.com, linking to this Q+A should be enough to tell them everything they need to know. Let me know if you don't want to take the time and I'll take care of it.

Kanal answered 13/4, 2016 at 9:3 Comment(7)
Good, solid data with the exact cause of such a strange issue, always a pleasure to read, +1.Wearing
Please post a link to the connect.microsoft.com article so we can vote for it.Kanal
I assume using byte instead of sbyte should be fine too and might be preferable if the real code is used with say an ORM where you don't want your flags in the database to take up extra space.Tommie
I considered recommending BUG = 0xff but decided not to since we can't see why -1 is important.Kanal
I'd post the bug on dotnet/coreclr rather than connect, you'll get straight to the JIT devs.Besiege
I'm a dev on the JIT team at Microsoft. I've reproduced the bug and have opened an issue for it internally (shipping x86 JIT is not yet in the open on github). In terms of timing of when this would be fixed, I anticipate that we will have this fix included in the next major release of the tools. If this bug is having business impact, and you need a fix earlier, please file the connect (connect.microsoft.com) issue so we can look impact and what alternatives we have to getting a fix to you faster.Bengaline
@RussellC.Hadley - here is another one: #41213137Kanal
L
8

Let's consider OP's declaration:

enum State : sbyte { OK = 0, BUG = -1 }

Since the bug only occurs when BUG is negative (from -128 to -1) and State is an enum of signed byte I started to suppose that there were a cast issue somewhere.

If you run this:

Console.WriteLine((sbyte)s[0, 0]);
Console.WriteLine((sbyte)State.BUG);
Console.WriteLine(s[0, 0]);
unchecked
{
    Console.WriteLine((byte) State.BUG);
}

it will output :

255

-1

BUG

255

For a reason that I ignore(as of now) s[0, 0] is cast to a byte before evaluation and that's why it claims that a == s[0,0] is false.

Labrecque answered 13/4, 2016 at 8:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.