Удваивать? = double? + двойной?

Я хотел выполнить ping сообщество StackOverflow, чтобы узнать, не теряю ли я этот простой бит кода С#.

Я разрабатываю Windows 7, создавая это в .NET 4.0, x64 Debug.

У меня есть следующий код:

static void Main()
{
    double? y = 1D;
    double? z = 2D;

    double? x;
    x = y + z;
}

Если я отлаживаю и помещаю точку останова на конечную фигурную фигуру, я ожидаю, что x = 3 в окне просмотра и в окне "Немедленное окно". x = null.

Если я отлаживаю x86, все работает нормально. Что-то не так с компилятором x64 или что-то не так со мной?

Ответ 1

Ответ Douglas правилен в отношении кода DIT, оптимизирующего JIT (оба компилятора x86 и x64 сделают это). Однако, если компилятор JIT оптимизирует мертвый код, это будет сразу же очевидно, потому что x даже не появится в окне Locals. Кроме того, часы и немедленное окно вместо этого выдают вам сообщение об ошибке при попытке получить к нему доступ: "Имя" x "не существует в текущем контексте". Это не то, что вы описали как происходящее.

То, что вы видите, на самом деле является ошибкой в ​​Visual Studio 2010.

Сначала я попытался воспроизвести эту проблему на моей главной машине: Win7x64 и VS2012. Для целей .NET 4.0 x равен 3.0D, когда он ломается на закрывающей фигурной скобке. Я решил попробовать и .NET 3.5 цели, и с этим, x также был установлен в 3.0D, а не null.

Так как я не могу сделать идеальное воспроизведение этой проблемы, так как .NET 4.5 установлен поверх .NET 4.0, я создал виртуальную машину и установил на ней VS2010.

Здесь я смог воспроизвести проблему. С точкой останова на закрывающей фигурной скобке метода Main, как в окне просмотра, так и в локальном окне, я увидел, что x был null. Здесь начинается интерес. Вместо этого я нацелился на рабочую среду v2.0 и обнаружил, что он тоже там был нулевым. Конечно, это не может быть так, поскольку у меня такая же версия среды выполнения .NET 2.0 на моем другом компьютере, которая успешно показала x со значением 3.0D.

Итак, что же происходит? После некоторого копания в windbg я нашел проблему:

VS2010 показывает вам значение x до того, как оно действительно было назначено.

Я знаю, что это не похоже на то, что указатель инструкции проходит через строку x = y + z. Вы можете проверить это самостоятельно, добавив несколько строк кода в метод:

double? y = 1D;
double? z = 2D;

double? x;
x = y + z;

Console.WriteLine(); // Don't reference x here, still leave it as dead code

С точкой останова на конечной фигурной фигурной скобке, локали и окно просмотра показывают x равным 3.0D. Однако, если вы выполните код, вы заметите, что VS2010 не показывает x как назначенный до тех пор, пока вы не пройдете через Console.WriteLine().

Я не знаю, была ли эта ошибка когда-либо сообщена Microsoft Connect, но вы можете сделать это, используя этот код в качестве примера. Очевидно, что он был исправлен в VS2012, поэтому я не уверен, будет ли исправление исправлено или нет.


Здесь, что на самом деле происходит в JIT и VS2010

С исходным кодом мы можем видеть, что делает VS, и почему это неправильно. Мы также видим, что переменная x не оптимизируется (если вы не отметили сборку, которая должна быть скомпилирована с включенными оптимизациями).

Сначала рассмотрим определения локальной переменной IL:

.locals init (
    [0] valuetype [mscorlib]System.Nullable`1<float64> y,
    [1] valuetype [mscorlib]System.Nullable`1<float64> z,
    [2] valuetype [mscorlib]System.Nullable`1<float64> x,
    [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0000,
    [4] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0001,
    [5] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0002)

Это нормальный выход в режиме отладки. Visual Studio определяет повторяющиеся локальные переменные, которые используются во время назначений, а затем добавляет дополнительные команды IL для копирования из переменной CS * в соответствующую пользовательскую локальную переменную. Вот соответствующий код IL, который показывает это:

// For the line x = y + z
L_0045: ldloca.s CS$0$0000 // earlier, y was stloc.3 (CS$0$0000)
L_0047: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault()
L_004c: conv.r8            // Convert to a double
L_004d: ldloca.s CS$0$0001 // earlier, z was stloc.s CS$0$0001
L_004f: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault()
L_0054: conv.r8            // Convert to a double 
L_0055: add                // Add them together
L_0056: newobj instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0) // Create a new nulable
L_005b: nop                // NOPs are placed in for debugging purposes
L_005c: stloc.2            // Save the newly created nullable into `x`
L_005d: ret 

Сделайте более глубокую отладку с помощью WinDbg:

Если вы отлаживаете приложение в VS2010 и оставляете точку останова в конце метода, мы можем легко подключить WinDbg в неинвазивном режиме.

Вот кадр для метода Main в стеке вызовов. Мы заботимся о IP (указатель инструкции).

0:009> !clrstack
OS Thread Id: 0x135c (9)
Child SP         IP               Call Site
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main(System.String[])
[And so on...]

Если мы посмотрим на собственный машинный код для метода Main, мы увидим, какие инструкции были выполнены в то время, когда VS нарушает выполнение:

000007ff`00173388 e813fe25f2      call    mscorlib_ni+0xd431a0 
           (000007fe`f23d31a0) (System.Nullable`1[[System.Double, mscorlib]]..ctor(Double), mdToken: 0000000006001ef2)
****000007ff`0017338d cc              int     3****
000007ff`0017338e 8d8c2490000000  lea     ecx,[rsp+90h]
000007ff`00173395 488b01          mov     rax,qword ptr [rcx]
000007ff`00173398 4889842480000000 mov     qword ptr [rsp+80h],rax
000007ff`001733a0 488b4108        mov     rax,qword ptr [rcx+8]
000007ff`001733a4 4889842488000000 mov     qword ptr [rsp+88h],rax
000007ff`001733ac 488d8c2480000000 lea     rcx,[rsp+80h]
000007ff`001733b4 488b01          mov     rax,qword ptr [rcx]
000007ff`001733b7 4889442440      mov     qword ptr [rsp+40h],rax
000007ff`001733bc 488b4108        mov     rax,qword ptr [rcx+8]
000007ff`001733c0 4889442448      mov     qword ptr [rsp+48h],rax
000007ff`001733c5 eb00            jmp     000007ff`001733c7
000007ff`001733c7 0f28b424c0000000 movaps  xmm6,xmmword ptr [rsp+0C0h]
000007ff`001733cf 4881c4d8000000  add     rsp,0D8h
000007ff`001733d6 c3              ret

Используя текущий IP-адрес, полученный из !clrstack в Main, мы видим, что выполнение было приостановлено в инструкции непосредственно после вызова конструктора System.Nullable<double>. (int 3 - это прерывание, используемое отладчиками для прекращения выполнения). Я окружил эту строку с помощью *, и вы также можете сопоставить строку с L_0056 в IL.

Последовательная сборка x64 фактически присваивает ее локальной переменной x. Наш указатель команд еще не выполнил этот код, поэтому VS2010 преждевременно ломается до того, как переменная x была назначена нативным кодом.

EDIT: в x64 команда int 3 помещается перед кодом назначения, как вы можете видеть выше. В x86 эта команда помещается после кода назначения. Это объясняет, почему VS рано ломается только в x64. Трудно сказать, является ли это ошибкой Visual Studio или JIT-компилятора. Я не уверен, какое приложение вставляет точки прерывания.

Ответ 2

Известно, что x64 JIT-компилятор более агрессивен в своих оптимизации, чем x86. (Вы можете ссылаться на "" Проверка границ массива в CLR" для случая, когда компиляторы x86 и x64 генерируют код, который семантически отличается. )

В этом случае компилятор x64 обнаруживает, что x никогда не читается и вообще не выполняет его назначение; это называется удаление мертвого кода в оптимизации компилятора. Чтобы этого не произошло, просто добавьте следующую строку сразу после назначения:

Console.WriteLine(x);

Вы заметите, что печатается не только правильное значение 3, но и переменная x будет отображать правильное значение в отладчике, а (редактировать), следуя Console.WriteLine, который ссылается на него.

Изменить. Кристофер Курренс предлагает альтернативное объяснение , указывающее на ошибку в Visual Studio 2010, которая может быть более точной, чем выше.