Почему значение перечисления из многомерного массива не равно самому себе?

Рассмотрим:

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

Как это можно объяснить? Это происходит в отладочных сборках в Visual Studio 2015 при работе в x86 JIT. Выпуск сборки или запуск в x64 JIT-выводах True, как ожидалось.

Воспроизведение из командной строки:

csc Test.cs /platform:x86 /debug

(/debug:pdbonly, /debug:portable и /debug:full также воспроизводятся.)

Ответ 1

Вы обнаружили ошибку генерации кода в джиттере .NET 4 x86. Это очень необычный вариант, он не работает, когда код не оптимизирован. Код машины выглядит следующим образом:

        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  

Плотное дело с большим количеством времен и дублированием кода, что нормально для неоптимизированного кода. Инструкция на 013F04B8 примечательна, то есть там, где происходит необходимое преобразование из sbyte в 32-разрядное целое. Вспомогательная функция getter массива возвращала 0x0000000FF, равную State.BUG, и ее необходимо преобразовать в -1 (0xFFFFFFFF) до того, как значение будет сравниваться. Инструкция MOVSX представляет собой команду Sign eXtension.

То же самое происходит и в 013F04CC, но на этот раз существует команда no MOVSX для того же преобразования. Что там, где фишки падают, команда CMP сравнивает 0xFFFFFFFF с 0x000000FF, и это ложь. Таким образом, это ошибка пропуска, генератор кода не смог снова запустить MOVSX для выполнения одного и того же преобразования sbyte в int.

Что особенно необычно в этой ошибке, так это то, что это правильно работает при включении оптимизатора, теперь он знает, что использовать MOVSX в обоих случаях.

Вероятная причина, по которой эта ошибка не была обнаружена так долго, - это использование sbyte в качестве базового типа перечисления. Довольно редко. Использование многомерного массива также полезно, комбинация является фатальной.

В противном случае довольно критическая ошибка, я бы сказал. Насколько широко распространено это может быть трудно догадаться, у меня есть только 4.6.1 x86 джиттера для тестирования. X64 и джиттер 3.5 x86 генерируют совершенно другой код и избегают этой ошибки. Временным решением для продолжения является удаление sbyte в качестве базового типа enum и его значение по умолчанию - int, поэтому расширение знака не требуется.

Вы можете отправить сообщение об ошибке на connect.microsoft.com, чтобы связать его с этим Q + A, должно быть достаточно, чтобы сообщить им все, что им нужно знать. Дайте мне знать, если вы не хотите тратить время, и я позабочусь об этом.

Ответ 2

Рассмотрим утверждение OP:

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

Так как ошибка возникает только тогда, когда BUG является отрицательной (от -128 до -1), а State является перечислением подписанного байта, я начал предполагать, что там была проблема с литой.

Если вы запустите это:

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

он выведет:

255

1

ОШИБКИ

255

По той причине, что я игнорирую (на данный момент)s[0, 0], передается в байты перед оценкой и поэтому утверждает, что a == s[0,0] является ложным.