AndAlso和OrElse可能会异常缓慢。

15

我正在使用VB.NET 2010编写一个计算密集型程序,希望优化速度。我发现,如果将操作的结果分配给类级变量,运算符AndAlsoOrElse的速度异常缓慢。例如,当执行以下语句时:

a = _b AndAlso _c  
_a = a  

在编译后的可执行文件中,这两个单语句之间需要大约6个机器周期。

_a = _b AndAlso _c  

执行这个语句需要大约80个机器周期。在这里,_a_b_cForm1的私有布尔变量,而相关语句位于Form1的实例过程中,其中a是一个局部布尔变量。

我找不到这个单独语句为什么要花费如此长的时间。我使用NetReflector进行了探索,一直到CIL代码层面, 代码看起来很好:

Instruction               Explanation                              Stack  
00: ldarg.0               Push Me (ref to current inst of Form1)   Me  
01: ldarg.0               Push Me                                  Me, Me  
02: ldfld bool Form1::_b  Pop Me, read _b and push it              _b, Me  
07: brfalse.s 11          Pop _b; if false, branch to 11           Me  
09: ldarg.0               (_b true) Push Me                        Me, Me  
0a: ldfld bool Form1::_c  (_b true) Pop Me, read _c and push it    _c, Me  
0f: brtrue.s 14           (_b true) Pop _c; if true, branch to 14  Me  
11: ldc.i4.0              (_b, _c not both true) Push result 0     result, Me  
12: br.s 15               Jump unconditionally to 15               result, Me  
-----  
14: ldc.i4.1              (_b, _c both true) Push result 1         result, Me  
15: stfld bool Form1::_a  Pop result and Me; write result to _a    (empty)  
1a:

有人可以解释一下为什么语句_a = _b AndAlso _c需要80个机器周期,而不是预测的5个或者更少吗?

我正在使用Windows XP,.NET 4.0和Visual Studio Express 2010。我用自己编写的一个比较简陋的代码片段来测量时间,该片段基本上使用Stopwatch对象计时一个包含代码的For-Next循环,循环中包含1000次迭代,并将其与一个空的For-Next循环进行比较;在两个循环中都包含了一个无用的指令来浪费一些周期并防止处理器停顿。这种方法对我来说足够粗略但够用。


抱歉如果这个评论对某些人来说似乎有些不合适,但如果我正在寻找计算速度并测量周期时间,我可能不会使用VB.NET或.NET总体。 - TyCobb
也许如果您发布更多的代码,我们可以提供其他改善效率的建议。 - Jeremy
1
@the_lotus 那样做实际上会更慢吗?And会同时评估两侧。 - TyCobb
1
@TyCobb 如果两边都是布尔变量,那么执行二进制 AND 可能比评估是否需要考虑一侧或两侧更快。 - the_lotus
2
@Jeremy:我故意把代码削减到最少,以便说明我的观点。我可以轻松地解决问题,但我喜欢了解我使用的工具。 - Eric P Smith
显示剩余4条评论
1个回答

12
这段代码运行缓慢的原因有两个因素。这个情况在IL中无法看到,只有机器码能够给你洞见。
第一个因素与AndAlso操作符相关。这是一种短路操作符,如果左侧操作数为False,右侧操作数将不会被计算。这需要在机器码中使用分支。分支是处理器能够执行的最慢的操作之一,它必须提前“猜测”分支以避免可能需要清空流水线的风险。如果猜测错误,将导致性能大幅下降。这在本文中得到了很好的说明。如果变量a高度随机,则典型的性能损失是500%左右。
你可以通过使用And操作符来避免这种风险,因为它不需要在机器码中使用分支。它只是一个单一指令,由处理器实现的AND。在这种表达式中,没有理由偏爱AndAlso,如果右侧操作数得到计算也不会出错。虽然IL中显示了分支,但Jitter可能仍然使用CMOV指令(条件移动)使机器码无需分支。
但在你的情况中,最重要的是Form类继承自MarshalByRefObject类。继承链是MarshalByRefObject> Component> Control> ScrollableControl> ContainerControl> Form。

MBRO在Just-in-Time(即时)编译器中受到特殊处理,代码可能会使用代理与实际对象所在的另一个AppDomain或另一台计算机进行交互。代理对于类的几乎任何成员来说都是透明的,它们被实现为简单的方法调用。除了字段之外,因为访问字段是通过内存读/写而不是方法调用完成的,所以字段无法被代理。如果Jitter不能证明对象是本地的,那么就必须调用CLR,使用名为JIT_GetFieldXxx()和JIT_SetFieldXxx()的帮助方法。CLR知道对象引用是代理还是真正的对象,并处理它们之间的区别。开销相当大,80个周期左右。

只要变量是您Form类的成员,您就没有太多可以做的事情。将它们移动到辅助类中是解决方法。


很好!感谢您的智慧 :) - Jeremy
3
太棒了!非常感谢。当我将变量移动到一个辅助类时,该指令的平均执行时间从80个周期缩短到3.5个周期。 - Eric P Smith
2
不错的结果。问答不能再好了。请更新您的问题并花费一些篇幅介绍您如何进行测量,因为不够多的程序员这样做。 - Hans Passant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接