问题
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