Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is a 'With ... End With' really more efficient?

Tags:

vb.net

cil

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.

like image 521
Kumba Avatar asked Dec 09 '22 13:12

Kumba


2 Answers

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.

like image 194
Guffa Avatar answered Dec 17 '22 10:12

Guffa


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).

like image 25
Cody Gray Avatar answered Dec 17 '22 10:12

Cody Gray