10

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 _);
        }
    }
}

Build config

When debugging I can see the following:

Debug step 1 Debug step 2 Debug step 3

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:

  1. Bug in Visual Studio debugger
  2. 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...

Memory 1

Memory 2

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

Darxis
  • 1,482
  • 1
  • 17
  • 37
  • Hi. I think is a memory optimization. Since x is never used, just to assign a value to result, probably the compiler or the CLR is using the same memory space for x and result. When you add the Console.WriteLine the parameter x is used by itself, and the optimization doesn't occur. Also, maybe the fact that you are discarding the output value could be part of the reason. Anyway, I don't see the problem. In this code x, since is a "by value" param, should not be altered. – Pepelui360 Apr 15 '20 at 12:01
  • Hi, @Pepelui360, thanks for the comment. I think by saying memory optimization you was actually referring to code optimization. I understand that the "x" value is not used and optimization would optimize this, but the problem is that optimization shouldn't occur when a project is being built in Debug mode with "Optimize code" disabled. Debugging optimized code could return misleading variable values. – Darxis Apr 15 '20 at 12:12
  • 3
    Not an optimization bug, not a debugger bug. The value of x does in fact change in memory. Looks like an x64 code generation issue to me, the machine code is too convoluted to easily find the flaw. Nothing I could do to actually cause the code to fail, as soon as you try to refer to the value of x in code then the behavior disappears. So they might consider it "by design". Use Help > Send Feedback > Report a Problem to make them aware of the extremely confusing behavior. – Hans Passant Apr 15 '20 at 12:51
  • @HansPassant Thank you very much for your comment, I have updated my question, see "Edit 2" – Darxis Apr 15 '20 at 12:58
  • 2
    https://sharplab.io is an easier way to get machine code. Per Hans, the code generated by the runtime is quite involved and it's not that easy to debug. The JIT does quite a bit of work to get code involving `decimal`s to compile nice; it's not at all inconceivable that it's optimizing something despite being asked not to optimize. Making sure code is *not* optimized is understandably often lower on the priority list. (On the other hand, it could hide a genuine aliasing bug that might rear its head and lead to actual failure under other circumstances.) – Jeroen Mostert Apr 15 '20 at 13:01
  • @Darxis Did you report it on the github? Can you provide a link to this issue? – Paweł Łukasik Apr 16 '20 at 07:10
  • @PawełŁukasik I haven't yet, should I? (if yes, then on which repo, dotnet/roslyn?) I have only reported on the VS Developer Community. I have updated the question and added the link to the issue at the end – Darxis Apr 16 '20 at 08:57
  • @Darxis any link is good. Thx. Just wanted to follow the issue. – Paweł Łukasik Apr 16 '20 at 10:39

0 Answers0