Ответ 1
Крайне сложно, если не совсем невозможно, заставить Microsoft 32-разрядный компилятор C/С++ генерировать инструкции CMOVcc
.
Что нужно помнить, так это то, что условные ходы были впервые введены с процессором Pentium Pro, а в то время как у Microsoft был коммутатор компилятора, который настроил бы сгенерированный код для этого процессора 6-го поколения (long-derecated /G6
), они никогда не выпускали код, который работал бы исключительно на этом процессоре. Код по-прежнему необходим для работы на процессорах 5-го поколения (например, Pentium и AMD K6), поэтому он не мог использовать команды CMOVcc
, поскольку они могли бы генерировать незаконные исключения команд. В отличие от компилятора Intel, глобальная динамическая диспетчеризация не была (и до сих пор не реализована).
Кроме того, стоит отметить, что ни один коммутатор не был представлен для целевых исключительно процессоров 6-го поколения и позже. Там нет /arch:CMOV
или что бы они ни назвали. Поддерживаемые значения для /arch
switch идут прямо от IA32
(самый низкий общий знаменатель, для которого CMOV
будет потенциально незаконным) до SSE
. Тем не менее, документация подтверждает, что, как можно было бы ожидать, включение генерации кода SSE или SSE2 неявно позволяет использовать условно-перемещающие инструкции и что угодно еще что было введено перед SSE:
В дополнение к использованию инструкций SSE и SSE2, компилятор также использует другие инструкции, которые присутствуют в версиях процессоров, поддерживающих SSE и SSE2. Примером может служить инструкция CMOV, которая впервые появилась на версии процессоров Intel Pentium Pro.
Поэтому, чтобы иметь любую надежду на то, чтобы заставить компилятор испускать инструкции 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/С++, который убеждает компилятор испускать требуемый код объекта. Даже если вы можете приблизиться к идеальному выходу, который все же значительно лучше, чем штраф, который вы платите за встроенную сборку.
Обратите внимание, что ни один из этих искажений не требуется, если вы нацеливаете x86-64. Microsoft 64-разрядный компилятор C/С++ значительно более агрессивен в отношении использования инструкций CMOVcc
, когда это возможно,, даже более старых версий. Поскольку это сообщение в блоге объясняется, компилятор x64, связанный с Visual Studio 2010, содержит ряд улучшений качества кода, включая лучшую идентификацию и использование CMOV
.
Никаких специальных флагов компилятора или других соображений здесь не требуется, поскольку все процессоры, поддерживающие 64-битный режим, поддерживают условные перемещения. Полагаю, именно поэтому они смогли получить это право для 64-битного компилятора. Я также подозреваю, что некоторые из этих изменений, внесенных в компилятор x86-64 в VS 2010, были перенесены в компилятор x86-32 в VS 2012, объяснив, почему он, по крайней мере, знает о существовании CMOV
, но он все еще не " t использовать его так агрессивно, как 64-битный компилятор.
Суть в том, что при таргетинге на x86-64 писать код таким образом, который имеет наибольший смысл. Оптимизатор действительно знает, как выполнить свою работу!