MSIL Disassembler Investigation

DISCLAIMER: This post is part of an initial investigation. So please point out any errors in my evaluation of the problem and solution.

Today at work, a co-worker and I got into a discussion regarding inheriting methods vs helper classes. I sided on the inheriting method. I argued that helper classes could lead to a “kitchen sink” helper or a lot of classes that developers would need to sift through to find the method they need.

A little context may be needed here. On our project, we have a base test case class that has methods for creating domain objects. These methods are to be used by individual test methods in child classes. The methods help build up targeted domain objects for each test case. The discussion revolved around compile-time, code speed and code size.

So tonight I decided to do some investigation to see if I was just talking crap. I fully admit that I talk a lot of it at times. To help educate myself, I dove into using the MSIL Disassembler (Ildasm.exe1. I wrote a sample project that had 3 styles of code architecture; inheritance, static helpers and non-static helpers. The results were interesting.

Reviewing the Hello() method of the 3 types. Pay attention to the bold sections.

Child

 1.method public hidebysig instance class TestCompile.DomainObject 
 2        Hello(string world) cil managed
 3{
 4  // Code size       33 (0x21)
 5  .maxstack  2
 6  .locals init ([0] class TestCompile.DomainObject '<>g__initLocal0',
 7           [1] class TestCompile.DomainObject CS$1$0000)
 8  IL_0000:  nop
 9  IL_0001:  newobj     instance void TestCompile.DomainObject::.ctor()
10  IL_0006:  stloc.0
11  IL_0007:  ldloc.0
12  IL_0008:  ldstr      "1"
13  IL_000d:  callvirt   instance void TestCompile.DomainObject::set_Id(string)
14  IL_0012:  nop
15  IL_0013:  ldloc.0
16  IL_0014:  ldarg.1
17  IL_0015:  callvirt   instance void TestCompile.DomainObject::set_Name(string)
18  IL_001a:  nop
19  IL_001b:  ldloc.0
20  IL_001c:  stloc.1
21  IL_001d:  br.s       IL_001f
22  IL_001f:  ldloc.1
23  IL_0020:  ret
24} // end of method Parent::Hello

Non-Static Helper

 1.method public hidebysig instance class TestCompile.DomainObject 
 2        Hello(string world) cil managed
 3{
 4  // Code size       33 (0x21)
 5  .maxstack  2
 6  .locals init ([0] class TestCompile.DomainObject '<>g__initLocal0',
 7           [1] class TestCompile.DomainObject CS$1$0000)
 8  IL_0000:  nop
 9  IL_0001:  newobj     instance void TestCompile.DomainObject::.ctor()
10  IL_0006:  stloc.0
11  IL_0007:  ldloc.0
12  IL_0008:  ldstr      "1"
13  IL_000d:  callvirt   instance void TestCompile.DomainObject::set_Id(string)
14  IL_0012:  nop
15  IL_0013:  ldloc.0
16  IL_0014:  ldarg.1
17  IL_0015:  callvirt   instance void TestCompile.DomainObject::set_Name(string)
18  IL_001a:  nop
19  IL_001b:  ldloc.0
20  IL_001c:  stloc.1
21  IL_001d:  br.s       IL_001f
22  IL_001f:  ldloc.1
23  IL_0020:  ret
24} // end of method Helper1::Hello

Static Helper

 1.method public hidebysig static class TestCompile.DomainObject 
 2        Hello(string world) cil managed
 3{
 4  // Code size       33 (0x21)
 5  .maxstack  2
 6  .locals init ([0] class TestCompile.DomainObject '<>g__initLocal0',
 7           [1] class TestCompile.DomainObject CS$1$0000)
 8  IL_0000:  nop
 9  IL_0001:  newobj     instance void TestCompile.DomainObject::.ctor()
10  IL_0006:  stloc.0
11  IL_0007:  ldloc.0
12  IL_0008:  ldstr      "1"
13  IL_000d:  callvirt   instance void TestCompile.DomainObject::set_Id(string)
14  IL_0012:  nop
15  IL_0013:  ldloc.0
16  IL_0014:  ldarg.0
17  IL_0015:  callvirt   instance void TestCompile.DomainObject::set_Name(string)
18  IL_001a:  nop
19  IL_001b:  ldloc.0
20  IL_001c:  stloc.1
21  IL_001d:  br.s       IL_001f
22  IL_001f:  ldloc.1
23  IL_0020:  ret
24} // end of method StaticHelper1::Hello

The parts in bold describe the code size, max deep of the stack and the local variables created. Pretty much identical. Looking strictly at this data it appears that there is no gain in moving the methods to separate classes.

The same is true for the Goodbye() method.

Next, let’s take a look at the test class method.

Child

 1.method public hidebysig instance void  DoSomething() cil managed
 2{
 3  // Code size       26 (0x1a)
 4  .maxstack  2
 5  .locals init ([0] class TestCompile.DomainObject hello,
 6           [1] class TestCompile.DomainObject goodbye)
 7  IL_0000:  nop
 8  IL_0001:  ldarg.0
 9  IL_0002:  ldstr      "child 2"
10  IL_0007:  call       instance class TestCompile.DomainObject TestCompile.Parent::Hello(string)
11  IL_000c:  stloc.0
12  IL_000d:  ldarg.0
13  IL_000e:  ldstr      "child 2"
14  IL_0013:  call       instance class TestCompile.DomainObject TestCompile.Parent::Goodbye(string)
15  IL_0018:  stloc.1
16  IL_0019:  ret
17} // end of method ChildTest::DoSomething

Non-Static Helper

 1.method public hidebysig instance void  DoSomething() cil managed
 2{
 3  // Code size       34 (0x22)
 4  .maxstack  2
 5  .locals init ([0] class TestCompile.DomainObject hello,
 6           [1] class TestCompile.DomainObject goodbye)
 7  IL_0000:  nop
 8  IL_0001:  newobj     instance void TestCompile.Helper1::.ctor()
 9  IL_0006:  ldstr      "child 2"
10  IL_000b:  call       instance class TestCompile.DomainObject TestCompile.Helper1::Hello(string)
11  IL_0010:  stloc.0
12  IL_0011:  newobj     instance void TestCompile.Helper2::.ctor()
13  IL_0016:  ldstr      "child 2"
14  IL_001b:  call       instance class TestCompile.DomainObject TestCompile.Helper2::Goodbye(string)
15  IL_0020:  stloc.1
16  IL_0021:  ret
17} // end of method HelperTest::DoSomething

Static Helper

 1.method public hidebysig instance void  DoSomething() cil managed
 2{
 3  // Code size       24 (0x18)
 4  .maxstack  1
 5  .locals init ([0] class TestCompile.DomainObject hello,
 6           [1] class TestCompile.DomainObject goodbye)
 7  IL_0000:  nop
 8  IL_0001:  ldstr      "child 2"
 9  IL_0006:  call       class TestCompile.DomainObject TestCompile.StaticHelper1::Hello(string)
10  IL_000b:  stloc.0
11  IL_000c:  ldstr      "child 2"
12  IL_0011:  call       class TestCompile.DomainObject TestCompile.StaticHelper2::Goodbye(string)
13  IL_0016:  stloc.1
14  IL_0017:  ret
15} // end of method StaticHelperTest::DoSomething

It appears that static helper classes might be the winner. There are 2 less operations than the inheritance model and uses a little less memory. The difference appears to be the ldarg.0 operation. Since instance methods have an implicit parameter (“this”), it is loaded first onto the stack.

I don’t fully understand all that I am seeing in the MSIL output. But from this simple example it appears that I was wrong from a technical perspective. I concede on that side of the argument. However, my other concerns about the human developer are still valid (ie: kitchen-sink and/or a maze of confusing helpers).

The human side can be addressed. The technical side has data to back it up.

1 The java equivalent to the MSIL decompiler is the “javap” command. I would love to try this experiment in java to see if the results are similar._

Full MSIL Output
Project Files

comments powered by Disqus