#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
также выполняется забор памяти.