пинг-понг openmp прерывается при использовании оптимизации

#openmp

#openmp

Вопрос:

У меня есть следующая программа openmp, скомпилированная с помощью mpicc -fopenmp -O0 ping_pong.c. На моей машине выполняется ./a.out -N 10000000 обычно выдает «сделано за 1.2225 секунды, m: 10000001». Если я повышаю уровень оптимизации, программа зависает. Есть ли способ 1) уменьшить время выполнения при сохранении функциональности ping pong? 2) сделать код терпимым (без зависания, не медленнее) к оптимизации?

 #include <omp.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int main(int argc, char **argv) { 
  int num_threads = 2; 
  int N = 1000000; 
 
  for (int i = 1; i < argc; i  ) { 
    if (strcmp(argv[i], "-N") == 0) { 
      N = atoi(argv[  i]); 
    } 
  } 
 
  omp_set_num_threads(num_threads); 
 
  int m = 0; 
 
  double t0 = omp_get_wtime(); 
  #pragma omp parallel 
  { 
    int id = omp_get_thread_num(); 
    while (m < N) { 
      if (id == 0) { 
        if (m % 2 == 0) m  ; 
      } 
      if (id == 1) { 
        if (m % 2 == 1) m  ; 
      } 
    } 
  } 
  double t = omp_get_wtime() - t0; 
  printf("done in %g secs, m: %dn", t, m); 
}
  

Комментарии:

1. На самом деле это не параллельный код. Конечно, вы вставили параллельную прагму, но без конструкции совместного использования работы (например, цикла for) все это означает, что несколько потоков выполняют один и тот же код.

2. @HighPerformanceMark код внутри while цикла представляет sections собой конструкцию OpenMP worksharing, написанную явно.

3. Да, я вижу, что @HristoIliev, я полагаю, мой предыдущий комментарий мог быть лучше выражен, поскольку на самом деле это не код OpenMP …

Ответ №1:

Более быстрая и полностью оптимизируемая очистка переменной m перед каждым оператором if.

 // to compile: gcc -fopenmp -O* ping_pong.c
// * can be 0, 1, 2, 3, or fast
// to run: ./a.out -N 10000000

#include <assert.h>
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv) {
  int num_threads = 2;
  int N = 1000000;

  for (int i = 1; i < argc; i  ) {
    if (strcmp(argv[i], "-N") == 0) {
      N = atoi(argv[  i]);
    }
  }

  omp_set_num_threads(num_threads);

  int m = 0;

  int count0 = 0;
  int count1 = 0;
  int *arr0 = (int *)calloc(N / 2   2, sizeof(int));
  int *arr1 = (int *)calloc(N / 2   2, sizeof(int));

  double t0 = omp_get_wtime();
  #pragma omp parallel
  {
    int id = omp_get_thread_num();
    if (id == 0) {
      printf("id %d reporting for duty!n", id);
      while (m < N) {
        #pragma omp flush (m)
        if (m % 2 == 0) {
          arr0[count0] = m;
          m  ;
          count0  ;
        }
      }
    }
    else if (id == 1) {
      printf("id %d reporting for duty!n", id);
      while (m < N) {
        #pragma omp flush (m)
        if (m % 2 == 1) {
          arr1[count1] = m;
          m  ;
          count1  ;
        }
      }
    }
  }
  double t = omp_get_wtime() - t0;
  printf("done in %g secs, m: %d, count0: %d, count1: %dn", t, m, count0, count1);

  for (int i = 1; i < N / 2; i  ) {
    if (arr0[i] != arr0[i - 1]   2) {
      printf("arr0[%d] = %d, arr0[%d] = %dn", i, arr0[i], i - 1, arr0[i - 1]);
      assert(0);
    }
  }

  for (int i = 1; i < N / 2; i  ) {
    if (arr1[i] != arr1[i - 1]   2) {
      printf("arr1[%d] = %d, arr1[%d] = %dn", i, arr1[i], i - 1, arr1[i - 1]);
      assert(0);
    }
  }

  printf("Both arrays are correctly formed.n");

  free(arr0);
  free(arr1);

  return 0;
}
  

Комментарии:

1. Идиоматический код OpenMP будет использовать sections конструкцию с двумя section блоками вместо if (id == 0) { ... } else if (id == 1) { ... } .

2. Наверняка на m есть гонки? (m является общим, а m не является атомарным).

3. @HristoIliev Я думаю, что идиоматический код OpenMP будет использовать omp for schedule(dynamic) 🙂

4. @JimCownie, с моей точки зрения, на самом деле нет гонок m из-за того, как работает код. Поток 0 увеличивается только m в том случае, если он четный. Как только это произойдет, m он станет нечетным, и поток 0 не будет обновлять его снова, прежде чем поток 1 запустится и m снова выровняется. Повторный flush(m) гарантирует, что даже если поток пропустит приращение от другого потока в текущей итерации цикла, он в конечном итоге сбросит свои изменения и получит новое значение после одной или нескольких итераций.

5. @HristoIliev Вы, конечно, правы. С моей точки зрения, это остается странным фрагментом кода, но, возможно, он предназначен для учебного примера, а не для того, что действительно хотелось бы сделать! (В конце концов, если бы вы действительно хотели такого распределения работы, #pragma omp для schedule(static,1) добился бы этого гораздо более очевидным образом).

Ответ №2:

Модель памяти OpenMP позволяет различным потокам временно различать представления общих переменных. В архитектурах, совместимых с кэшем, таких как x86, наиболее частой причиной расхождения представлений являются оптимизации регистров.

Это очень сильно зависит от компилятора, но -O0 большинство компиляторов не выполняют оптимизацию регистров, поэтому оба if (m % 2 == 0) и m приводят к коду, который считывает или записывает фактическую ячейку памяти m . С -O1 и выше m оптимизируется для переменной регистра, и результат записывается обратно в память только при выходе из while цикла. В последнем случае после одной итерации значение регистра m в потоке 0 становится нечетным, и поток застревает. Аналогично, начальное значение m в потоке 1 равно четному ( 0 ), и этот поток уже застрял.

Конструкция OpenMP предназначена для предотвращения оптимизации регистров (и спекулятивного выполнения / переупорядочения инструкций) от искажения согласованного представления общих переменных flush . Вам нужно несколько #pragma omp flush(m) строк, чтобы убедиться, что оба потока видят последнее значение m .

Вы также можете объявить m как volatile int m = 0 . volatile Модификатор предотвращает оптимизацию регистра m , поэтому вы получите код, аналогичный тому, что -O0 производит. Это не то же самое, что использование директивы OpenMP flush , поскольку на x86 flush также выполняется забор памяти.