【问题标题】:Is a 'With ... End With' really more efficient?'With ... End With' 真的更有效吗?
【发布时间】:2010-11-04 06:30:55
【问题描述】:

所以我在玩 ILDASM 时发现了一个奇怪的现象,我在 Google 上找不到很好的解释。

似乎在 VB.NET 中使用 With 块时,生成的 MSIL 大于 w/o。所以这让我问,With Blocks 真的更高效吗? MSIL 是 JIT 到本机机器代码的原因,因此更小的代码大小应该意味着更高效的代码,对吧?

这是两个类(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 语句创建的临时引用大致相同,因此性能几乎没有提升。

【问题讨论】:

  • “所以更小的代码大小应该意味着更高效的代码,对吧?”哈哈……没有。
  • 我认为 with/end with 只是为了方便!
  • 这看起来不像是优化的构建。特别是 stloc.x/ldloc.x 组合应该被优化掉。 (stloc.x 弹出到 x 本地,ldloc.0 将其加载回来)
  • 是的,它来自调试版本,但我正在测试项目中的 With blocks at work,并注意到即使使用发布版本,ILDASM 中给定函数/方法的代码大小也是仍然比没有更大,这(当时)让我质疑发生了什么。

标签: vb.net cil


【解决方案1】:

看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 的存储。

无论哪种方式,如果它仍然创建另一个变量,那是一个非常便宜的操作。如果有任何性能差异,它会非常小,并且可能会下降。

【讨论】:

  • 只是一件小事——你错过了最后的空分配。
  • @Enigmativity:是的,你是对的。但是,JIT 编译器很可能也会将其优化掉。 :)
  • 我一直认为With声明就是这样做的;创建一个临时变量。
  • +1 我认为这是对我的帖子讨论的相同基本结果的更清晰的解释。
  • 没错,但您实际上指出了引用被放入堆栈的方式,我不知道。
【解决方案2】:

这是指导性部分,来自使用 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)。

【讨论】:

  • 这是我正在寻找的答案(但其他人的要点)。我一直在我正在处理的项目中使用缓存(这引发了这个特定问题),在某些情况下,我同时缓存/使用 3-4 个属性/对象。有效地做你描述 With 块所做的事情,一次不止一次。
  • 另外,通过“昂贵的操作”,有什么例子?
  • 一个代价高昂的操作的经典例子,尤其是在 VB 6 中,是一个数据库操作。从数据库中检索值始终是代码中最耗时的步骤之一,With 语句为 VB 程序员提供了一种缓存其结果的方法,而无需了解它在幕后工作的来龙去脉。
  • @Enigmativity:ldloc.x 指令对堆栈上指定索引处的局部变量的访问进行编码。我的意思是,当不使用 With 关键字时,IL 正在从堆栈上的第一个局部变量(索引 0)访问类实例,而当使用 With 关键字时,创建的 SECOND 局部变量通过 With 命令(索引 1)正在用于访问类实例。第一个变量(索引 0)在这两种情况下仍然存在,因为它是在两个函数的顶部使用 Dim 关键字创建的,但在使用 With 时未使用。
  • 基本上,索引值0和1实际上表示栈上两个不同的变量。请参阅 MSDN:msdn.microsoft.com/en-us/library/…。它在执行或堆栈上正在修改的数据方面没有区别,因为它们都指向完全相同的类,但它们是两个不同的引用,就像你写了“Dim a as Integer = 1 , 将 b 调暗为 Integer = a"。
【解决方案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

在幕后生成的内容方面是相同的。这不是微软第一次让我感到惊讶 :-)

我使用类似的东西来使我的源代码更具可读性,这是足够的理由。当我必须在六个月后回来修复一个微妙的错误时,我不想要一大堆代码。我希望它干净易读。


现在,您的 MSIL 代码可能没有针对同一事物进行优化,仅仅是因为它还没有被认为是必要的。您提到了 JIT 编译器,因此将任何优化推迟到那时可能是有意义的。

一旦决定 JIT 这段代码(例如因为它的大量使用),那将是我开始应用优化的地方。这样,您的 编译器 可以更简单,因为它不必担心可能不需要的大量优化:YAGNI。

请注意,这只是我的假设,我不代表微软。

【讨论】:

  • 是的,我熟悉 With blocks 的美学目的。我主要对效率方面感兴趣。根据文档, with 应该持有对父对象的引用,这样您就不必不断地重新限定名称。然而,在 VB6/VBA 中,您必须权衡多个资格的权衡与加载一个参考。因此,除非您有类似三个或更多的资格,否则只有使用 With 块才有意义。然而,使用 .NET,编译器看起来非常激进,并且可能消除了对 With 块的需要。
  • 以上都说了,我也有部分兴趣更好地理解为什么它会生成它为两个不同类所做的 MSIL 代码。
  • 是否可以使用 JIT 编译器来测试优化?还是 MSIL 是 .NET 中最低的?
  • 不知道,@Kumba,你现在已经远远超出了我的舒适区。虽然我很高兴吃 .NET,但我对它是由什么制成的:-)
  • 好吧,那么它实际上就不会成为一个公共子表达式。除非它知道它是安全的,否则我不会期望优化器会做那种事情,它无法判断的默认操作是让它未优化。这与 gcc 等其他优化器没有什么不同。
【解决方案4】:

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

【讨论】:

  • “幕后”版本中的c1 = New Class1怎么了?还是我遗漏了一些明显的东西?
  • 这是一个很好的观点。当我在 VB6 中工作时,我被 With 块所做的引用副本所烧毁。到目前为止,我已经设法避免在 VB.NET 中出现类似的错误。
  • 实际上,最后的 Nothing 分配没有任何用处(JIT 会向 GC 指示该变量在方法的剩余主体中未使用,因此它不是一个活根)
  • @paxdiablo - 抱歉,我正在反编译原始代码,而不是我的“修改”版本。
  • @Damien_The_Unbeliever:如果 With 语句在每次迭代的长时间循环中执行,则将 temp 设置为 Nothing 将允许它在循环内被垃圾收集,即使编译器不能' t 告诉它的价值不会被重用。如果在 lambda 表达式中使用 With 语句,则 set-to-null 也可能是相关的(尽管可能不是很好)。
【解决方案5】:

这取决于您如何使用它。如果你使用:

With someobject.SomeHeavyProperty
   .xxx 
End With

With 语句将节省一些对属性 getter 的调用。否则,效果应该被 JIT 取消。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-29
    • 1970-01-01
    • 1970-01-01
    • 2010-10-07
    • 1970-01-01
    • 1970-01-01
    • 2015-10-23
    相关资源
    最近更新 更多