Распараллеливание с атомарным omp

Интересно, эквивалентны ли эти два цикла по производительности, когда a является глобальным int, а M - int []:

#pragma omp for
for (int i = 0; i < N; ++i) {
    #pragma omp atomic
    a += M[i];
}

for (int i = 0; i < N; ++i) {
    a += M[i];
}

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

Контрольный показатель? Также прочтите статью reduction().

Shawn 17.12.2018 23:29
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
134
1

Ответы 1

Проблема с вашим циклом без синхронизации заключается в том, что результат может быть неверным.

Чтобы ускорить вычисления, вы можете использовать предложение reduction следующим образом:

#pragma omp parallel for reduction(+:a)

Вы легко можете противопоставить эффекту расчетов:

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

int main(void)
{
    long long int setup = 0, sum_atomic = 0, sum_simple = 0, sum_reduc = 0, sum_noomp = 0;
    const int N = 100000;
    int *M = malloc(sizeof *M * N);
    double tick[5];

    for (int i = 0; i < N; ++i) {
        M[i] = i;
    }

    /* setup zone to prevent measuring first parallel zone drawback */
    #pragma omp parallel for 
    for (int i = 0; i < N; ++i) {
        setup += M[i];
    }

    /* using single thread execution */
    tick[0] = omp_get_wtime();
    for (int i = 0; i < N; ++i) {
        sum_noomp += M[i];
    }        

    /* using reduction */
    tick[1] = omp_get_wtime();
    #pragma omp parallel for reduction(+:sum_reduc)
    for (int i = 0; i < N; ++i) {
        sum_reduc += M[i];
    }

    /* using openmp, the wrong way */
    tick[2] = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < N; ++i) {
        sum_simple += M[i];
    }

    /* using atomic keyword */
    tick[3] = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < N; ++i) {
        #pragma omp atomic
        sum_atomic += M[i];
    }

    tick[4] = omp_get_wtime();

    printf("noomp:  %lld, in %.0f us\n", sum_noomp,  1000000.*(tick[1]-tick[0]));
    printf("reduc:  %lld, in %.0f us\n", sum_reduc,  1000000.*(tick[2]-tick[1]));
    printf("simple: %lld, in %.0f us\n", sum_simple, 1000000.*(tick[3]-tick[2]));   
    printf("atomic: %lld, in %.0f us\n", sum_atomic, 1000000.*(tick[4]-tick[3]));

    free(M);

    return 0;
}

На двухъядерном ЦП результат будет следующим:

noomp:  4999950000, in 28 us  -- single thread quite fast 
reduc:  4999950000, in 17 us  -- reduction: twice fast
simple: 2024135316, in 12 us  -- incorrect sum 
atomic: 4999950000, in 3686 us -- atomic kw: slow compared to single thread version

Так:

  • более быстрый способ - использовать openmp с reduction
  • openmp медленнее, чем последовательная версия при использовании atomic
  • Результат openmp неверен при использовании без reduction или atomic

Подробности:

Скомпилирован с помощью gcc 8.2.1 с опцией -Wall -O3 -mtune=native -march=native -fopenmp на tio машина, с использованием двух ядер процессора Xeon E5-2650 v4.

Для протокола: встреча с самой первой областью parallel обычно требует гораздо более затратной настройки среды OpenMP, чем любые последующие. Следовательно, способ написания вашего теста дает очень несправедливый недостаток первому циклу (тот, который использует atomic). Точно так же, чтобы представить настоящий интерес, ваше сравнение также должно включать время для последовательного кода в качестве ссылки. И, наконец, вы должны указать компилятор и переключатели, которые вы использовали, чтобы позволить другим воспроизвести ваши результаты.

Gilles 18.12.2018 07:42

Спасибо, что учли мои комментарии. Теперь последнее слово мудрости: никогда не тестируйте код без полного уровня оптимизации со стороны компилятора, иначе результаты будут бессмысленными. Обычно здесь заменяют ваш -O0 на -O3 -mtune=native -march=native.

Gilles 18.12.2018 10:46

@ Жиль: Хорошо, я понял. Во-первых, я отключил оптимизацию, потому что опасаюсь, что компилятор не оптимизирует цикл for ...

Mathieu 18.12.2018 10:53

Поскольку вы распечатываете результат, этого быть не должно.

Gilles 18.12.2018 10:54

Другие вопросы по теме