Неправильная замена прагмы OpenMP в расширении макроса

Когда прагма OpenMP используется как часть аргумента макроса, она заменяется неправильно. В этом коде:

#define make_body( ... ) { __VA_ARGS__ }
extern foo( int );
int main(){
  make_body(
    #pragma omp parallel for
    for( int i = 0; i < 10; i += 1 ){
      foo( i );
    }
  )
}

Я ожидаю, что он будет расширен до:

extern foo( int )
int main(){
  {
    #pragma omp parallel for
    for( int i = 0; i < 10; i += 1 ){
      foo( i );
    }
  }
}

Однако (согласно gcc -E) он расширяется до:

extern foo( int );
int main(){
#pragma omp parallel for
  { 
    for( int i = 0; i < 10; i += 1 ){ 
      foo( i ); 
    } 
  }
}

Это правильное поведение? Как я могу получить ожидаемое поведение, желательно без изменения аргументов макроса? Это происходит со всеми прагмами? Это эффект вариативного макроса? Выполняют ли другие компиляторы ту же замену?

Использование gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609


person memorableusername    schedule 20.11.2018    source источник
comment
Мне любопытно: каков реальный целевой вариант использования? Пример, представленный в вопросе, не может быть им, потому что было бы проще и менее хлопотно поставить литерал {}, как нормальный человек.   -  person John Bollinger    schedule 21.11.2018
comment
Реальный вариант использования — это библиотека синхронизации кода, которая вставляет код остановки, запуска и истекшего времени таймера вокруг раздела кода. До сих пор он работал довольно хорошо, но этот пример OpenMP, к сожалению, все ломает. Чтобы обойти это, я добавлю несколько макросов для этих отдельных компонентов, чтобы раздел кода не был аргументом макроса.   -  person memorableusername    schedule 21.11.2018


Ответы (3)


Это правильное поведение?

Было бы точнее сказать, что это не неправильное поведение. Стандарт говорит об аргументах макроса следующее:

Если в списке аргументов есть последовательности токенов предварительной обработки, которые в противном случае действовали бы как директивы предварительной обработки, поведение не определено.

Вы воспользовались этим случаем и, соответственно, получили неопределенное поведение.

Это происходит со всеми прагмами? Это эффект вариативного макроса? Выполняют ли другие компиляторы ту же замену?

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

Как я могу получить ожидаемое поведение, желательно без изменения аргументов макроса?

C11, который поддерживается GCC 5, имеет решение для конкретного случая макросов, которые выдают прагмы: оператор _Pragma. Он чаще используется в буквальном тексте замены макроса, так что у вас может быть макрос, представляющий прагму, но он также должен работать в аргументе макроса. Однако вам придется немного изменить аргумент макроса:

  make_body(
    _Pragma("omp parallel for")
    for( int i = 0; i < 10; i += 1 ){
      foo( i );
    }
  )
person John Bollinger    schedule 20.11.2018
comment
Boo неопределенное поведение! Спасибо за подробное объяснение. - person memorableusername; 21.11.2018

Вам не разрешено помещать какие-либо директивы предварительной обработки в аргументы макроса, подобного функции. (C11 6.10.3p11, последнее предложение.)

В этом случае вы должны написать _Pragma("omp parallel for") и получить желаемый эффект. Эксперименты показывают, что это правильно работает как с clang, так и с gcc (версии 7 и 8 соответственно), но только если вы укажете -fopenmp в командной строке. Да, даже при использовании -E.

person zwol    schedule 20.11.2018
comment
_Pragma — это функция C99, ей почти 20 лет! stackoverflow.com/questions/45477355 / - person Jim Cownie; 26.11.2018
comment
Я комментирую, чтобы подчеркнуть последнюю часть, потому что она меня очень сильно укусила. Для openmp вам обязательно нужно использовать -fopenmp, чтобы это работало: godbolt.org/z/h9NI-n - person memorableusername; 21.06.2019

Альтернативное решение: это исправляет случай, который мы видели в реальном коде, который слишком велик для воссоздания здесь (не то чтобы мы были точно уверены, почему это все равно происходит). -fopenmp также необходимо.

#define EMPTY()
#define DELAY(x) x EMPTY()
#define STRINGIZE(x) STRINGIZE_NO_PREPROCESS(x)
#define STRINGIZE_NO_PREPROCESS(x) #x

#define PRAGMA(x) _Pragma( STRINGIZE(x) )
#define DELAYED_PRAGMA_assist(x) PRAGMA(x)
#define DELAYED_PRAGMA DELAY(DELAYED_PRAGMA_assist)

#define PRAGMA_IN_MACRO_ARG(x) DELAYED_PRAGMA(x)
#define PRAGMA_IN_MACRO_BODY(x) PRAGMA(x)

PRAGMA_IN_MACRO_ARG можно использовать для правильного размещения прагмы, когда код передается другому макросу в качестве аргумента. Например:

#define WRAP( body ) { body }

#define loop( i, N, body )\
if( N > 0 ) \
WRAP( \
  PRAGMA_IN_MACRO_ARG(omp parallel for private(i)) \
  for( i = 0; i < N; ++i ){ \
      body \
  } \
)

loop( i, 10, printf("%d\n", i); )

PRAGMA_IN_MACRO_BODY можно использовать для правильного размещения прагмы, когда код является просто частью тела макроса, а не входными данными для другого макроса. Например:

#define loop( i, N, body )\
if( N > 0 ) \
{\
  PRAGMA_IN_MACRO_BODY(omp parallel for private(i)) \
  for( i = 0; i < N; ++i ){ \
      body \
  } \
}

loop( i, 10, printf("%d\n", i); )
person memorableusername    schedule 26.07.2019