Генерация инструкций 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

4

Решение

это очень трудно, если не просто невозможно заставить 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 инструкции. Вот два важных замечания:

  1. MSVC 10 (Visual Studio 2010) и ранее виртуально никогда генерировать CMOV инструкции. Я никогда видел их в выводе, независимо от того, сколько вариантов исходного кода я пробовал. Я говорю «виртуально», потому что может быть какой-то безумный крайний случай, который я пропустил, но я очень сильно сомневаюсь в этом. Ни один из флагов оптимизации не влияет на это.

    Тем не менее, MSVC 11 (Visual Studio 2012) внес существенные улучшения в генератор кода, по крайней мере, в этом аспекте. Эта и более поздние версии компилятора теперь кажутся как минимум знать о существовании CMOVcc инструкции и может излучать их при правильных условиях (то есть, /arch:SSE или позже, и использование условного оператора, как описано ниже).

  2. Я нашел это самый эффективный способ уговорить компилятор испустить CMOV Инструкция заключается в использовании условный оператор вместо длинной формы ifelse заявление. Хотя эти две конструкции должны быть полностью эквивалентны в том, что касается генератора кода, это не так.

    Другими словами, пока вы может быть см. следующее переведено на без филиалов 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 напишите код так, чтобы это было наиболее целесообразно. Оптимизатор действительно знает, как делать свою работу!

3

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]