AndAlso OrElse 可能异常缓慢
AndAlso OrElse can be anomalously slow
我正在用 VB.NET 2010 编写一个计算密集型程序,我希望优化速度。我发现如果将运算结果分配给 class 级变量,运算符 AndAlso
和 OrElse
会异常缓慢。例如,虽然语句
a = _b AndAlso _c
_a = a
在编译后的exe中,它们之间需要大约6个机器周期,单个语句
_a = _b AndAlso _c
大约需要 80 个机器周期。这里的_a
、_b
、_c
是Form1
的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 是解决方法。
我正在用 VB.NET 2010 编写一个计算密集型程序,我希望优化速度。我发现如果将运算结果分配给 class 级变量,运算符 AndAlso
和 OrElse
会异常缓慢。例如,虽然语句
a = _b AndAlso _c
_a = a
在编译后的exe中,它们之间需要大约6个机器周期,单个语句
_a = _b AndAlso _c
大约需要 80 个机器周期。这里的_a
、_b
、_c
是Form1
的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 是解决方法。