В попытке получить некоторые инструкции cmov на Intel Core 2 под управлением Windows 7 Pro, я написал код ниже. Все, что он делает, это берет строку из консоли в качестве входных данных, применяет некоторые операции сдвига для генерации случайного начального числа, а затем передает это начальное значение в srand для генерации небольшого массива псевдослучайных чисел. Затем псевдослучайные числа оцениваются на предмет того, удовлетворяют ли они предикатной функции (более произвольное перетасовка битов), и выдают ‘*’ или ‘_’. Целью эксперимента является генерация команд cmov, но, как вы можете видеть в разборке ниже, их нет.
Любые советы о том, как изменить код или флаги, чтобы они были сгенерированы?
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdlib>
bool blackBoxPredicate( const unsigned int& ubref ) {
return ((ubref << 6) ^ (ubref >> 2) ^ (~ubref << 2)) % 15 == 0;
}
int main() {
const unsigned int NUM_RINTS = 32;
unsigned int randomSeed = 1;
unsigned int popCount = 0;
unsigned int * rintArray = new unsigned int[NUM_RINTS];
std::string userString;
std::cout << "input a string to use as a random seed: ";
std::cin >> userString;
std::for_each(
userString.begin(),
userString.end(),
[&randomSeed] (char c) {
randomSeed = (randomSeed * c) ^ (randomSeed << (c % 7));
});
std::cout << "seed computed: " << randomSeed << std::endl;
srand(randomSeed);
for( int i = 0; i < NUM_RINTS; ++i ) {
rintArray[i] = static_cast<unsigned int> (rand());
bool pr = blackBoxPredicate(rintArray[i]);
popCount = (pr) ? (popCount+1) : (popCount);
std::cout << ((pr) ? ('*') : ('_')) << " ";
}
std::cout << std::endl;
delete rintArray;
return 0;
}
И использовал этот make-файл для сборки:
OUT=cmov_test.exe
ASM_OUT=cmov_test.asm
OBJ_OUT=cmov_test.obj
SRC=cmov_test.cpp
THIS=makefile
CXXFLAGS=/nologo /EHsc /arch:SSE2 /Ox /W3
$(OUT): $(SRC) $(THIS)
cl $(SRC) $(CXXFLAGS) /FAscu /Fo$(OBJ_OUT) /Fa$(ASM_OUT) /Fe$(OUT)
clean:
erase $(OUT) $(ASM_OUT) $(OBJ_OUT)
И все же, когда я посмотрел, сгенерировано ли что-нибудь, я увидел, что компиляторы Microsoft сгенерировали следующую сборку для этого последнего цикла for:
; 34 : popCount = (pr) ? (popCount+1) : (popCount);
; 35 :
; 36 : std::cout << ((pr) ? ('*') : ('_')) << " ";
00145 68 00 00 00 00 push OFFSET $SG30347
0014a 85 d2 test edx, edx
0014c 0f 94 c0 sete al
0014f f6 d8 neg al
00151 1a c0 sbb al, al
00153 24 cb and al, -53 ; ffffffcbH
00155 04 5f add al, 95 ; 0000005fH
00157 0f b6 d0 movzx edx, al
0015a 52 push edx
0015b 68 00 00 00 00 push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
00160 e8 00 00 00 00 call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@D@Z ; std::operator<<<std::char_traits<char> >
00165 83 c4 08 add esp, 8
00168 50 push eax
00169 e8 00 00 00 00 call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
0016e 46 inc esi
0016f 83 c4 08 add esp, 8
00172 83 fe 20 cmp esi, 32 ; 00000020H
00175 72 a9 jb SHORT $LL3@main
Для справки, вот мои строки идентификатора процессора и версия компилятора.
PROCESSOR_ARCHITECTURE=x86
PROCESSOR_IDENTIFIER=x86 Family 6 Model 58 Stepping 9, GenuineIntel
PROCESSOR_LEVEL=6
PROCESSOR_REVISION=3a09
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
это очень трудно, если не просто невозможно заставить 32-битный компилятор C / C ++ от Microsoft испускать CMOVcc
инструкции.
Что вы должны помнить, так это то, что условные шаги были впервые представлены с процессором Pentium Pro, и хотя у Microsoft был переключатель компилятора, который мелодия сгенерированный код для этого процессора 6-го поколения (давно устарел /G6
), они никогда не выдавали код, который будет работать исключительно на этом процессоре. Код все еще должен был работать на процессорах 5-го поколения (то есть, Pentium и AMD K6), поэтому он не мог использовать CMOVcc
инструкции, потому что те генерировали бы незаконные исключения инструкции. В отличие от компилятора Intel, глобальная динамическая диспетчеризация не была (и все еще не реализована).
Также стоит отметить, что ни один переключатель не был введен для исключительно Процессоры 6-го поколения и позже. Нет никаких /arch:CMOV
или как они могли бы назвать это. Поддерживаемые значения для /arch
переключатель идти прямо из IA32
(самый низкий общий знаменатель, для которого CMOV
было бы потенциально незаконным) SSE
, Тем не мение, документация делает подтвердите, что, как и следовало ожидать, включение генерации кода SSE или SSE2 неявно позволяет использовать инструкции условного перемещения и все остальное, что было представлено до SSE:
Помимо использования инструкций SSE и SSE2, компилятор также использует другие инструкции, присутствующие в ревизиях процессора, которые поддерживают SSE и SSE2. Примером является инструкция CMOV, которая впервые появилась в ревизии Pentium Pro процессоров Intel.
Поэтому, чтобы иметь хоть какую-то надежду заставить компилятор выдать CMOV
инструкции, вы должны установить /arch:SSE
или выше. В настоящее время, конечно, это не имеет большого значения. Вы можете просто установить /arch:SSE
или же /arch:SSE2
и быть безопасным, так как все современные процессоры поддерживают эти наборы команд.
Но это только половина дела. Даже когда у вас включены правильные ключи компилятора, заставить MSVC излучать крайне сложно CMOV
инструкции. Вот два важных замечания:
MSVC 10 (Visual Studio 2010) и ранее виртуально никогда генерировать CMOV
инструкции. Я никогда видел их в выводе, независимо от того, сколько вариантов исходного кода я пробовал. Я говорю «виртуально», потому что может быть какой-то безумный крайний случай, который я пропустил, но я очень сильно сомневаюсь в этом. Ни один из флагов оптимизации не влияет на это.
Тем не менее, MSVC 11 (Visual Studio 2012) внес существенные улучшения в генератор кода, по крайней мере, в этом аспекте. Эта и более поздние версии компилятора теперь кажутся как минимум знать о существовании CMOVcc
инструкции и может излучать их при правильных условиях (то есть, /arch:SSE
или позже, и использование условного оператора, как описано ниже).
Я нашел это самый эффективный способ уговорить компилятор испустить CMOV
Инструкция заключается в использовании условный оператор вместо длинной формы if
—else
заявление. Хотя эти две конструкции должны быть полностью эквивалентны в том, что касается генератора кода, это не так.
Другими словами, пока вы может быть см. следующее переведено на без филиалов CMOVLE
инструкция:
int value = (a < b) ? a : b;
ты сможешь всегда получить код ветвления для следующей последовательности:
int value;
if (a < b) value = a;
else value = b;
По крайней мере, даже если использование вами условного оператора не вызывает CMOV
инструкции (например, в MSVC 10 или более ранней версии), вам все еще может повезти, чтобы получить код без ответвлений другими способами —например, SETcc
или умное использование SBB
а также NEG
/NOT
/INC
/DEC
, Это то, что использует разборка, которую вы показали в вопросе, и хотя она не так оптимальна, как CMOVcc
это конечно сравнимый и разница не стоит беспокоиться. (Единственная другая инструкция ветвления является частью цикла.)
Если вы действительно хотите безветвительный код (который Вы часто делаете это при оптимизации рук), и вам не повезло получить компилятор для генерации кода, который вам нужен, вам нужно научиться писать исходный код более умно. Мне повезло с написанием кода, который вычисляет результат без ветвлений с использованием побитовых или арифметических операторов.
Например, вы можете пожелать, чтобы следующая функция генерировала оптимальный код:
int Minimum(int a, int b)
{
return (a < b) ? a : b;
}
Вы следовали правилу № 2 и использовали условный оператор, но если вы используете более старую версию компилятора, вы все равно получите код ветвления. Перехитрите компилятор, используя классический трюк:
int Minimum_Optimized(int a, int b)
{
return (b + ((a - b) & -(a < b)));
}
Полученный объектный код не совсем оптимален (он содержит CMP
инструкция, которая является избыточной, так как SUB
уже устанавливает флаги), но он не имеет ответвлений и, следовательно, все равно будет значительно быстрее, чем первоначальная попытка случайных входов, которые приводят к сбою прогнозирования ветвлений.
В качестве другого примера представьте, что вы хотите определить, является ли 64-разрядное целое число отрицательным в 32-разрядном приложении. Вы пишете следующий самоочевидный код:
bool IsNegative(int64_t value)
{
return (value < 0);
}
и вы будете очень разочарованы результатами. GCC и Clang оптимизируют это разумно, но MSVC выплевывает неприятную условную ветвь. Уловка (непереносимая) заключается в том, что знаковый бит находится в старших 32 битах, поэтому вы можете явно изолировать и проверить его с помощью побитовой манипуляции:
bool IsNegative_Optimized(int64_t value)
{
return (static_cast<int32_t>((value & 0xFFFFFFFF00000000ULL) >> 32) < 0);
}
Кроме того, один из комментаторов предлагает использовать встроенную сборку. Хотя это возможно (32-разрядный компилятор Microsoft поддерживает встроенную сборку), это часто плохой выбор. Встроенная сборка разрушает оптимизатор довольно значительными способами, поэтому, если вы не пишете значительное Множество кода во встроенной сборке вряд ли приведет к значительному увеличению производительности. Кроме того, встроенный синтаксис Microsoft чрезвычайно ограничен. Он меняет гибкость на простоту во многом. В частности, нет возможности указать вход значения, так что вы застряли, загружая ввод из памяти в регистр, и вызывающая сторона вынуждена подготовить ввод из регистра в память. Это создает феномен, который я люблю называть «целой лотерейкой» или, для краткости, «медленный код». Вы не переходите на встроенную сборку в тех случаях, когда допустим медленный код. Таким образом, всегда предпочтительнее (по крайней мере, в MSVC) выяснить, как писать исходный код C / C ++, который убеждает компилятор испускать нужный объектный код. Даже если вы можете получить только близко до идеального результата, это все же значительно лучше, чем штраф, который вы платите за использование встроенной сборки.
Обратите внимание, что ни одно из этих искажений не требуется, если вы нацелены на x86-64. 64-битный компилятор C / C ++ от Microsoft значительно более агрессивен в использовании CMOVcc
инструкции, когда это возможно, даже старые версии. Как этот блог объясняет, Компилятор x64 в комплекте с Visual Studio 2010 содержит ряд улучшений качества кода, включая лучшую идентификацию и использование CMOV
инструкции.
Никаких специальных флагов компилятора или других соображений здесь не требуется, так как все процессоры, поддерживающие 64-битный режим, поддерживают условные перемещения. Я полагаю, именно поэтому они смогли сделать это правильно для 64-битного компилятора. Я также подозреваю, что некоторые из этих изменений, внесенных в компилятор x86-64 в VS 2010, были перенесены в компилятор x86-32 в VS 2012, объясняя, почему он хотя бы знает о существовании CMOV
, но он все еще не использует его так агрессивно, как 64-битный компилятор.
Суть в том, что при нацеливании на x86-64 напишите код так, чтобы это было наиболее целесообразно. Оптимизатор действительно знает, как делать свою работу!
Других решений пока нет …