AndAlso OrElse 可能异常缓慢

AndAlso OrElse can be anomalously slow

我正在用 VB.NET 2010 编写一个计算密集型程序,我希望优化速度。我发现如果将运算结果分配给 class 级变量,运算符 AndAlsoOrElse 会异常缓慢。例如,虽然语句

a = _b AndAlso _c  
_a = a  

在编译后的exe中,它们之间需要大约6个机器周期,单个语句

_a = _b AndAlso _c  

大约需要 80 个机器周期。这里的_a_b_cForm1的Private Boolean变量,所讨论的语句在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。我用我自己的一个坦率的肮脏片段测量了时间,它基本上使用秒表对象来计时 For -Next 循环 1000 次迭代包含有问题的代码,并将其与空的 For-Next 循环进行比较;它在两个循环中都包含一条无用指令,以浪费几个周期并防止处理器停顿。简陋但足以满足我的目的。

这里有两个因素导致这段代码变慢。您无法从 IL 中看到这一点,只有机器代码可以让您深入了解。


第一个是与 AndAlso 运算符关联的通用运算符。它是一个短路运算符,如果左侧操作数的计算结果为 False,则右侧操作数不会被计算。这需要机器代码中的一个分支。分支是处理器可以做的最慢的事情之一,它必须预先猜测分支以避免不得不刷新管道的风险。如果它猜错了,那么它将遭受重大的性能打击。 this post 中介绍得很好。如果 a 变量是高度随机的,因此分支预测不佳,则典型的性能损失约为 500%。

您可以通过使用 And 运算符来避免这种风险,它不需要机器代码中的分支。它只是一条指令,AND 由处理器实现。在这样的表达式中支持 AndAlso 是没有意义的,如果对右侧操作数进行求值,则不会出错。此处不适用,但即使 IL 显示分支,抖动仍可能使用 CMOV 指令(条件移动)使机器代码无分支。


但在您的案例中最重要的是表单 class 继承自 MarshalByRefObject class。继承链为 MarshalByRefObject > Component > Control > ScrollableControl > ContainerControl > Form.

MBRO 由即时编译器特殊处理,代码可能正在使用 class 对象的代理,而真实对象位于另一个 AppDomain 或另一台机器中。对于 class 的几乎任何类型的成员,代理对抖动都是透明的,它们被实现为简单的方法调用。除了字段,它们不能被代理,因为对字段的访问是通过内存 read/write 而不是方法调用完成的。如果抖动无法证明该对象是本地对象,则使用名为 JIT_GetFieldXxx() 和 JIT_SetFieldXxx() 的辅助方法强制调用 CLR。 CLR 知道对象引用是代理还是真正的交易并处理差异。开销相当大,80 个周期听起来很合适。

只要变量是表单的成员,您对此无能为力 class。将它们移动到助手 class 是解决方法。