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

151

Рассматривать:

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также воспроизводить.)

Синго
источник
2
ideone.com/li3EzY это правда. добавить дополнительную информацию о .net версии, IDE, компилятор
Backs
1
Тоже самое. Но после игры с настройками проекта я понял, что снятие флажка «Предпочитать 32-битный» на вкладке «Сборка» заставляет его работать как задумано - возвращая true. Так что для меня это похоже на проблему с WoW64.
Дмитрий Ротай
2
Кажется, вы указали на ошибку в рамках.
Фабьен ПЕРРОННЕТ
1
Интересно, что запуск неработающего кода, ildasmа затем его ilasm«исправление» ...
Джон Скит
2
В /debug=IMPLфлаг листья сокрушен; /debug=OPT"исправляет" это.
Джон Скит

Ответы:

163

Вы нашли ошибку генерации кода в джиттере .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-разрядное целое число. Вспомогательная функция получения массива вернула 0x0000000FF, равный State.BUG, и его необходимо преобразовать в -1 (0xFFFFFFFF), прежде чем можно будет сравнить значение. Инструкция MOVSX является инструкцией Sign eXtension.

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

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

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

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

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

Ганс Пассант
источник
33
Хорошие, достоверные данные с точной причиной такой странной проблемы, всегда приятно читать, +1.
Лассе В. Карлсен
11
Пожалуйста, опубликуйте ссылку на статью connect.microsoft.com, чтобы мы могли проголосовать за нее.
Ганс Пассант
Я предполагаю, что использование byteвместо sbyteдолжно быть тоже хорошо и может быть предпочтительным, если реальный код используется, например, с ORM, где вы не хотите, чтобы ваши флаги в базе данных занимали дополнительное место.
Во
6
Я бы опубликовал сообщение об ошибке в dotnet / coreclr, а не подключил, вы сразу перейдете к разработчикам JIT.
Лукас Тресневский
8
Я разработчик в команде JIT в Microsoft. Я воспроизвел ошибку и открыл внутреннюю проблему для нее (доставка git x86 еще не открыта на github). С точки зрения сроков, когда это будет исправлено, я ожидаю, что это исправление будет включено в следующий основной выпуск инструментов. Если эта ошибка оказывает влияние на бизнес, и вам нужно исправить ее раньше, пожалуйста, зарегистрируйте проблему connect (connect.microsoft.com), чтобы мы могли посмотреть, как это повлияет, и какие у нас есть альтернативы, чтобы быстрее получить исправление.
Рассел С. Хэдли
8

Давайте рассмотрим декларацию 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

BUG

255

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

Томас Аюб
источник