是'With ... End With'真的更有效率吗?

时间:2010-11-04 06:30:55

标签: vb.net cil

所以我正在玩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语句创建的临时引用大致相同,因此性能几乎没有增益。

5 个答案:

答案 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会使效果无效。