Recursion in Windows 7 64 bit

2019-04-12 19:23发布

问题:

I have this helper class

public static class DateTimeHelper
  {
    public static int GetMonthDiffrence(DateTime date1, DateTime date2)
    {
      if (date1 > date2)
      {
        return getmonthdiffrence(date2, date1);
      }
      else
      {
        return ((date2.year - date1.year) * 12) + (date2.month - date1.month);
      }      
    }
  }

The function calculate the number of months between two dates, it do exactly what I want. So far there is no problem.

The problem is when I am on release and windows 7 64 bit I get always the same value "0"

When I got deep down the problem, I got aware that at some point, and because of the recursive call the two parameters are equal.

I repeat that I have this bug only if I lunch the release built un-attached to the debugger and on windows 7 64 bit.

Can anyone have any idea of this comportment? And if so I need some links to get in more details.

Here is the IL Code. (I think it can help to understand more)

.class public auto ansi abstract sealed beforefieldinit Helpers.DateTimeHelper
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig static 
        int32 GetMonthDiffrence (
            valuetype [mscorlib]System.DateTime date1,
            valuetype [mscorlib]System.DateTime date2
        ) cil managed 
    {
        // Method begins at RVA 0x6a658
        // Code size 52 (0x34)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: call bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime, valuetype [mscorlib]System.DateTime)
        IL_0007: brfalse.s IL_0011

        IL_0009: ldarg.1
        IL_000a: ldarg.0
        IL_000b: call int32 Helpers.DateTimeHelper::GetMonthDiffrence(valuetype [mscorlib]System.DateTime, valuetype [mscorlib]System.DateTime)
        IL_0010: ret

        IL_0011: ldarga.s date2
        IL_0013: call instance int32 [mscorlib]System.DateTime::get_Year()
        IL_0018: ldarga.s date1
        IL_001a: call instance int32 [mscorlib]System.DateTime::get_Year()
        IL_001f: sub
        IL_0020: ldc.i4.s 12
        IL_0022: mul
        IL_0023: ldarga.s date2
        IL_0025: call instance int32 [mscorlib]System.DateTime::get_Month()
        IL_002a: ldarga.s date1
        IL_002c: call instance int32 [mscorlib]System.DateTime::get_Month()
        IL_0031: sub
        IL_0032: add
        IL_0033: ret
    } // end of method DateTimeHelper::GetMonthDiffrence
} 

EDIT:

Here is a test program if you wish to reproduce the issue:

class Program
  {
    static void Main(string[] args)
    {
      for (int i = 2000; i < 3000; i++)
      {
        var date1 = new DateTime(i, 1, 1);
        var date2 = new DateTime(i + 1, 1, 1);
        var monthdiff = DateTimeHelper.GetMonthDiffrence(date2, date1);
        if (monthdiff == 0)
          Console.WriteLine(string.Format("date1 => {0}, date2 => {1}, diff=> {2}", date2, date1, monthdiff.ToString()));
      }
      Console.WriteLine("done!");
      Console.ReadKey();
    }
  }

you have to build the progect on release mode and 64 bit configuration, and then go to the location of the built result and execute the program. Thanks before hand. My best regards.

回答1:

I can replicate this behavior on Windows 7, .Net 4.5, Visual Studio 2012, x64 target, Release mode with debugger attached, but “Suppress JIT optimization on module load” disabled. This seems to be a bug in the tail call optimization (which is why you're getting it only on x64).

IL does not really matter here, the native code does. The relevant part of code for GetMonthDiffrence() is:

0000005e  cmp         rdx,rcx 
00000061  setg        al 
00000064  movzx       eax,al 
00000067  test        eax,eax 
00000069  je          0000000000000081 // else branch
0000006b  mov         rax,qword ptr [rsp+68h] 
00000070  mov         qword ptr [rsp+60h],rax 
00000075  mov         rax,qword ptr [rsp+60h] 
0000007a  mov         qword ptr [rsp+68h],rax 
0000007f  jmp         0000000000000012 // start of the method

The important part are the 4 mov instructions. They try to exchange [rsp+68h] and [rsp+60h] (which is where the parameters are stored), but they do it incorrectly, so both end up with the same value.

Interestingly, if I remove the call to Console.ReadKey() from your Main(), the code works fine, because the call to GetMonthDiffrence() is inlined and the tail call optimization is not performed in that case.

A possible workaround would be to add [MethodImpl(MethodImplOptions.NoInlining)] to your method, that seems to disable the tail call optimization.

I have submitted this bug on Connect.