Матричное умножение в OpenMP С++ выполняется медленнее параллельно

Я изучаю основы параллельного выполнения цикла for, используя OpenMP.

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

double **basicMultiply(double **A, double **B, int size) {
   int i, j, k;
   double **res = createMatrix(size);
   omp_set_num_threads(4);
   #pragma omp parallel for private(k)
   for (i = 0; i < size; i++) {
      for (j = 0; j < size; j++) {
         for (k = 0; k < size; k++) {
            res[i][j] += A[i][k] * B[k][j];
         }
      }
   }
   return res;
}

Большое спасибо!

Ответы

Ответ 1

Ваша проблема связана с состоянием гонки на переменной внутреннего цикла j. Его нужно сделать приватным.

В C89 я бы сделал что-то вроде этого:

#pragma omp parallel
{
    int i, j, k;
    #pragma omp for
    for(i=0; ...

Для С++ или C99 используйте смешанные объявления

#pragma omp parallel for
for(int i=0; ...

Выполняя это, вам не нужно явно объявлять что-либо общее или личное.

Некоторые дополнительные комментарии к вашему коду. При использовании B[k][j] ваш однопоточный код не кэширует. Это считывает кешью, затем переходит к следующей строке кэша и так далее до тех пор, пока не будет выполнен точечный продукт, и к этому времени другие кегли будут выселены. Вместо этого сначала нужно перенести транспонирование и получить доступ как BT[j][k]. Кроме того, вы выделили массивы массивов, а не один непрерывный 2D-массив. Я исправил ваш код, чтобы использовать транспонирование и смежный 2D-массив.

Вот время, которое я получаю для size = 512.

no transpose  no openmp 0.94s
no transpose, openmp    0.23s
tranpose, no openmp     0.27s
transpose, openmp       0.08s

Ниже приведен код (см. также http://coliru.stacked-crooked.com/a/ee174916fa035f97)

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

void transpose(double *A, double *B, int n) {
    int i,j;
    for(i=0; i<n; i++) {
        for(j=0; j<n; j++) {
            B[j*n+i] = A[i*n+j];
        }
    }
}

void gemm(double *A, double *B, double *C, int n) 
{   
    int i, j, k;
    for (i = 0; i < n; i++) { 
        for (j = 0; j < n; j++) {
            double dot  = 0;
            for (k = 0; k < n; k++) {
                dot += A[i*n+k]*B[k*n+j];
            } 
            C[i*n+j ] = dot;
        }
    }
}

void gemm_omp(double *A, double *B, double *C, int n) 
{   
    #pragma omp parallel
    {
        int i, j, k;
        #pragma omp for
        for (i = 0; i < n; i++) { 
            for (j = 0; j < n; j++) {
                double dot  = 0;
                for (k = 0; k < n; k++) {
                    dot += A[i*n+k]*B[k*n+j];
                } 
                C[i*n+j ] = dot;
            }
        }

    }
}

void gemmT(double *A, double *B, double *C, int n) 
{   
    int i, j, k;
    double *B2;
    B2 = (double*)malloc(sizeof(double)*n*n);
    transpose(B,B2, n);
    for (i = 0; i < n; i++) { 
        for (j = 0; j < n; j++) {
            double dot  = 0;
            for (k = 0; k < n; k++) {
                dot += A[i*n+k]*B2[j*n+k];
            } 
            C[i*n+j ] = dot;
        }
    }
    free(B2);
}

void gemmT_omp(double *A, double *B, double *C, int n) 
{   
    double *B2;
    B2 = (double*)malloc(sizeof(double)*n*n);
    transpose(B,B2, n);
    #pragma omp parallel
    {
        int i, j, k;
        #pragma omp for
        for (i = 0; i < n; i++) { 
            for (j = 0; j < n; j++) {
                double dot  = 0;
                for (k = 0; k < n; k++) {
                    dot += A[i*n+k]*B2[j*n+k];
                } 
                C[i*n+j ] = dot;
            }
        }

    }
    free(B2);
}

int main() {
    int i, n;
    double *A, *B, *C, dtime;

    n=512;
    A = (double*)malloc(sizeof(double)*n*n);
    B = (double*)malloc(sizeof(double)*n*n);
    C = (double*)malloc(sizeof(double)*n*n);
    for(i=0; i<n*n; i++) { A[i] = rand()/RAND_MAX; B[i] = rand()/RAND_MAX;}

    dtime = omp_get_wtime();
    gemm(A,B,C, n);
    dtime = omp_get_wtime() - dtime;
    printf("%f\n", dtime);

    dtime = omp_get_wtime();
    gemm_omp(A,B,C, n);
    dtime = omp_get_wtime() - dtime;
    printf("%f\n", dtime);

    dtime = omp_get_wtime();
    gemmT(A,B,C, n);
    dtime = omp_get_wtime() - dtime;
    printf("%f\n", dtime);

    dtime = omp_get_wtime();
    gemmT_omp(A,B,C, n);
    dtime = omp_get_wtime() - dtime;
    printf("%f\n", dtime);

    return 0;

}

Ответ 2

Кроме того. "Z boson", я проверил ваш код C на ноутбуке с Intel i5 (2 физических ядра или 4 логических). К сожалению, скорость вычислений не очень быстро. Для случайных двойных матриц 2000x2000 я получил следующие результаты (используя VS 2010 с OpenMP 2.0):

Скомпилирован для Win64: C = A * B, где A, B - матрицы с размером (2000x2000):

max number of threads = 4
Create random matrices:      = 0.303555 s
no transpose  no openmp = 100.539924 s
no transpose, openmp = 47.876084 s
transpose, no openmp = 27.872169 s
transpose, openmp = 15.821010 s

Скомпилирован для Win32: C = A * B, где A, B - матрицы с размером (2000x2000):

max number of threads = 4
Create random matrices:      = 0.378804 s
no transpose  no openmp = 98.613992 s
no transpose, openmp = 48.233655 s
transpose, no openmp = 29.590350 s
transpose, openmp = 13.678097 s

Обратите внимание, что для кода "Hynek Blaha" время вычисления в моей системе 739.208s ( 226.62s с помощью openMP)!

В то время как в Matlab x64:

n = 2000; 
A = rand(n); B = rand(n);

tic
C = A*B;
toc

время вычисления 0,591440 секунд.

Но с использованием пакета openBLAS я достиг скорости 0.377814 секунд (используя minGW с openMP 4.0). Пакет Armadillo предоставляет простой способ (на мой взгляд) для подключения матричных операций с openBLAS (или другими подобными пакетами). В этом случае код

#include <iostream>
#include <armadillo>
using namespace std;
using namespace arma;

int main(){
    int n = 2000;
    N = 10; // number of repetitions
    wall_clock timer;

    arma_rng::set_seed_random();

    mat A(n, n, fill::randu), B(n, n, fill::randu);

    timer.tic();
    // repeat simulation N times
    for(int n=1;n<N;n++){
      mat C = A*B;
    }
    cout << timer.toc()/double(N) << "s" << endl;

    return 0;
}

Ответ 3

Если size невелик, накладные расходы на синхронизацию потоков будут теневое увеличение производительности от параллельных вычислений.