OpenMPを使用したforループの並列実行の基本を学んでいます。
悲しいことに、私の並列プログラムはシリアルバージョンよりも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;
}
どうもありがとうございました!
問題は、内部ループ変数j
の競合状態が原因です。プライベートにする必要があります。
C89の場合、次のようにします。
#pragma omp parallel
{
int i, j, k;
#pragma omp for
for(i=0; ...
C++またはC99の場合、混合宣言を使用します
#pragma omp parallel for
for(int i=0; ...
これを行うと、共有またはプライベートを明示的に宣言する必要はありません。
あなたのコードへのいくつかのさらなるコメント。 B[k][j]
を実行すると、シングルスレッドコードはキャッシュフレンドリーではありません。これはキャッシュラインを読み取り、ドット積が完了するまで次のキャッシュラインに移動し、その時点で他のキャッシュラインが削除されます。代わりに、最初に転置を行い、BT[j][k]
としてアクセスする必要があります。さらに、1つの連続した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;
}
加えて。 「Zボソン」、Intel i5(2つの物理コアまたは4つの論理コア)を搭載したラップトップでCコードをテストしました。残念ながら、計算速度はそれほど速くありません。 2000x20ランダム二重行列の場合、次の結果が得られました(VS2010とOpenMP2.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
「HynekBlaha」コードの場合、私のシステムでの計算時間は739.208s(226.62s openMPの場合)であることに注意してください。
一方、Matlab x64:
n = 2000;
A = Rand(n); B = Rand(n);
tic
C = A*B;
toc
計算時間は.591440秒です。
しかし、openBLASパッケージを使用すると、.377814秒の速度に達しました(openMP 4.0でminGWを使用)。 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;
}
size
が小さい場合、スレッド同期のオーバーヘッドは、並列計算によるパフォーマンスの向上を隠します。