256-битная векторизация через OpenMP SIMD предотвращает оптимизацию компилятора (скажем, функцию inlining)?
рассмотрим следующий пример игрушки, где A
- это n x 2
матрица хранится в порядке столбцов, и я хочу вычислить ее сумму столбцов. sum_0
вычисляет только сумму 1-го столбца, в то время как sum_1
также 2-й колонке. Это действительно искусственный пример, так как нет необходимости определять две функции для этой задачи (я могу написать одну функцию с двойным гнездом цикла, где внешний цикл повторяется из 0
to j
). Он построен, чтобы продемонстрировать шаблонная проблема у меня есть в реальности.
/* "test.c" */
#include <stdlib.h>
// j can be 0 or 1
static inline void sum_template (size_t j, size_t n, double *A, double *c) {
if (n == 0) return;
size_t i;
double *a = A, *b = A + n;
double c0 = 0.0, c1 = 0.0;
#pragma omp simd reduction (+: c0, c1) aligned (a, b: 32)
for (i = 0; i < n; i++) {
c0 += a[i];
if (j > 0) c1 += b[i];
}
c[0] = c0;
if (j > 0) c[1] = c1;
}
#define macro_define_sum(FUN, j)
void FUN (size_t n, double *A, double *c) {
sum_template(j, n, A, c);
}
macro_define_sum(sum_0, 0)
macro_define_sum(sum_1, 1)
если я скомпилирую его с
gcc -O2 -mavx test.c
GCC (скажем, последний 8.2), после встраивания, постоянного распространения и устранения мертвого кода, оптимизирует код с участием c1
функции sum_0
( проверьте его на Godbolt).
мне нравится этот трюк. Написав одну функцию шаблона и передав различные параметры конфигурации, оптимизирующий компилятор может генерировать разные версии. Это намного чище, чем копирование и вставка большой части кода и ручное определение различных версий функций.
однако такое удобство теряется, если я активирую OpenMP 4.0+ с
gcc -O2 -mavx -fopenmp test.c
sum_template
больше не встроен и не применяется исключение мертвого кода (проверьте это на Godbolt). Но если я удалю флаг -mavx
для работы с 128-битным SIMD оптимизация компилятора работает так, как я ожидаю (проверьте его на Godbolt). Так это жучок? Я нахожусь на x86-64 (Sandybridge).
Примечание
использование автоматической векторизации GCC -ftree-vectorize -ffast-math
не было бы этой проблемы ( проверьте это на Godbolt). Но я хочу использовать OpenMP, потому что он позволяет переносить прагму выравнивания через разные компиляторы.
фон
я пишу модули для пакета R, который должен быть переносимым между платформами и компиляторами. Запись R расширение не требует сборки. Когда R построен на платформе, он знает, что компилятор по умолчанию находится на этой платформе, и настраивает набор флагов компиляции по умолчанию. R не имеет флага автоматической векторизации, но имеет флаг OpenMP. Это означает,что использование OpenMP SIMD является идеальным способом использования SIMD в пакете R. См.1 и 2 немного более подробно.
2 ответов
самый простой способ решить эту проблему-с __attribute__((always_inline))
или другие переопределения для компилятора.
#ifdef __GNUC__
#define ALWAYS_INLINE __attribute__((always_inline)) inline
#elif defined(_MSC_VER)
#define ALWAYS_INLINE __forceinline inline
#else
#define ALWAYS_INLINE inline // cross your fingers
#endif
ALWAYS_INLINE
static inline void sum_template (size_t j, size_t n, double *A, double *c) {
...
}
Godbolt доказательство того, что он работает.
кроме того, не забывайте использовать -mtune=haswell
не только -mavx
. Обычно это хорошая идея. (Тем не менее, перспективные выровненные данные остановят GCC по умолчанию -mavx256-split-unaligned-load
настройка от разделения 256-битных нагрузок на 128-бит vmovupd
+ vinsertf128
, поэтому код gen для этой функция в порядке с tune=haswell. Но, как правило, требуется это для GCC автоматически векторизовать любые другие функции.
вам действительно не нужно static
вместе с inline
; если компилятор решает не вставлять его, он может по крайней мере использовать одно и то же определение в единицах компиляции.
обычно ССЗ решает inline или не по назначению-размер эвристики. Но даже установка -finline-limit=90000
не получает gcc для встроенного с вашим #pragma omp
(как заставить gcc встроить a функции?). Я предполагал, что gcc не понимал, что постоянное распространение после вставки упростит условное, но 90000 "псевдо-инструкций" кажется достаточно большим. Могут быть и другие эвристики.
возможно, OpenMP устанавливает некоторые вещи для каждой функции по-разному, что может сломать оптимизатор, если он позволит им встроиться в другие функции. Используя __attribute__((target("avx")))
останавливает эту функцию от вставки в функции, скомпилированные без AVX (так что вы можете выполнить runtime отправка безопасно, без "заражения" других функций инструкциями AVX через if(avx)
условиях.)
одна вещь, которую OpenMP делает, что вы не получаете с регулярной автоматической векторизацией, заключается в том, что сокращения могут быть векторизованы без включения -ffast-math
.
к сожалению, OpenMP по-прежнему не утруждает себя развертыванием с несколькими аккумуляторами или чем-либо, чтобы скрыть задержку FP. #pragma omp
это довольно хороший намек на то, что цикл на самом деле горячий и стоит тратить размер кода на, поэтому gcc действительно должен это сделать, даже без -fprofile-use
.
поэтому, особенно если это когда-либо работает на данных, которые горячие в кэше L2 или L1 (или, возможно, L3), вы должны сделать что-то, чтобы получить лучшую пропускную способность.
и кстати, выравнивание обычно не является огромным делом для AVX на Haswell. Но 64-байтовое выравнивание имеет гораздо большее значение на практике для AVX512 на SKX. Например, замедление 20% для несоосных данных вместо пары %.
(но перспективное выравнивание во время компиляции это отдельная проблема от фактического выравнивания данных во время выполнения. Оба полезны, но перспективное выравнивание во время компиляции делает более жесткий код с gcc7 и ранее или на любом компиляторе без AVX.)
мне отчаянно нужно было решить эту проблему, потому что в моем реальном проекте C, если ни один трюк с шаблоном не использовался для автоматической генерации различных версий функций (просто называемых "версиями"), мне нужно было бы написать в общей сложности 1400 строк кода для 9 разных версий, а не только 200 строк для одного шаблона.
я смог найти выход, и теперь публикую решение, используя пример игрушки в вопросе.
я планировал используйте inline функции sum_template
для версии. В случае успеха, это происходит во время компиляции, когда компилятор выполняет оптимизацию. Однако ПРАГМА OpenMP оказывается неудачной во время компиляции. Затем можно выполнить управление версиями на этапе предварительной обработки с помощью макрос только.
, чтобы избавиться от inline функции sum_template
, Я вручную вставляю его в макрос macro_define_sum
:
#include <stdlib.h>
// j can be 0 or 1
#define macro_define_sum(FUN, j) \
void FUN (size_t n, double *A, double *c) { \
if (n == 0) return; \
size_t i; \
double *a = A, * b = A + n; \
double c0 = 0.0, c1 = 0.0; \
#pragma omp simd reduction (+: c0, c1) aligned (a, b: 32) \
for (i = 0; i < n; i++) { \
c0 += a[i]; \
if (j > 0) c1 += b[i]; \
} \
c[0] = c0; \
if (j > 0) c[1] = c1; \
}
macro_define_sum(sum_0, 0)
macro_define_sum(sum_1, 1)
в этом макрос-только версии j
непосредственно заменяется 0 или 1 at во время расширения макроса. Тогда как в inline функция + макрос подходите в вопросе, у меня только sum_template(0, n, a, b, c)
или sum_template(1, n, a, b, c)
на стадии предварительной обработки, и j
в теле sum_template
распространяется только в более позднее время компиляции.
к сожалению, выше макрос дает ошибку. Я не могу определить или протестировать макрос внутри другого (см. 1, 2, 3). ПРАГМА OpenMP, начинающаяся с #
вызывает проблему здесь. Поэтому я должен разделить этот шаблон на две части: Часть до прагмы и часть после.
#include <stdlib.h>
#define macro_before_pragma \
if (n == 0) return; \
size_t i; \
double *a = A, * b = A + n; \
double c0 = 0.0, c1 = 0.0;
#define macro_after_pragma(j) \
for (i = 0; i < n; i++) { \
c0 += a[i]; \
if (j > 0) c1 += b[i]; \
} \
c[0] = c0; \
if (j > 0) c[1] = c1;
void sum_0 (size_t n, double *A, double *c) {
macro_before_pragma
#pragma omp simd reduction (+: c0) aligned (a: 32)
macro_after_pragma(0)
}
void sum_1 (size_t n, double *A, double *c) {
macro_before_pragma
#pragma omp simd reduction (+: c0, c1) aligned (a, b: 32)
macro_after_pragma(1)
}
мне давно не нужно macro_define_sum
. Я могу определить sum_0
и sum_1
сразу с помощью двух макросов. Я также могу настроить прагму соответствующим образом. Здесь вместо функции шаблона у меня есть шаблоны для блоков кода функции и может использовать их с легкостью.
выходные данные компилятора в этом случае ( проверьте его на Godbolt).
обновление
Спасибо за различные отзывы; все они очень конструктивны (вот почему я люблю переполнение стека).
спасибо Марк Glisse В для меня использование прагмы openmp внутри #define. Да, это было плохо, что я не искал эту проблему. #pragma
- это директива, а не реальный макрос, поэтому должен быть какой-то способ поместить его в макрос. Вот аккуратная версия с использованием _Pragma
оператор:
/* "neat.c" */
#include <stdlib.h>
// stringizing: https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
#define str(s) #s
// j can be 0 or 1
#define macro_define_sum(j, alignment) \
void sum_ ## j (size_t n, double *A, double *c) { \
if (n == 0) return; \
size_t i; \
double *a = A, * b = A + n; \
double c0 = 0.0, c1 = 0.0; \
_Pragma(str(omp simd reduction (+: c0, c1) aligned (a, b: alignment))) \
for (i = 0; i < n; i++) { \
c0 += a[i]; \
if (j > 0) c1 += b[i]; \
} \
c[0] = c0; \
if (j > 0) c[1] = c1; \
}
macro_define_sum(0, 32)
macro_define_sum(1, 32)
другие изменения включают в себя:
- я знак конкатенации создать имя функции;
-
alignment
является аргументом макроса. Для AVX значение 32 означает хорошее выравнивание, а значение 8 (sizeof(double)
) по существу не подразумевает выравнивания. преобразования в строку требуется разберите эти маркеры на строки, которые_Pragma
требует.
использовать gcc -E neat.c
проверить результат предварительной обработки. Компиляция дает желаемый результат сборки ( проверьте это на Godbolt).
несколько комментариев по Питеру Кордесу информативный ответ
использование атрибутов функций complier. я не профессиональный программист C. Мои опыты с C происходят только от написания расширений R. Среда разработки определяет, что я не очень хорошо знаком с атрибутами компилятора. Я знаю некоторые,но не использую их.
-mavx256-split-unaligned-load
не является проблемой в моем приложении, потому что я выделю выровненную память и применю заполнение для обеспечения выравнивания. Мне просто нужно пообещать компилятору выравнивания, чтобы он мог генерировать выровненные инструкции загрузки / хранения. Мне нужно сделать некоторую векторизацию на несогласованных данных, но это способствует очень ограниченной части всего вычисления. Даже если я получу штраф за производительность при разделенной несогласованной нагрузке на самом деле не будет замечен. Я также не компилятор каждого файла C с автоматической векторизацией. Я только делаю SIMD, когда операция горячая на кэше L1 (т. е. она привязана к процессору, а не к памяти). Кстати, -mavx256-split-unaligned-load
на GCC; что это для других компиляторов?
я осознаю разницу между static inline
и inline
. Если inline
функция доступна только для одного файла, я объявлю его как static
так что компилятор не создает его копию.
OpenMP SIMD может сделать уменьшение эффективно даже без GCC ' s -ffast-math
. Однако он не использует горизонтальное добавление для агрегирования результатов внутри регистра аккумулятора в конце сокращения; он запускает скалярный цикл для добавления каждого двойного слова (см. блок кода .L5 и .L27 in выход Godbolt).
пропускная способность-хорошая точка (особенно для арифметики с плавающей запятой, которая имеет относительно большой задержка, но высокая пропускная способность). Мой реальный код C, где применяется SIMD, - это тройное гнездо цикла. Я разворачиваю внешние два цикла, чтобы увеличить блок кода в самом внутреннем цикле для повышения пропускной способности. Тогда достаточно векторизации самого внутреннего. С примером игрушки в этом Q & A, где я просто суммирую массив, я могу использовать -funroll-loops
спросить GCC для разворачивать петли, используя несколько аккумуляторов для увеличения объема.
на этом Q & A
я думаю, люди будут относиться к этому Q & A более технически, чем я. Они могут быть заинтересованы в использовании атрибутов компилятора или настройке флагов / параметров компилятора для принудительной вставки функций. Поэтому ответ Питера, а также комментарий Марка под ответом по-прежнему очень ценны. Спасибо снова.