подвійний? = подвійний? + подвійний?


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 = нуль замість цього.

Якщо я налагоджую в x86, все, здається, працює нормально. Щось не так із компілятором x64 чи щось не так зі мною?


15
Це відповідає моїм інтересам. Тепер нам залишається лише почекати пана Скіта.
Mike G

3
Це може бути налагоджувач? Спробуйте поставити в кінці Console.WriteLine () і подивіться, що він роздруковує.
siride

3
Це виглядає як відкладене виконання, яке налагоджувач не бачить далі. Можливо, призначення не виконується доти, доки воно не буде потрібно.
Джоел Етертон,

5
@igrimpe Мабуть 1 + 2 = 25 для дуже великих значень 1 і 2?
Кріс Сінклер,

2
Дякуємо за відповідь усіх. Досить круто бачити, як громада так швидко стрибає на це. Поведінка компілятора x64 робить налагодження таких типів тверджень дещо неінтуїтивно зрозумілим, але принаймні здається, що виконання коду працює належним чином у великій схемі самого додатка.
MrE

Відповіді:


86

Відповідь Дугласа правильно про JIT оптимізуючи мертвий код ( як x86 і x64 компіляторів будуть робити це). Однак, якби компілятор JIT оптимізував мертвий код, це було б відразу очевидно, оскільки xнавіть не з'являлося б у вікні Locals. Крім того, годинник і безпосереднє вікно замість цього видадуть вам помилку при спробі отримати до нього доступ: "Ім'я 'x' не існує в поточному контексті". Це не те, що ви описали як те, що відбувається.

Те, що ви бачите, насправді є помилкою у Visual Studio 2010.

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

Оскільки я не можу досконало відтворити цю проблему, оскільки у мене .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)
Сайт ІР-дзвінків дочірнього СП
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 рет

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

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

EDIT: У 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 на даний час є абсолютно різними базами коду […] J86 xIT JIT швидший з точки зору швидкості компіляції; x64 JIT повільніший, але робить цікавіші оптимізації ".
Дуглас,

2
@MrE - Це не повинно бути позначено як правильна відповідь. Якби компілятор JIT оптимізував мертвий код, Visual Studio взагалі не зміг би вирішити xзмінну. Це не буде з'являтися в вікні місцевих жителів, і намагається його перегляд в годинах або негайні вікна дадуть повідомлення: The name 'x' does not exist in the current context.
Крістофер Керренс,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.