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

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

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

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

Другие вопросы по тегам