Создание команд CMOV с использованием компиляторов Microsoft

В попытке выставить некоторые инструкции cmov на ядре intel 2, работающем под управлением Windows 7, я написал код ниже. Все, что он делает, это взять строку из консоли в качестве ввода, применить некоторые операции сдвига для генерации случайного семени, а затем передать это семя на 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 создали следующую сборку для этого последнего цикла:

; 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 [email protected]@@[email protected][email protected]@[email protected]@@[email protected] ; std::cout
  00160 e8 00 00 00 00   call    [email protected]@[email protected]@@[email protected]@[email protected][email protected]@[email protected]@@[email protected]@[email protected] ; std::operator<<<std::char_traits<char> >
  00165 83 c4 08     add     esp, 8
  00168 50       push    eax
  00169 e8 00 00 00 00   call    [email protected]@[email protected]@@[email protected]@[email protected][email protected]@[email protected]@@[email protected]@[email protected] ; 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 [email protected]

Для вашей справки, вот мои строки cpu id и версия компилятора.

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

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