Can C# 'is' operator suffer under release

2019-03-09 04:58发布

问题:

Below is a simple test fixture. It succeeds in Debug builds and fails in Release builds (VS2010, .NET4 solution, x64):

[TestFixture]
public sealed class Test
{
    [Test]
    public void TestChecker()
    {
        var checker = new Checker();
        Assert.That(checker.IsDateTime(DateTime.Now), Is.True);
    }
}

public class Checker
{
    public bool IsDateTime(object o)
    {
        return o is DateTime;
    }
}

It seems code optimization wreaks some havoc; if I disable it on the Release build, it works as well. That was rather puzzling to me. Below, I've used ILDASM to disassemble the 2 versions of the build:

Debug IL:

.method public hidebysig instance bool IsDateTime(object o) cil managed
{
  // Code size       15 (0xf)
  .maxstack  2
  .locals init (bool V_0)
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  isinst     [mscorlib]System.DateTime
  IL_0007:  ldnull
  IL_0008:  cgt.un
  IL_000a:  stloc.0
  IL_000b:  br.s       IL_000d
  IL_000d:  ldloc.0
  IL_000e:  ret
} // end of method Validator::IsValid

Release IL:

.method public hidebysig instance bool IsDateTime(object o) cil managed
{
  // Code size       10 (0xa)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  isinst     [mscorlib]System.DateTime
  IL_0006:  ldnull
  IL_0007:  cgt.un
  IL_0009:  ret
} // end of method Validator::IsValid

It seems a store and load is optimized away. Targeting earlier versions of the .NET framework made the problem go away, but that may just be a fluke. I found this behaviour somewhat unnerving, can anybody explain why the compiler would think it safe to do an optimization that produces different observable behaviour?

Thanks in advance.

回答1:

This bug already came up in this SO question by Jacob Stanley. Jacob has already reported the bug, and Microsoft has confirmed that it is indeed a bug in the CLR JIT. Microsoft had this to say:

This bug will be fixed in a future version of the runtime. I'm afraid it's too early to tell if that will be in a service pack or the next major release.

Thank you again for reporting the issue.

You should be able to work around the bug by adding the following attribute to TestChecker():

[MethodImpl(MethodImplOptions.NoInlining)]


回答2:

It isn't related to the C# compiler, the IL is identical. You found a bug in the .NET 4.0 jitter optimizer. You can repro it in Visual Studio. Tools + Options, Debugging, General, untick the "Suppress JIT optimization on module load" option and run the Release build to repro the failure.

I haven't looked at it closely enough yet to identify the bug. It looks very strange, it inlines the method and completely omits the code for the boxing conversion. The machine code is substantially different from the code generated by the version 2 jitter.

A clean workaround isn't that easy, you can do it by suppressing inlining. Like this:

    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
    public bool IsDateTime(object o) {
        return o is DateTime;
    }

You can report the bug at connect.microsoft.com. Let me know if you don't want to and I'll take care of it.


Never mind, that was already done. It wasn't fixed in the maintenance release that was included with VS2010 SP1.


This bug has been fixed, I can no longer repro it. My current version of clrjit.dll is 4.0.30319.237 dated May 17th 2011. I can't tell exactly what update repaired it. I got a security update on Aug 5th 2011 that updated clrjit.dll to revision 235 with a date of Apr 12, that would be the earliest.



回答3:

The store and load is essentially a nop as far as flow-control goes, but probably massage some CPU caches in some way. The actual flow just loads the argument on the stack, checks if it's an instance (which return null or the instance), pushes null onto the stack, and compares (greater-than) which results in a boolean being left on the stack.

Now what the JITter does with it is another story altogether (and would depend on what platform you are using. The JITter will do all sorts of crazy things in the name of performance (our team recently got hit because tail-call optimization changed to optimize across domain boundaries which broke GetCallingAssembly()). It's possible the JITter is inlining IsDateTime, noticing that there's not way it can't not be a DateTime and just pushing true onto the stack.

It's also possible your release version is targetting a slightly different Framework so DateTime in the test assembly is not DateTime in the tested assembly.

I realize that doesn't answer why your code is breaking.



回答4:

For reference I checked with mono

  • Mono JIT compiler version 2.6.7 (Debian 2.6.7-3ubuntu1)
  • Mono JIT compiler version 2.8.2 (sehe/d1c74ad Fri Feb 18 21:46:52 CET 2011)

Both presented no problems whatsoever. Here is the IL with optimization in 2.8.2

.method public hidebysig 
       instance default bool IsDateTime (object o)  cil managed 
{
    // Method begins at RVA 0x2130
    // Code size 10 (0xa)
    .maxstack 8
    IL_0000:  ldarg.1 
    IL_0001:  isinst [mscorlib]System.DateTime
    IL_0006:  ldnull 
    IL_0007:  cgt.un 
    IL_0009:  ret 
} // end of method Checker::IsDateTime

Without optimizations is exactly the same

Here is the result of mono's jitted code for this IL:

00000130 <TestData_Checker_IsDateTime_object>:
     130:       55                      push   %ebp
     131:       8b ec                   mov    %esp,%ebp
     133:       53                      push   %ebx
     134:       56                      push   %esi
     135:       83 ec 10                sub    $0x10,%esp
     138:       e8 00 00 00 00          call   13d <TestData_Checker_IsDateTime_object+0xd>
     13d:       5b                      pop    %ebx
     13e:       81 c3 03 00 00 00       add    $0x3,%ebx
     144:       8b 45 0c                mov    0xc(%ebp),%eax
     147:       89 45 f4                mov    %eax,-0xc(%ebp)
     14a:       8b 75 0c                mov    0xc(%ebp),%esi
     14d:       83 7d 0c 00             cmpl   $0x0,0xc(%ebp)
     151:       74 1a                   je     16d <TestData_Checker_IsDateTime_object+0x3d>
     153:       8b 45 f4                mov    -0xc(%ebp),%eax
     156:       8b 00                   mov    (%eax),%eax
     158:       8b 00                   mov    (%eax),%eax
     15a:       8b 40 08                mov    0x8(%eax),%eax
     15d:       8b 48 08                mov    0x8(%eax),%ecx
     160:       8b 93 10 00 00 00       mov    0x10(%ebx),%edx
     166:       33 c0                   xor    %eax,%eax
     168:       3b ca                   cmp    %edx,%ecx
     16a:       0f 45 f0                cmovne %eax,%esi
     16d:       85 f6                   test   %esi,%esi
     16f:       0f 97 c0                seta   %al
     172:       0f b6 c0                movzbl %al,%eax
     175:       8d 65 f8                lea    -0x8(%ebp),%esp
     178:       5e                      pop    %esi
     179:       5b                      pop    %ebx
     17a:       c9                      leave  
     17b:       c3                      ret    
     17c:       8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi