Мне отчаянно нужно было решить эту проблему, потому что в моем реальном C-проекте, если бы не использовался шаблонный трюк для автоматического генерирования различных версий функций (в дальнейшем просто называемых «версиями»), мне нужно было бы написать в общей сложности 1400 строк. кода для 9 различных версий вместо 200 строк для одного шаблона.
Мне удалось найти выход, и сейчас выкладываю решение, используя пример с игрушкой в вопросе.
Я планировал использовать встроенную функцию sum_template
для управления версиями. В случае успеха это происходит во время компиляции, когда компилятор выполняет оптимизацию. Тем не менее, прагма OpenMP, как оказалось, не позволяет управлять версиями во время компиляции. Затем можно выполнить управление версиями на этапе предварительной обработки, используя только macros .
Чтобы избавиться от функции 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)
В этой версии macro , j
непосредственно заменяется на 0 или 1 at во время раскрытия макроса. Принимая во внимание, что при подходе inline + macro в вопросе у меня есть только 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 ).
Обновление
Спасибо за различные отзывы; все они очень конструктивны (вот почему я люблю переполнение стека).
Спасибо Марк Глиссе за указание на Использование прагмы 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)
) по существу не предполагает выравнивания. Stringizing требуется для разбора этих токенов в строки, которые необходимы _Pragma
.
Используйте gcc -E neat.c
для проверки результата предварительной обработки. Компиляция дает желаемый вывод сборки ( Проверьте это на Годболте ).
Несколько комментариев к информативному ответу Питера Кордеса
Использование атрибутов функции компилятора. Я не профессиональный программист на Си. Мой опыт работы с C основан на написании расширений R. Среда разработки определяет, что я не очень знаком с атрибутами компилятора. Я знаю некоторые, но на самом деле не использую их.
-mavx256-split-unaligned-load
не является проблемой в моем приложении, потому что я выделю выровненную память и добавлю заполнение для обеспечения выравнивания. Мне просто нужно пообещать компилятору выравнивания, чтобы он мог генерировать выровненные инструкции загрузки / сохранения. Мне нужно сделать некоторую векторизацию для невыровненных данных, но это вносит вклад в очень ограниченную часть всего вычисления. Даже если я получу снижение производительности при разделенной нагрузке, это не будет замечено в реальности. Я также не компилирую каждый файл C с автоматической векторизацией. Я делаю SIMD только тогда, когда операция выполняется в кеше L1 (т. Е. Она связана с процессором, а не с памятью). Кстати, -mavx256-split-unaligned-load
для GCC ; что это за другие компиляторы?
Мне известно о разнице между static inline
и inline
. Если функция inline
доступна только для одного файла, я объявлю ее как static
, чтобы компилятор не генерировал ее копию.
OpenMP SIMD может эффективно выполнять сокращение даже без GCC -ffast-math
. Однако он не использует горизонтальное сложение для агрегирования результатов в регистре аккумулятора в конце сокращения; он запускает скалярный цикл для сложения каждого двойного слова (см. блок кода .L5 и .L27 в Вывод Годбола ).
Пропускная способность - хорошая точка (особенно для арифметики с плавающей запятой, которая имеет относительно большую задержку, но высокую пропускную способность). Мой настоящий C-код, где применяется SIMD - это гнездо с тройным циклом. Я разверну два внешних цикла, чтобы увеличить блок кода в самом внутреннем цикле, чтобы повысить пропускную способность. Тогда векторизации самого внутреннего достаточно. В примере с игрушкой в этом разделе вопросов и ответов, где я просто суммирую массив, я могу использовать -funroll-loops
, чтобы запросить GCC на развертывание цикла, используя несколько аккумуляторов для повышения пропускной способности.
В этом Q & A
Я думаю, что большинство людей будут относиться к этому вопросу более технически, чем я. Их может заинтересовать использование атрибутов компилятора или настройка флагов / параметров компилятора для принудительного встраивания функции. Поэтому и ответ Петра, и комментарий Марка под ответом все еще очень ценны. Еще раз спасибо.