двойной? = двойной? + двойной?

79

Я хотел связаться с сообществом StackOverflow, чтобы узнать, теряю ли я рассудок из-за этого простого фрагмента кода C #.

Я разрабатываю для 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 или что-то не так со мной?

MrE
источник
15
Это соответствует моим интересам. Теперь нам просто нужно дождаться мистера Скита.
Mike G
3
Может быть, отладчик? Попробуйте поместить Console.WriteLine () в конец и посмотрите, что он распечатает.
siride
3
Это похоже на отложенное выполнение, за которым отладчик не видит. Возможно, задание не будет выполнено до тех пор, пока оно не понадобится.
Joel Etherton
5
@igrimpe Очевидно, 1 + 2 = 25 для очень больших значений 1 и 2?
Крис Синклер,
2
Спасибо за ответ. Приятно видеть, как сообщество так быстро взялось за это. На мой взгляд, поведение компилятора x64 делает отладку этого типа операторов немного неинтуитивно, но, по крайней мере, кажется, что выполнение кода работает правильно в общей схеме самого приложения.
MrE

Ответы:

86

Ответ Дугласа верен в отношении мертвого кода, оптимизирующего JIT ( как компиляторы x86, так и x64 будут делать это). Однако, если бы JIT-компилятор оптимизировал мертвый код, это было бы сразу очевидно, потому что xоно даже не появилось бы в окне локальных переменных. Более того, просмотр и немедленное окно вместо этого выдали бы вам ошибку при попытке доступа к нему: «Имя '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
Идентификатор потока ОС: 0x135c (9)
Дочерний SP IP Call Site
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main (System.String [])
[И так далее...]

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

000007ff`00173388 e813fe25f2 вызов 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 добавить rsp, 0D8h
000007ff`001733d6 c3 ret

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

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

РЕДАКТИРОВАТЬ: В x64 int 3инструкция помещается перед кодом назначения, как вы можете видеть выше. В x86 эта инструкция помещается после кода назначения. Это объясняет, почему VS рано ломается только в x64. Трудно сказать, виновата ли в этом Visual Studio или JIT-компилятор. Я не уверен, какое приложение вставляет перехватчики точки останова.

Кристофер Карренс
источник
6
@ChristopherCurrens Хорошо, ты заслужил зеленую галочку. Это имеет смысл. Я пришлю что-нибудь в Microsoft. Спасибо за внимание к этому. Очень впечатлен усилиями сообщества.
MrE
30

Компилятор x64 JIT, как известно, более агрессивен в оптимизации, чем x86. (Вы можете обратиться к разделу «Устранение проверки границ массива в CLR » в случае, если компиляторы x86 и x64 генерируют код, который семантически отличается.)

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

Console.WriteLine(x);

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

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

Дуглас
источник
Эээ, я только что проверил это, так как это тоже была моя первая мысль, и этот ответ на самом деле неверен. Отладчик фактически отображает null после присвоения до тех пор, пока не будет вызван Console.WriteLine().
tomfanning
@tomfanning: Это не отменяет ответ. Компилятор выдает инструкцию для выполнения присваивания только в том месте, где он обнаруживает, что это будет необходимо - в Console.WriteLine.
Дуглас
1
@leppie Компилятор является (или , по крайней мере, раньше) различны. Например, он будет встраиваться гораздо агрессивнее, чем компилятор x86.
Конрад Рудольф
2
@leppie: « Устранение проверки границ массива в CLR » дает примеры того, где компиляторы могут фактически вызвать семантические различия. «JIT-компиляторы для x86 и x64 в настоящее время представляют собой совершенно разные кодовые базы […] JIT x86 быстрее с точки зрения скорости компиляции; x64 JIT медленнее, но делает более интересные оптимизации ».
Дуглас
2
@MrE - Это не должно быть отмечено как правильный ответ. Если бы JIT-компилятор оптимизировал мертвый код, Visual Studio вообще не смогла бы разрешить xпеременную. Это не будет появляться в окне местных жителей, и пытается его просмотр в часах или немедленные окна дадут сообщение: The name 'x' does not exist in the current context.
Christopher Currens