I have a problem with some C# code being optimized in Debug
mode despite having Optimize code
disabled.
Code
namespace Test
{
internal class Program
{
public class Test
{
public void TestMethod(decimal x, out decimal result)
{
result = x / 100m;
//Console.WriteLine($"{x} {result}");
result++;
//Console.WriteLine($"{x} {result}");
}
}
private static void Main()
{
var x = new Test();
x.TestMethod(12300m, out _);
}
}
}
When debugging I can see the following:
As you can see in the second step both x
and result
variables were modified but the only one expected to be modified was result
.
Now some analyse of this problem. My guess is that it could be caused by two things:
- Bug in Visual Studio debugger
- Compiler that optimize code
If it is a bug in the Visual Studio debugger then why uncommenting the Console.WriteLine
code fixes this issue?
If it is the compiler that optimizes code then why it is not visible in the dotPeek decompiled sources? Is it possible that the CLR or something else optimizes the code at runtime? The assembly source decompiled by dotPeek looks like this:
// Decompiled with JetBrains decompiler
// Type: Test.Program
// Assembly: Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 010328D8-C177-4463-81CD-43CDDFF608A4
// Assembly location: XXX
using System;
namespace Test
{
internal class Program
{
private static void Main()
{
new Program.Test().TestMethod(new Decimal(12300), out Decimal _);
}
public class Test
{
public void TestMethod(Decimal x, out Decimal result)
{
result = x / new Decimal(100);
++result;
}
}
}
}
What I have already tried and have not fixed this issue:
- Clean Solution + Build Solution
- Rebuild Solution
- Clean Project + Build Project
- Rebuild Project
- Check the
Optimize code
flag, Build Solution, uncheck it, Build Solution again (suggested in https://stackoverflow.com/a/31297200/699850) - Close Visual Studio and delete the hidden
.vs
directory
Edit 1:
Here is the MSIL code
.method public hidebysig instance void TestMethod(valuetype [mscorlib]System.Decimal x,
[out] valuetype [mscorlib]System.Decimal& result) cil managed
{
// Code size 38 (0x26)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.2
IL_0002: ldarg.1
IL_0003: ldc.i4.s 100
IL_0005: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
IL_000a: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Division(valuetype [mscorlib]System.Decimal,
valuetype [mscorlib]System.Decimal)
IL_000f: stobj [mscorlib]System.Decimal
IL_0014: ldarg.2
IL_0015: ldarg.2
IL_0016: ldobj [mscorlib]System.Decimal
IL_001b: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Increment(valuetype [mscorlib]System.Decimal)
IL_0020: stobj [mscorlib]System.Decimal
IL_0025: ret
} // end of method Test::TestMethod
Edit 2:
@HansPassant thanks for your comment, you mentioned that this issue is specific to x64. Well, I have checked Prefer 32-bit
and the issue is gone so it must be an issue with x64! I have debugged the app once again with the memory views open. We can see both variables change in the memory so it could be a bug in the x64 code generation as you mentioned. I am going to debug the x64 code now...
Edit 3 (final?):
I have debugged the x64 code and it turned out that the runtime generates optimized x64 code...
Here are the generated x64 assembly codes for two cases, one when System.Console.WriteLine
is called (code is not optimized) and one when the System.Console.WriteLine
is commented (code should not be optimized but is optimized):
With System.Console.WriteLine calls:
result = x / 100m;
00007FFD078809F7 lea rcx,[rbp+0D8h]
00007FFD078809FE vxorps xmm0,xmm0,xmm0
00007FFD07880A03 vmovdqu xmmword ptr [rcx],xmm0
00007FFD07880A08 lea rcx,[rbp+0D8h]
00007FFD07880A0F mov edx,64h
00007FFD07880A14 call 00007FFD64B42280 // System.Decimal..ctor(Int32) // new Decimal(100)
00007FFD07880A19 mov rcx,qword ptr [rbp+120h] // rcx = &result
00007FFD07880A20 mov qword ptr [rbp+0D0h],rcx // [rbp+0D0h] = rcx -> &result
00007FFD07880A27 lea rcx,[rbp+0C0h] // ?
00007FFD07880A2E mov qword ptr [rbp+70h],rcx // ?
00007FFD07880A32 mov rcx,qword ptr [rbp+118h] // rcx = &x
00007FFD07880A39 vmovdqu xmm0,xmmword ptr [rcx] // xmm0 = rcx -> x <--- Here we copy 'x' value to xmm0
00007FFD07880A3E vmovdqu xmmword ptr [rbp+88h],xmm0 // [rbp+88h] = xmm0 -> x <--- Here we copy xmm0 ('x' value) to [rbp+88h]
00007FFD07880A47 vmovdqu xmm0,xmmword ptr [rbp+0D8h] // xmm0 = copy of 100m
00007FFD07880A50 vmovdqu xmmword ptr [rbp+78h],xmm0 // [rbp+78h] = xmm0 -> copy of 100m
00007FFD07880A56 mov rcx,qword ptr [rbp+70h] // ?
00007FFD07880A5A lea rdx,[rbp+88h] // rdx = [rbp+88h] -> copy of 'x' as first op_Division arg
00007FFD07880A61 lea r8,[rbp+78h] // R8 = [rbp+78h] -> 100m as second op_Division arg
00007FFD07880A65 call 00007FFD64B440A0 // Call System.Decimal.op_Division(System.Decimal, System.Decimal)
00007FFD07880A6A mov rdx,qword ptr [rbp+0D0h]
00007FFD07880A71 vmovdqu xmm0,xmmword ptr [rbp+0C0h]
00007FFD07880A7A vmovdqu xmmword ptr [rdx],xmm0
With System.Console.WriteLine calls commented:
result = x / 100m;
00007FFD078909F7 lea rcx,[rbp+88h]
00007FFD078909FE vxorps xmm0,xmm0,xmm0
00007FFD07890A03 vmovdqu xmmword ptr [rcx],xmm0
00007FFD07890A08 lea rcx,[rbp+88h]
00007FFD07890A0F mov edx,64h
00007FFD07890A14 call 00007FFD64B42280 // System.Decimal..ctor(Int32) // new Decimal(100)
00007FFD07890A19 mov rcx,qword ptr [rbp+0D0h] // rcx = &result
00007FFD07890A20 mov qword ptr [rbp+80h],rcx // [rbp+80h] = rcx -> &result
00007FFD07890A27 lea rcx,[rbp+70h] // ?
00007FFD07890A2B mov qword ptr [rbp+40h],rcx // ?
00007FFD07890A2F mov rcx,qword ptr [rbp+0C8h] // rcx = &x
00007FFD07890A36 mov qword ptr [rbp+38h],rcx // [rbp+38h] = rcx -> &x
00007FFD07890A3A vmovdqu xmm0,xmmword ptr [rbp+88h] // xmm0 = copy of 100m
00007FFD07890A43 vmovdqu xmmword ptr [rbp+48h],xmm0 // [rbp+48h] = xmm0 -> copy of 100m
00007FFD07890A49 mov rcx,qword ptr [rbp+40h] // ?
00007FFD07890A4D mov rdx,qword ptr [rbp+38h] // rdx = [rbp+38h] = &x -> 'x' instance address as first op_Division arg
00007FFD07890A51 lea r8,[rbp+48h] // r8 = [rbp+48h] -> copy of 100m as second op_Division arg
00007FFD07890A55 call 00007FFD64B440A0 // Call System.Decimal.op_Division(System.Decimal, System.Decimal)
00007FFD07890A5A mov rcx,qword ptr [rbp+80h]
00007FFD07890A61 vmovdqu xmm0,xmmword ptr [rbp+70h]
00007FFD07890A67 vmovdqu xmmword ptr [rcx],xmm0
As you can see the are similar but the first one copies the x
value to a new memory location:
00007FFD07880A39 vmovdqu xmm0,xmmword ptr [rcx] // xmm0 = rcx -> x <--- Here we copy 'x' value to xmm0
00007FFD07880A3E vmovdqu xmmword ptr [rbp+88h],xmm0 // [rbp+88h] = xmm0 -> x <--- Here we copy xmm0 ('x' value) to [rbp+88h]
and then passes it to the System.Decimal.op_Division(System.Decimal, System.Decimal)
function. But the second one does not copy it and passes its reference directly; it is missing the two instructions above.
I have submitted a bug as @HansPassant has suggested with a link to this SO question. Link: https://developercommunity.visualstudio.com/content/problem/992021/c-code-being-optimized-in-debug-mode-with-optimize.html