So I'm playing with ILDASM and noticed an oddity that I can't find a really good explanation for on Google.
It seems that when using With blocks in VB.NET, the resulting MSIL larger than w/o. So this leads me to ask, are With Blocks really more efficient? MSIL is what gets JITed into native machine code, so smaller code size should imply more efficient code, right?
Here's a sample of two classes (Class2 and Class3), which set the same values for an instance of Class1. Class2 does it without a With block, while Class3 uses With. Class1 has six properties, touching 6 private members. Each member is of a specific data type, and it's all a part of this testcase.
Friend Class Class2
Friend Sub New()
Dim c1 As New Class1
c1.One = "foobar"
c1.Two = 23009
c1.Three = 3987231665
c1.Four = 2874090071765301873
c1.Five = 3.1415973801462975
c1.Six = "a"c
End Sub
End Class
Friend Class Class3
Friend Sub New()
Dim c1 As New Class1
With c1
.One = "foobar"
.Two = 23009
.Three = 3987231665
.Four = 2874090071765301873
.Five = 3.1415973801462975
.Six = "a"c
End With
End Sub
End Class
Here's the resulting MSIL for Class2:
.method assembly specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 84 (0x54)
.maxstack 2
.locals init ([0] class WindowsApplication1.Class1 c1)
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: newobj instance void WindowsApplication1.Class1::.ctor()
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: ldstr "foobar"
IL_0012: callvirt instance void WindowsApplication1.Class1::set_One(string)
IL_0017: ldloc.0
IL_0018: ldc.i4 0x59e1
IL_001d: callvirt instance void WindowsApplication1.Class1::set_Two(int16)
IL_0022: ldloc.0
IL_0023: ldc.i4 0xeda853b1
IL_0028: callvirt instance void WindowsApplication1.Class1::set_Three(uint32)
IL_002d: ldloc.0
IL_002e: ldc.i8 0x27e2d1b1540c3a71
IL_0037: callvirt instance void WindowsApplication1.Class1::set_Four(uint64)
IL_003c: ldloc.0
IL_003d: ldc.r8 3.1415973801462975
IL_0046: callvirt instance void WindowsApplication1.Class1::set_Five(float64)
IL_004b: ldloc.0
IL_004c: ldc.i4.s 97
IL_004e: callvirt instance void WindowsApplication1.Class1::set_Six(char)
IL_0053: ret
} // end of method Class2::.ctor
And here is the MSIL for Class3:
.method assembly specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 88 (0x58)
.maxstack 2
.locals init ([0] class WindowsApplication1.Class1 c1,
[1] class WindowsApplication1.Class1 VB$t_ref$L0)
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: newobj instance void WindowsApplication1.Class1::.ctor()
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: stloc.1
IL_000e: ldloc.1
IL_000f: ldstr "foobar"
IL_0014: callvirt instance void WindowsApplication1.Class1::set_One(string)
IL_0019: ldloc.1
IL_001a: ldc.i4 0x59e1
IL_001f: callvirt instance void WindowsApplication1.Class1::set_Two(int16)
IL_0024: ldloc.1
IL_0025: ldc.i4 0xeda853b1
IL_002a: callvirt instance void WindowsApplication1.Class1::set_Three(uint32)
IL_002f: ldloc.1
IL_0030: ldc.i8 0x27e2d1b1540c3a71
IL_0039: callvirt instance void WindowsApplication1.Class1::set_Four(uint64)
IL_003e: ldloc.1
IL_003f: ldc.r8 3.1415973801462975
IL_0048: callvirt instance void WindowsApplication1.Class1::set_Five(float64)
IL_004d: ldloc.1
IL_004e: ldc.i4.s 97
IL_0050: callvirt instance void WindowsApplication1.Class1::set_Six(char)
IL_0055: ldnull
IL_0056: stloc.1
IL_0057: ret
} // end of method Class3::.ctor
The only major difference I can discern at a glance is the use of the ldloc.1
opcode over ldloc.0
. Per MSDN, the difference between these two is negligible, with ldloc.0
being an efficient method of using ldloc
to access a local variable at index 0, and ldloc.1
being the same, just for index 1.
Note that Class3's code size is 88 versus 84. These are from the Release/Optimized builds. Built in VB Express 2010, .NET 4.0 Framework Client Profile.
Thoughts?
EDIT:
Wanted to add for those stumbling on this thread the generic gist of the answers, as I understand them.
Sensible use of With ... End With
:
With ObjectA.Property1.SubProperty7.SubSubProperty4
.SubSubSubProperty1 = "Foo"
.SubSubSubProperty2 = "Bar"
.SubSubSubProperty3 = "Baz"
.SubSubSubProperty4 = "Qux"
End With
Non-sensible use of With ... End With
:
With ObjectB
.Property1 = "Foo"
.Property2 = "Bar"
.Property3 = "Baz"
.Property4 = "Qux"
End With
The reason is because with ObjectA's example, you're going several members down, and each resolution of that member takes some work, so by only resolving the references one time and sticking the final reference into a temp variable (which is all that With
really does), this speeds up accessing the properties/methods hidden deep in that object.
ObjectB is not as efficient because you're only going one level deep. Each resolution is about the same as accessing the temp reference created by the With
statement, so there is little-to-no gain in performance.
Looking at the IL code, what the With
block does is basically:
Friend Class Class3
Friend Sub New()
Dim c1 As New Class1
Dim temp as Class1 = c1
temp.One = "foobar"
temp.Two = 23009
temp.Three = 3987231665
temp.Four = 2874090071765301873
temp.Five = 3.1415973801462975
temp.Six = "a"c
temp = Nothing
End Sub
End Class
But what's important is what the JIT compiler makes of this. The language compiler doesn't do much optimisation, that is mainly left for the JIT compiler. Most likely it will see that the variable c1
isn't used for anything other than creating another variable, and optimise away the storage of c1
completely.
Either way, if it does still create another variable, that is a very cheap operation. If there is any performance difference it's very small, and it can fall either way.
This is the instructive section, from the class that uses the With statement:
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: stloc.1
IL_000e: ldloc.1
The zero-indexed instructions appear in the class which does not use the With statement as well, and they correspond to the instantiation of c1 in the source (Dim c1 As New Class1
)
The one-indexed instructions in the class that does use the With statement indicates that a new local variable is created on the stack. That's what the With statement does: behind the scenes, it instantiates a local copy of the object referenced in the With statement. The reason this can improve performance is if accessing the instance is a costly operation, the same reason as caching a local copy of a property can improve performance. The object itself doesn't have to be retrieved again each time one of its properties is changed.
You also observe that you see ldloc.1
instead of ldloc.0
in the IL for the class that uses the With statement. This is because the reference to the local variable created by the With statement (the second variable in the evaluation stack) is being used, as opposed to the first variable in the evaluation stack (the instantiation of Class1 as the variable c1
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With