I'm writing a computationally intensive program with VB.NET 2010 and I wish to optimise speed. I find that the operators AndAlso
and OrElse
are anomalously slow if the result of the operation is assigned to a class-level variable. For example, while the statements
a = _b AndAlso _c
_a = a
take about 6 machine cycles between them in the compiled exe, the single statement
_a = _b AndAlso _c
takes about 80 machine cycles. Here _a
, _b
and _c
are Private Boolean variables of Form1
, and the statements in question are in an instance procedure of Form1
, of which a
is a local Boolean variable.
I cannot find why the single statement takes so long. I have explored it using NetReflector down to the level of the CIL code, which looks good:
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:
Can anyone shed any light on why the statement _a = _b AndAlso _c
takes 80 machine cycles instead of the predicted 5 or so?
I'm using Windows XP with .NET 4.0 and Visual Studio Express 2010. I measured the times with a frankly dirty snippet of my own which basically uses a Stopwatch object to time a For-Next loop with 1000 iterations containing the code in question and compare it with an empty For-Next loop; it includes one useless instruction in both loops to waste a few cycles and prevent processor stalling. Crude but good enough for my purposes.
In a Boolean comparison, the Or operator always evaluates both expressions, which could include making procedure calls. The OrElse Operator performs short-circuiting, which means that if expression1 is True , then expression2 is not evaluated.
Data Types. The AndAlso operator is defined only for the Boolean Data Type. Visual Basic converts each operand as necessary to Boolean before evaluating the expression. If you assign the result to a numeric type, Visual Basic converts it from Boolean to that type such that False becomes 0 and True becomes -1 .
This is used to connect two (or more) logical conditions checking like if (condition1 AndAlso condition2). If the first condition is false, It won't perform the second condition . So our execution time is saving in a logical operation in which more conditions are combined using “AndAlso" operator.
OrElse(Expression, Expression) Creates a BinaryExpression that represents a conditional OR operation that evaluates the second operand only if the first operand evaluates to false .
There are two factors at play here that make this code slow. You cannot see this from the IL, only the machine code can give you insight.
First is the general one associated with the AndAlso operator. It is a short-circuiting operator, the right-hand side operand does not get evaluated if the left-hand side operand evaluates to False. This requires a branch in the machine code. Branching is one of the slowest thing a processor can do, it must guess at the branch up front to avoid the risk of having to flush the pipeline. If it guesses wrong then it will take a major perf hit. Very well covered in this post. The typical perf loss if the a
variable is highly random, and the branch therefore poorly predicted, is around 500%.
You avoid this risk by using the And operator instead, it doesn't require a branch in the machine code. It is just a single instruction, AND is implemented by the processor. There is no point in favoring AndAlso in an expression like that, nothing goes wrong if the right-hand side operand gets evaluated. Not applicable here, but even if the IL shows a branch then the jitter might still make the machine code branch-less with a CMOV instruction (conditional move).
But most significant in your case is that the Form class inherits from the MarshalByRefObject class. The inheritance chain is MarshalByRefObject > Component > Control > ScrollableControl > ContainerControl > Form.
MBRO is treated specially by the Just-in-Time compiler, the code might be working with a proxy for the class object with the real object living in another AppDomain or another machine. A proxy is transparent to the jitter for almost any kind of member of the class, they are implemented as simple method calls. Except fields, they cannot be proxied because access to a field is done with a memory read/write, not a method call. If the jitter cannot prove that the object is local then it is forced to call into the CLR, using helper methods named JIT_GetFieldXxx() and JIT_SetFieldXxx(). The CLR knows whether the object reference is a proxy or the real deal and deals with the difference. The overhead is quite substantial, 80 cycles sounds about right.
There is not much you can do about this as long as the variables are members of your Form class. Moving them into a helper class is the workaround.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With