Оптимизировали, оптимизировали, да не выоптимизировали!

Оптимизация? Конечно, каждый сталкивался с данной задачей при разработке своих, сколь-нибудь значительных, требующих определённых вычислений, приложений. При этом способов оптимизировать код существует огромное множество, и, как следствие, различных путей сделать это в автоматическом режиме с помощью опций компилятора. Вот здесь и возникает проблема – как выбрать то, что нужно нам и не запутаться?

Начнём с того, что оптимизации в современном компиляторе компании Intel® делятся на feature-specific (функциональные) и processor-specific (процессорные). К первым относят, например, оптимизации, подключаемые с помощью наиболее часто используемых ключиков O1, O2, O3. 

Другая история – оптимизация с помощью опций компилятора под конкретное «железо» и набор инструкций, который оно поддерживает. Хотелось бы раскрыть именно этот вопрос более детально.

Итак, ключей для генерации «специфичного» под какое-либо семейство процессоров несколько. Что же «специфичного» мы получаем? В первую очередь, использование SIMD инструкций поддерживаемой архитектуры. Рассмотрим возможные ключи:

1) -x<feature> (Linux), /Qx<feature> (Windows)

Если мы хотим, чтобы компилятор оптимизировал наш код под конкретный, заранее известный тип интеловского процессора, и, соответственно, поддерживаемый им набор инструкций и функциональностей, то можно использовать данный ключ. При этом мы не собираемся запускать приложение на системах, которые не поддерживают данный набор инструкций.

Скажем, есть у нас система с процессором архитектуры SandyBridge, на которой мы собираемся запускать приложение. Данный процессор поддерживает набор инструкций AVX, и именно это значение мы можем указать с помощью ключика –xAVX (Linux) или –QxAVX (Windows). Таким образом, мы получим оптимизированный бинарник, который при запуске будет проверять, поддерживает ли наш процессор AVX инструкции. Если да, то наше приложение успешно запуститься и раскроет все преимущества использования процессорной оптимизации. Если же нет, то вылетит ошибка о том, что наш процессор не поддерживает данную функциональность. Приложение выполняться не будет!

Список значений, которые можно прописать с помощью данного ключика следующий:

SSE2, SSE3, SSSE3, SSSE3_ATOM, SSE3_ATOM, SSE4.1, SSE4.2, AVX, CORE-AVX-I, CORE-AVX2

Важно, что наиболее «свежие» версии поддерживают предыдущие, поэтому, возвращаясь к нашему примеру, очевидно, что код, скомпилированный с опцией –xSSE4.1, будет успешно выполнен и на системах, поддерживающих SSE4.2, AVX и выше. Но не наоборот. Специфичные AVX инструкции не смогут выполняться на более старых системах их  не поддерживающих, что логично.

2) -m<feature> (Linux), /arch: <feature> (Windows)

Данный ключ позволит получить приложение, которое будет содержать оптимизацию для  всех типов процессоров (не только интеловских), поддерживающих заданный набор инструкций. При этом никакой проверки в main функцию на тип процессора добавляться не будет, а это значит, что если мы станем запускать приложение на системе с процессором, не поддерживающим заданный набор инструкций, произойдёт неприятность… в отличие от проверки в случае с флагом –x, где просто выдаётся сообщение о несовместимости, в данном случае программа «упадёт», пытаясь выполнить конкретную инструкцию.

3) -ax<feature> (Linux), /Qax<feature> (Windows)

Ещё одна опция компилятора, позволяющая создать несколько версий (code paths), оптимизированных под разные архитектуры. Мы можем указать сразу несколько архитектур через запятую, например –axSSE4.2,AVX. Понятно, что в данном случае увеличится размер выходного файла (он так и будет один). При этом создаются как минимум два варианта – базовый (baseline) и оптимизированный под заданную архитектуру код. Есть одно но… компилятор сам проверяет, будет ли выигрыш в производительности от создания дополнительной, оптимизированной ветки. Если выигрыш есть, сгенерирует несколько версий, и во время выполнения программы, в зависимости от используемого процессора, будет выбрана нужная. С использованием этого ключика мы получим возможность создавать приложения, извлекающие максимум возможностей на наиболее свежих процессорах, при этом так же вполне валидно работающих на более старых и не интеловских процессорах (будет выполняться базовая ветка).

Интересны дополнительные комбинации перечисленных ключиков. Скажем, использование опций –ax и –x одновременно приводит к тому, что последней будет задана базовая версия, которая по умолчанию выставляется как SSE2. Поэтому, если есть необходимость её изменить с SSE2 на что-то другое, используем сочетание этих опций. Равно как и сочетание –ax и –m приведёт к тому, что в базовой версии будет оптимизированный код, который выполняется не только на интеловских процессорах.

Есть ещё один интересный флажок -mia32. Только он сможет создать приложение, способное выполняться на процессорах, более древних, чем Pentium 4 (не поддерживающих SSE2). Скажем, комбинация опций -mia32 -axSSE2 (Linux) или /arch:IA32 /QaxSSE2 (Windows) создаст приложение, которое будет выполняться на любых процессорах с архитектурой IA-32, плюс ещё и оптимизированная под SSE2 ветка.

И напоследок… если мы не знаем, какой набор инструкций поддерживает наша текущая система, но хотим оптимизировать именно под неё, нам поможет флаг –xHost(Linux) и /QxHost (Windows). Она скажет компилятору генерировать код, оптимизированный под поддерживаемую нашим процессором архитектуру. На другой машине наше приложение может и не заработает, но есть вероятность, что этого нам и не нужно.

А теперь немного практики. Будем компилировать программку перемножения матриц с разными ключиками на Windows. Тесты я провожу на системе с процессором архитектуры SandyBridge, поэтому собрав программку с флагом -QxCORE-AVX2, при запуске получаю следующую ошибку:

Fatal Error: This program was not built to run in your system.

Please verify that both the operating system and the processor support Intel(R) AVX2, BMI, LZCNT, HLE, RTM and FMA instructions.

Меняю ключик на –QaxAVX,CORE-AVX2, и программа запускается успешно, при этом выполняются специфичныe AVX инструкции. Можно было бы написать и просто –QaxCORE-AVX2 – как мы помним, в дефолтной baseline версии будет код, оптимизированный с использованием SSE2 инструкций.

Кстати, интересно глянуть и на ASM листинг. Добавив опцию –S (к опциям –QaxAVX,CORE-AVX2), получаем соответствующий файл с расширением *.asm на выходе. В нём видно, что сгенерировалось три версии инструкций, причём эти версии находятся в разных местах листинга:

lea       r8, QWORD PTR [rax+rcx*8]                     ;76.2
vcvtdq2pd ymm15, xmm13                                  ;76.2
add       rcx, 16                                       ;76.2
vextracti128 xmm14, ymm13, 1                            ;76.2
vcvtdq2pd ymm14, xmm14                                  ;76.2
vpaddd    ymm13, ymm13, ymm4                            ;76.2
vfmadd213pd ymm15, ymm5, ymm0                           ;76.2
...
lea       r8, QWORD PTR [rax+rcx*8]                     ;76.2
vpaddd    xmm5, xmm5, xmm1                              ;76.2
vmulpd    ymm15, ymm3, ymm15                            ;76.2
add       rcx, 16                                       ;76.2
vaddpd    ymm15, ymm2, ymm15                            ;76.2
vmovupd   YMMWORD PTR [imagerel(a)+rbp+r8], ymm15       ;76.2
vcvtdq2pd ymm15, xmm5                                   ;76.2
...
lea       r8, QWORD PTR [rax+rcx*8]                     ;76.2
movaps    XMMWORD PTR [imagerel(a)+rbp+r8], xmm15       ;76.2
add       rcx, 8                                        ;76.2
cvtdq2pd  xmm15, xmm14                                  ;76.2
mulpd     xmm15, xmm1                                   ;76.2
addpd     xmm15, xmm5                                   ;76.2
paddd     xmm14, xmm3                                   ;76.2

Вот как-то так работают processor-specific оптимизации в интеловском компиляторе. Надеюсь было интересно и познавательно.  До новых встреч!

Per informazioni complete sulle ottimizzazioni del compilatore, consultare l'Avviso sull'ottimizzazione