所以我正在玩ILDASM,并注意到一个奇怪的是我无法在Google上找到一个非常好的解释。
似乎在VB.NET中使用With块时,得到的MSIL大于w / o。所以这让我想问,With Blocks真的更有效吗? MSIL是JITed的本机代码,所以代码量越小意味着代码效率越高,对吧?
这是两个类(Class2和Class3)的示例,它们为Class1的实例设置相同的值。 Class2没有With块,而Class3使用With。 Class1有六个属性,涉及6个私有成员。每个成员都是特定的数据类型,它都是这个测试用例的一部分。
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
以下是Class2产生的MSIL:
.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
这是Class3的MSIL:
.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
我能够一眼就看出的唯一主要区别是使用ldloc.1
操作码而不是ldloc.0
。根据MSDN,这两者之间的差异可以忽略不计,ldloc.0
是使用ldloc
访问索引0处的局部变量的有效方法,而ldloc.1
是相同的,仅用于索引1。
请注意,Class3的代码大小为88而不是84.这些来自发布/优化版本。内置于VB Express 2010,.NET 4.0 Framework Client Profile。
思考?
修改
根据我的理解,他们希望为这些线索上的绊脚石添加答案的通用要点。
明智地使用With ... End With
:
With ObjectA.Property1.SubProperty7.SubSubProperty4
.SubSubSubProperty1 = "Foo"
.SubSubSubProperty2 = "Bar"
.SubSubSubProperty3 = "Baz"
.SubSubSubProperty4 = "Qux"
End With
不明智地使用With ... End With
:
With ObjectB
.Property1 = "Foo"
.Property2 = "Bar"
.Property3 = "Baz"
.Property4 = "Qux"
End With
原因是因为使用ObjectA的例子,你会让几个成员失望,并且该成员的每个分辨率都需要一些工作,因此只需要一次解析引用并将最终引用粘贴到一个临时变量(这是全部With
确实这样做了,这加快了访问该对象深处隐藏的属性/方法的速度。
ObjectB效率不高,因为你的深度只有一个级别。每个分辨率与访问With
语句创建的临时引用大致相同,因此性能几乎没有增益。
答案 0 :(得分:5)
查看IL代码,With
块的作用基本上是:
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
但重要的是JIT编译器对此做了什么。语言编译器没有做太多优化,主要是留给JIT编译器。最有可能的是,变量c1
不会用于创建另一个变量以外的任何变量,并且会完全优化c1
的存储。
无论哪种方式,如果它仍然创建另一个变量,那是一个非常便宜的操作。如果有任何性能差异,它会非常小,并且可能会出现任何一种情况。
答案 1 :(得分:3)
我不使用with
之类的东西来加快代码的运行速度。任何体面的编译器都应该生成相同的代码。如果现在任何编译器都没有消除常见的子表达式,我会感到惊讶,因此:
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
在封面下产生的内容方面是相同的。这不是微软让我感到惊讶的第一次时间: - )
我使用这样的东西来使我的源代码更具可读性,这是足够的理由。当我必须在六个月内回来修复一个微妙的bug时,我不想要大量的代码。我希望它清洁可读。
现在可能是因为它尚未被认为是必要的,因此您的MSIL代码不优化到同一件事。您提到了JIT编译器,因此在此之前推迟任何优化可能是有意义的。
一旦决定对这段代码进行JIT决定(例如因为它的用量很大),那么我将开始应用优化。这样,您的编译器可以更简单,因为它不需要担心可能不需要的大量优化:YAGNI。
请注意,这只是我的假设,我不代表微软。
答案 2 :(得分:3)
With
语句实际上添加了更多代码,以确保它在语义上保持正确。
如果您修改了代码:
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
我希望您希望.Six
属性仍然分配给原始c1
,而不是第二个。
因此,编译器在这个框架下执行此操作:
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
它会创建With
变量的副本,以便任何后续分配都不会更改语义。
它做的最后一件事是将对复制的变量的引用设置为Nothing
以允许它被垃圾收集(在奇怪的情况下,这在过程中很有用)。
实际上,它会为您的代码添加一个Nothing
分配,原始代码没有或不需要。
性能差异可以忽略不计。只有在有助于提高可读性时才使用With
。
答案 3 :(得分:3)
这是使用With语句的类的指导性部分:
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: stloc.1
IL_000e: ldloc.1
零索引指令出现在不使用With语句的类中,它们对应于源中的c1实例化(Dim c1 As New Class1
)
中使用With语句的类中的单索引指令表示在堆栈上创建了新的局部变量。这就是With语句的作用:在幕后,它实例化With语句中引用的对象的本地副本。这可以提高性能的原因是,如果访问实例是一项代价高昂的操作,与缓存属性的本地副本相同的原因可以提高性能。每次更改其中一个属性时,不必再次检索对象本身。
您还发现,对于使用With语句的类,您在IL中看到ldloc.1
而不是ldloc.0
。这是因为正在使用由With语句(评估堆栈中的第二个变量)创建的局部变量的引用,而不是评估堆栈中的第一个变量(将Class1实例化为变量c1
)。
答案 4 :(得分:0)
这取决于您如何使用它。如果您使用:
With someobject.SomeHeavyProperty
.xxx
End With
With
语句会保存对属性getter的一些调用。否则,JIT会使效果无效。