AndAlso OrElse может быть аномально медленным

Я пишу программу с интенсивным вычислением с VB.NET 2010, и я хочу оптимизировать скорость. Я обнаружил, что операторы AndAlso и OrElse аномально медленны, если результат операции присваивается переменной уровня класса. Например, хотя утверждения

a = _b AndAlso _c  
_a = a  

взять около 6 машинных циклов между ними в скомпилированном exe, единственном утверждении

_a = _b AndAlso _c  

занимает около 80 машинных циклов. Здесь _a, _b и _c являются частными булевыми переменными Form1, а рассматриваемые операторы находятся в процедуре экземпляра 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; он включает в себя одну бесполезную инструкцию в обеих циклах, чтобы тратить несколько циклов и предотвращать срыв процессора. Грубый, но достаточно хороший для моих целей.

Ответы

Ответ 1

Здесь есть два фактора, которые делают этот код медленным. Вы не можете видеть это от IL, только код машины может дать вам понимание.


Сначала это общий, связанный с оператором AndAlso. Это оператор короткого замыкания, правый операнд не получает оценку, если левый операнд оценивается как False. Для этого требуется ветвь в машинный код. Ветвление является одним из самых медленных, что может сделать процессор, он должен догадаться о ветки спереди, чтобы избежать риска сброса трубопровода. Если он догадывается, что это неправильно, то это займет серьезный удар. Очень хорошо описано в этом сообщении. Типичная первичная потеря, если переменная a является очень случайной, а ветвь, поэтому плохо предсказанная, составляет около 500%.

Вы избегаете этого риска, используя вместо этого оператор И, он не требует ветвления в машинный код. Это всего лишь одна инструкция, И реализована процессором. Нет никакого смысла в пользу AndAlso в таком выражении, ничто не пойдет не так, если будет проверен правый операнд правой стороны. Здесь не применимо, но даже если IL показывает ветку, то дрожание может все еще сделать машинный код без ветвления с инструкцией CMOV (условное перемещение).


Но наиболее важным в вашем случае является то, что класс Form наследуется от класса MarshalByRefObject. Цепочка наследования - MarshalByRefObject > Компонент > Контроль > ПрокруткаСвязь > КонтейнерControl > Форма.

MBRO обрабатывается специально компилятором Just-in-Time, код может работать с прокси для объекта класса с реальным объектом, живущим в другом AppDomain или другом компьютере. Прокси прозрачен для джиттера для почти любого члена класса, они реализованы как простые вызовы методов. Кроме полей, они не могут быть проксимированы, потому что доступ к полю выполняется с помощью чтения/записи памяти, а не вызова метода. Если джиттер не может доказать, что объект является локальным, он вынужден вызывать в CLR, используя вспомогательные методы с именем JIT_GetFieldXxx() и JIT_SetFieldXxx(). CLR знает, является ли ссылка на объект прокси или реальной сделкой и имеет дело с разницей. Накладные расходы довольно значительны, из-за правильного звучания 80 циклов.

Вы не можете этого сделать, если переменные являются членами вашего класса Form. Перемещение их в класс-помощник является обходным путем.