4

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.

Kumba
  • 2,390
  • 3
  • 33
  • 60
  • 6
    "so smaller code size should imply more efficient code, right?" haha... no. – Ed S. Nov 04 '10 at 06:33
  • 3
    I thought that with/end with were just for convenience ! – Marlon Nov 04 '10 at 06:36
  • This doesn't look like it is an optimized build. In particular the `stloc.x`/`ldloc.x` combinations should be optimized away. (`stloc.x` pops into the `x` local, and `ldloc.0` loads it back) – porges Nov 05 '10 at 03:04
  • Yeah, it's from the debug build, but I was testing out With blocks at work on a project, and noticed that even with the release build, the code size of a given function/method in ILDASM was still larger than without, which (at the time), led me to question what was going on. – Kumba Nov 06 '10 at 07:24

5 Answers5

5

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.

Guffa
  • 687,336
  • 108
  • 737
  • 1,005
3

I don't use things like with to make my code faster. Any decent compiler should generate exactly the same code. I'd be surprised if any compiler nowadays didn't common subexpression elimination, so that:

                 with a.b.c:
a.b.c.d = 1;         .d = 1;
a.b.c.e = 2;         .e = 2;
a.b.c.f = 3;         .f = 3;
                 end with

were identical in terms of what was generated under the covers. This wouldn't be the first time Microsoft have surprised me though :-)

I use things like that to make my source code more readable, which is reason enough. I don't want a huge morass of code when I have to come back in six months to fix a subtle bug. I want it clean and readable.


Now it may be that your MSIL code is not optimised to the same thing simply because it hasn't been deemed necessary yet. You mentioned the JIT compiler so it probably makes sense to defer any optimisation until then.

Once the decision has been made to JIT this piece of code (because of its heavy use for example), that would be the point where I would start applying optimisations. That way, your compiler can be simpler in that it doesn't have to worry about a lot of optimisation where it may not be needed: YAGNI.

Note that this is just supposition on my part, I don't speak for Microsoft.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • Yeah, I'm familiar with the aesthetic purpose of With blocks. I'm interested in the efficiency aspect mostly. Per documentation, with is supposed to hold a refernece to the parent object so that you don't have to constantly re-qualify the name.In VB6/VBA, however, you had to weigh the tradeoff of multiple qualifications over the loading of that one reference. So unless you had something like, three or more qualifications, only then did the use of a With block make sense. With .NET, however, it looks like the compiler is really aggressive, and possibly eliminates the need for With blocks. – Kumba Nov 04 '10 at 06:59
  • That all said above, I'm also partially interested in better understanding why it generated the MSIL code that it did for the two different classes. – Kumba Nov 04 '10 at 07:01
  • Is it possible to play with the JIT compiler at all to test optimizations out on it? Or is MSIL the lowest one can go in .NET? – Kumba Nov 04 '10 at 07:17
  • No idea, @Kumba, you're moving well beyond my comfort zone now. While I'm happy to eat .NET, I'm in no way interested in what it's made from :-) – paxdiablo Nov 04 '10 at 07:22
  • I've got a habit of taking things apart, so I ask questions like this a lot. x86/x64 assembler is horrific to look at anyways. MIPS assembler is much more readable (looks very similar to MSIL, actually). – Kumba Nov 04 '10 at 07:37
  • @Kumba: I don't believe there's any way to customize how the JIT compiler works (other than turning it on or off), but you can look at JIT-optimized code. Just attach Visual Studio to a running process of the release build of the app you want to study. You have to start the app outside of Visual Studio first because JIT optimizations are disabled by default when a process is started under the debugger. – Cody Gray - on strike Nov 04 '10 at 07:37
  • @Cody Gray: Doubt that I can do that with the Express copy of VB 2010. I'll have to save up for Pro at some point. – Kumba Nov 04 '10 at 07:45
  • @Kumba: True. Unless you happen to be a student (www.dreamspark.com) :-) – Cody Gray - on strike Nov 04 '10 at 07:48
  • @Cody Gray: Definitely not one of those. I got out of that quite a few years ago with pieces of paper that say I'm smart at something, which was enough to trick someone into hiring me ;) – Kumba Nov 04 '10 at 07:54
  • The subexpression elimination you suggest is unsafe in general. For instance, imagine that `b` is a property which returns a new object each time it's accessed - then the code on the left could behave differently from the code on the right. – kvb Nov 04 '10 at 14:38
  • 1
    Well, then it wouldn't actually _be_ a common subexpression. I wouldn't expect an optimiser to be doing that sort of stuff unless it knew it was safe, the default action where it can't tell is to leave it unoptimised. This would be no different to other optimisers like gcc. – paxdiablo Nov 04 '10 at 14:45
3

The With statement actually adds more code to ensure that it remains semantically correct.

If you have modified your code as such:

Dim c1 As New Class1
With c1
    .One = "foobar"
    .Two = 23009
    .Three = 3987231665
    .Four = 2874090071765301873
    .Five = 3.1415973801462975
    c1 = New Class1
    .Six = "a"c
End With

You would, I hope, expect that the .Six property is still assigned to the original c1 and not the second one.

So, under the hood the compiler does this:

Dim c1 As New Class1
Dim VB$t_ref$L0 As Class1 = c1
VB$t_ref$L0.One = "foobar"
VB$t_ref$L0.Two = &H59E1
VB$t_ref$L0.Three = &HEDA853B1
VB$t_ref$L0.Four = &H27E2D1B1540C3A71
VB$t_ref$L0.Five = 3.1415973801462975
VB$t_ref$L0.Six = "a"c
VB$t_ref$L0 = Nothing

It creates a copy of the With variable so that any subsequent assignments do not change the semantics.

The last thing it does it sets the reference to the copied variable to Nothing to allow it to be garbage collected (in odd cases where this is useful in the middle of a procedure).

In effect, it adds a single Nothing assignment to your code that the original code didn't have or need.

The performance difference is negligible. Only use With if it aids readability.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • What happened to the `c1 = New Class1` in the "under the cover" version? Or am I missing something obvious? – paxdiablo Nov 04 '10 at 07:07
  • This is a good point. I've been burned by the copy of the reference that a With block does when I was working in VB6. I've so far managed to avoid a similar mistake in VB.NET. – Kumba Nov 04 '10 at 07:10
  • 1
    actually, the Nothing assignment at the end does nothing useful (JIT would indicate to GC that the variable is unused in the remaining body of the method, so it's not a live root anyway) – Damien_The_Unbeliever Nov 04 '10 at 07:17
  • @`paxdiablo` - Sorry, I was decompiling the original code, not my "modified" version. – Enigmativity Nov 04 '10 at 11:17
  • @Damien_The_Unbeliever: If the With statement is executed within a long-time-per-iteration loop, setting the temp to Nothing will allow it to be garbage-collected within the loop even if the compiler couldn't tell its value wouldn't get reused. The set-to-null may also be relevant (though perhaps not in a good way) if a With statement is used within a lambda expression. – supercat Nov 15 '10 at 19:16
3

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

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
  • This is the answer I'm looking for (but great points from others). I've been using caching a lot in a project I'm working on (which prompted this particular question), and in some cases, I'm caching/using 3-4 properties/objects simultaneously. Effectively doing what you describe the With block as doing, just more than one-at-a-time. – Kumba Nov 04 '10 at 07:14
  • Also, by "costly operation", what would be an example of that? – Kumba Nov 04 '10 at 07:15
  • I totally understand your curiosity. I've always just known it worked this way, but never took the time to compare the two versions of IL. Regardless, paxdiablo's answer is important to keep in mind as well: the most important thing is making your source code readable. The With statment is syntactic sugar for doing the same thing you could be doing manually, but it's generally much more readable than something like "Dim aTempVariable as Class1 = c1". As several others have pointed out, if the With block is truly creating a second, unnecessary cached copy, it will most likely be JITted out. – Cody Gray - on strike Nov 04 '10 at 07:20
  • 1
    The classic example of a costly operation, especially in VB 6, is a database operation. Retrieving a value from a database is always one of the most time-consuming steps in your code, and the With statement gave VB programmers a way to cache its result without having to understand the ins and outs of how it all worked behind the scenes. – Cody Gray - on strike Nov 04 '10 at 07:23
  • Yeah, I like pax's answer, too. I can only flag one answer as the "right" answer on this site, though :) (no "assisted" answers like EE I guess). – Kumba Nov 04 '10 at 07:35
  • @`Cody Gray` - Can you elaborate on your answer as I do not see the caching you talk about in the IL code? Loading from index `0` versus `1` doesn't make a difference that I can tell? – Enigmativity Nov 04 '10 at 11:16
  • 1
    @Enigmativity: The ldloc.x instructions encode for an access of a local variable on the stack at the specified index. What I'm saying is that when the With keyword is not used, the IL is accessing the class instance from the first local variable on the stack (index 0), whereas when the With keyword is used, the SECOND local variable that is created by the With command (index 1) is being used to access the class instance. The first variable (index 0) still exists in both cases, as it's being created with the Dim keyword at the top of both functions, but it's unused when the With is used. – Cody Gray - on strike Nov 04 '10 at 12:18
  • 1
    Basically, the index values 0 and 1 actually indicate two different variables on the stack. See MSDN: http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldloc.aspx. It doesn't make a difference in terms of execution or what data is being modified on the stack, because both point to the exact same class, but they are two different references, just as if you had wrote "Dim a as Integer = 1, Dim b as Integer = a". – Cody Gray - on strike Nov 04 '10 at 12:25
  • And basically, I see the flaw in my logic with the use of the With statement. Since I'm only caching a copy of a single reference, the net savings is not there, in fact, this is why the first bit of code is more efficient. Now if I was accesing a chain of objects over and over again (`Obj1.Obj2.Obj3.Property`), caching that with the With statement would probably generate a savings that would be modeled in the resulting MSIL output. – Kumba Nov 04 '10 at 16:09
  • @`Cody Gray` - There is no improvement on caching another local variable, but, as `Kumba` pointed out, a chained reference does given an improvement. I created a timing test and worked out that there is a 2.6% improvement using With when a chained reference was four-levels deep (`a.B.C.D.One = "foobar"`). Going that deep is rare and the benefit is not great. – Enigmativity Nov 05 '10 at 01:28
  • @`Cody Gray` - Also I was assigning six properties in my test (just like the original question) so the difference amounted to an extra 18 `ldfld` IL calls per iteration of my test. According to my calculations my machine could perform in excess of 7.5 billion such calls per second. The performance benefit using `With` for its "caching" is, in practical terms, purely academic. – Enigmativity Nov 05 '10 at 01:57
  • @Enigmativity: I think I agree completely with what you're saying. My answer was not claiming there is any performance improvement to be found with the second piece of code. It's redundant and would most likely be optimized out by the JITter. As I said above, one of the few places you could find a performance improvement is accessing a field in a database or a chained reference multiple levels deep (which arguably indicates a design in need of rethinking). I was simply trying to explain the difference in the IL code produced for the asker; don't misunderstand my answer as advocating for – Cody Gray - on strike Nov 05 '10 at 04:35
  • the use of the With statement. I think more than anything it's a feature carried over from earlier versions of VB (pre-.NET), where it probably made more of a difference than on modern processors, or even was simply there for the convenience of a developer that would never have cared enough to look at IL anyway. :-) – Cody Gray - on strike Nov 05 '10 at 04:36
0

This depends on how you use it. If you use:

With someobject.SomeHeavyProperty
   .xxx 
End With

the With statement would save some call to the property getter. Otherwise, the effect should be nullified by the JIT.

J-16 SDiZ
  • 26,473
  • 4
  • 65
  • 84