The Is, As and the Is-As - WinDBG

In the previous post, I have investigated the performance implications of using the is and as keywords, including the new pattern matching syntax for the is keyword. Let me reference a previous work in this topic from Sasha Goldshtein, which motivated to do the investigation on the new pattern matching syntax of the is keyword: Micro-Benchmarking Done Wrong, And For The Wrong Reasons

In the previous post, I have measured performance with micro-benchmarking and took a look at the code, through ILSpy.

In this post I will take a look at the same code (invoking a non-inlined Work() method) but this time, I will use WinDBG and compare the native code generated by the JIT compiler.

JIT compiler's output may change with version updates of the framework, as this is an implementation detail to the CLR. The generated code also depends on the architecture of the system where we run our code. My test machine is x64 and uses RyuJIT compiler.

To look at the "jitted" code, I attach the windbg to the application waiting on a Console.ReadLine(). Let's load the SOS extension and take a look at the CLR stack:

.loadby sos clr
!clrstack

Which results in the following output below. Note that we stand on a Console.ReadLine() at this point.

OS Thread Id: 0x19c (0)
        Child SP               IP Call Site
0000001c92fce7a8 00007ff899b5ff44 [InlinedCallFrame: 0000001c92fce7a8] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000001c92fce7a8 00007ff87e125326 [InlinedCallFrame: 0000001c92fce7a8] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000001c92fce770 00007ff87e125326 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0000001c92fce850 00007ff87e96014a System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
0000001c92fce8e0 00007ff87e960054 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
0000001c92fce940 00007ff87e0f7a44 System.IO.StreamReader.ReadBuffer()
0000001c92fce990 00007ff87e0f77e3 System.IO.StreamReader.ReadLine()
0000001c92fcea00 00007ff87eb134ce System.IO.TextReader+SyncTextReader.ReadLine()
0000001c92fcea60 00007ff87e8f7057 System.Console.ReadLine()
0000001c92fcea90 00007ff82afd04cd ConsoleApp.Program.Main(System.String[]) [C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 61]
0000001c92fcecf0 00007ff88a5e5863 [GCFrame: 0000001c92fcecf0]

We can click the given method Program.Main to see the native instructions that got compiled. Let's see the compiled method in each case.

The type checking is keyword

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 22:
00007ff8`2afd0497 488bd6          mov     rdx,rsi
00007ff8`2afd049a 48b9a85aec2af87f0000 mov rcx,7FF82AEC5AA8h (MT: ConsoleApp.My)
00007ff8`2afd04a4 e89736615f      call    clr!JIT_IsInstanceOfClass (00007ff8`8a5e3b40)
00007ff8`2afd04a9 4885c0          test    rax,rax
00007ff8`2afd04ac 741a            je      00007ff8`2afd04c8

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 24:
00007ff8`2afd04ae 488bce          mov     rcx,rsi
00007ff8`2afd04b1 48b8a85aec2af87f0000 mov rax,7FF82AEC5AA8h (MT: ConsoleApp.My)
00007ff8`2afd04bb 483901          cmp     qword ptr [rcx],rax
00007ff8`2afd04be 7403            je      00007ff8`2afd04c3
00007ff8`2afd04c0 488bce          mov     rcx,rsi
00007ff8`2afd04c3 e8d8fbffff      call    00007ff8`2afd00a0 (ConsoleApp.My.Work(), mdToken: 0000000006000005)

What happens, is that we load the ConsoleApp.My type, check that the object is this type. The 'test' instrcution tests the result of InstanceOfClass. Then we load the My type again, but before calling the Work() method, we need to make a compare to check if our object is the given type (this is the casting in c#). This way the framework can throw an exception if there is a type mismatch.

The as keyword

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 29:
00007ff8`2b000496 48b9a85aef2af87f0000 mov rcx,7FF82AEF5AA8h (MT: ConsoleApp.My)
00007ff8`2b0004a0 e89b365e5f      call    clr!JIT_IsInstanceOfClass (00007ff8`8a5e3b40)
00007ff8`2b0004a5 488bc8          mov     rcx,rax

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 30:
00007ff8`2b0004a8 4885c9          test    rcx,rcx
00007ff8`2b0004ab 7405            je      00007ff8`2b0004b2

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 32:
00007ff8`2b0004ad e8eefbffff      call    00007ff8`2b0000a0 (ConsoleApp.My.Work(), mdToken: 0000000006000005)

When we repeat the same investigation for the as keyword, we see that the first part of the native code, is quite similar, however the second type check is completely omitted, hence this code will run faster.

The is with pattern matching

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 36:
00007ff8`2aff0496 48b9a85aee2af87f0000 mov rcx,7FF82AEE5AA8h (MT: ConsoleApp.My)
00007ff8`2aff04a0 e89b365f5f      call    clr!JIT_IsInstanceOfClass (00007ff8`8a5e3b40)
00007ff8`2aff04a5 4885c0          test    rax,rax
00007ff8`2aff04a8 7408            je      00007ff8`2aff04b2

C:\Users\...\documents\visual studio 2017\Projects\ConsoleApp\ConsoleApp\Program.cs @ 38:
00007ff8`2aff04aa 488bc8          mov     rcx,rax
00007ff8`2aff04ad e8eefbffff      call    00007ff8`2aff00a0 (ConsoleApp.My.Work(), mdToken: 0000000006000005)

In this case we see a very familiar structure to the as use case, hence it is being faster compared to the regular is for type checking. Our tests also showed that is has pretty similar execution time as the as keyword.