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

青春壹個敷衍的年華 提交于 2019-12-23 12:13:40

问题


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.


回答1:


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




回答2:


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.




回答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.




回答4:


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.




回答5:


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.



来源:https://stackoverflow.com/questions/4094378/is-a-with-end-with-really-more-efficient

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!