Генерация инструкций CMOV с использованием компиляторов Microsoft
Чтобы получить некоторые инструкции 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
1 ответ
Получить 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 напишите код так, чтобы это было наиболее целесообразно. Оптимизатор действительно знает, как делать свою работу!