何となく Blog by Jitta
Microsoft .NET 考

目次

Blog 利用状況
  • 投稿数 - 761
  • 記事 - 18
  • コメント - 36042
  • トラックバック - 222
ニュース
  • IE7以前では、表示がおかしい。div の解釈に問題があるようだ。
    IE8の場合は、「互換」表示を OFF にしてください。
  • 検索エンジンで来られた方へ:
    お望みの情報は見つかりましたか? よろしければ、コメント欄にどのような情報を探していたのか、ご記入ください。
It's ME!
  • はなおか じった
  • 世界遺産の近くに住んでます。
  • Microsoft MVP for Visual Developer ASP/ASP.NET 10, 2004 - 9, 2011
広告

記事カテゴリ

書庫

日記カテゴリ

ギャラリ

その他

わんくま同盟

同郷

 

処理並列は、必ず処理速度が向上するのか

 3つのアルゴリズムを並列化して、並列化によってどのように処理効率が向上するのか、検証します。

はじめに

 CPUは、コンピューターの頭脳です。単純に考えると、頭脳が2つあれば、1つの時と比べて、同じ時間でたくさんのことを考えることができそうです。10年ほど前、Intel製プロセッサーPentium IIの頃から、1枚のボードに複数のCPUを載せて高速化する試みがなされました。Intel製CPUでは2ユニットまででしたが、SUN Microsystems製のコンピューターでは、もっとたくさんのユニットを載せることができていました。今では、CPU自体に複数の「コア」を載せ、1ユニットで同時に別々のことを実行できるようになっています。

 ハードウェアが、同時に複数のことを実行できるようになったため、ソフトウェアもそれに対応する必要が出てきました。本記事では、昨今あちらこちらで声高に繰り返される、「並列処理化すれば速くなる」に疑問を持ったため、それを検証することにします。

対象読者

 なんといっても、並列処理化に興味のある方が対象です。特に、「並列化すれば速くなる」と、単純に思い込んでいる方に、読んでもらいたいと思います。

 本記事では、Visual C++ 2008を元に、基本的にC言語によるコードにより、検証を行います。よって、ある程度のC言語の知識があるとよいでしょう。しかし、処理は日本語で説明をするようにしますので、C言語の文法を知っている必要はありません。

必要な環境

 本記事を読む分には、特別な環境を必要としません。

 本記事で紹介するコード例を試すには、Visual Studio C++ 2008を使用します。また、実際に差異を見るためには、タスクマネージャーで複数のCPU使用履歴が表示されるPCが必要です。

並列処理化の有用性を検証する

どうやって検証するか

 まず、検証方法を設計します。どういう仮説があって、その仮説が正しいことを証明するためには、どの様な実験から、どの様な結果が出ればよいのか。これらを明らかにしておかなければ「検証」ではなく、「観察」になってしまいます。

 本記事は「並列化すれば速くなる」という仮定を検証します。このために、いくつかのアルゴリズムについてコードを作成します。そのコードを、Open MPによってマルチスレッド化します。シングル、マルチスレッドで実行完了までにかかる時間を計測し、それぞれマルチスレッドによってどれくらい実行時間が短縮できたかを算出します。短縮時間を比べることで、マルチスレッド化がどのようなな時にも有効なのか、調査することとします。

アルゴリズム1:素数を求める

 素数とはなんでしょうか。1と、その数以外の数では割り切ることができない数です。では、どうやってこれを求めましょうか。2からその数の手前までの数で順番に割ってみて、割りきれる数があるかどうか調べます。つまり、11が素数かどうか調べるには、2、3、4、5、6、7、8、9、10で割り切れるかどうかを調べます。コードで示すと次のようになります。

素数を判別するコード例
// 素数かどうか、判別する
// 戻り値:0…素数ではない / 0以外…素数
int IsPrimeNumber(const int 調べる数)
{
  for (int i = 2; i < 調べる数; ++i) {
      if (調べる数 % i == 0) {
          return 0;
      }
  }
  return 1;
}

 素数を求めたい範囲について、for文でループします。それぞれの数についてこの判別関数を通すことで、素数かどうかを調べます。とりあえず、この関数では「素数の数」だけ返すようにします。処理を並列化することの効率を測定するので、コンソールへの出力は行いません。コンソールへの出力がシリアル化される、コンソール出力は遅い処理である、というのが理由です。求めた素数は配列へ格納します。配列を動的に拡張すると、メモリを確保するという並列化できない要素が出てくるため、呼び出し側で確保しておくこととします。

ある数までの素数を求めるコード例
int GetPrimeNumbers(const int この数まで, int *素数配列 = NULL, const int 配列数 = 0)
{
    int count = 0;
    for (int i = 2; i <= この数まで; ++i) {
        if (IsPrimeNumber(i) != 0) {
            ++count;
            if (素数配列 != NULL && index < 配列数) {
                素数配列[i] = i;
            }
        }
    }
    return count;
}

 これで「ある数までの素数の数を求めるコード」ができました。では、このコードが正しいかどうか、検証します。コードによって、1~20までの素数の数を求めさせます。これくらいなら、自分で計算もできるでしょう。その結果とつきあわせます。

素数(の数)を求めるコードを検証するコード
#include "stdafx.h"
#include <iostream>
#include <omp.h>
int IsPrimeNumber(const int 調べる数) をここに
int GetPrimeNumbers(const int この数まで, int *素数配列 = NULL, const int 配列数 = 0) をここに
int _tmain(int argc, _TCHAR* argv[])
{
    std::cout << GetPrimeNumbers(2) << "個/2…1\n";
    std::cout << GetPrimeNumbers(3) << "個/3…2\n";
    std::cout << GetPrimeNumbers(4) << "個/4…2\n";
    std::cout << GetPrimeNumbers(5) << "個/5…3\n";
    std::cout << GetPrimeNumbers(6) << "個/6…3\n";
    std::cout << GetPrimeNumbers(7) << "個/7…4\n";
    std::cout << GetPrimeNumbers(8) << "個/8…4\n";
    std::cout << GetPrimeNumbers(9) << "個/9…4\n";
    std::cout << GetPrimeNumbers(10) << "個/10…4\n";
    std::cout << GetPrimeNumbers(11) << "個/11…5\n";
    std::cout << GetPrimeNumbers(12) << "個/12…5\n";
    std::cout << GetPrimeNumbers(13) << "個/13…6\n";
    std::cout << GetPrimeNumbers(14) << "個/14…6\n";
    std::cout << GetPrimeNumbers(15) << "個/15…6\n";
    std::cout << GetPrimeNumbers(16) << "個/16…6\n";
    std::cout << GetPrimeNumbers(17) << "個/17…7\n";
    std::cout << GetPrimeNumbers(18) << "個/18…7\n";
    std::cout << GetPrimeNumbers(19) << "個/19…8\n";
    std::cout << GetPrimeNumbers(20) << "個/20…8\n";
    return 0;
}
実行結果
1個/2…1
2個/3…2
2個/4…2
3個/5…3
3個/6…3
4個/7…4
4個/8…4
4個/9…4
4個/10…4
5個/11…5
5個/12…5
6個/13…6
6個/14…6
6個/15…6
6個/16…6
7個/17…7
7個/18…7
8個/19…8
8個/20…8

 コードが正しいことが検証できました。これを基に、マルチスレッド化のためのコードを追加します。

GetPrimeNumbersをマルチスレッド化する

int GetPrimeNumbersParallel(const int この数まで, int *素数配列 = NULL, const int 配列数 = 0)
{
    int count = 0;
    #pragma omp parallel for reduction(+:count)
    for (int i = 2; i <= この数まで; ++i) {
        if (IsPrimeNumber(i) != 0) {
            ++count;
            if (素数配列 != NULL && i < 配列数) {
                素数配列[i] = i;
            }
        }
    }
    return count;
}

 これも、検証しておきます。GetPrimeNumbersがシングルスレッドで動作することと、GetPrimeNumbersParallelがマルチスレッドで動作しているかを確認します。確認を行う土台が、確認を行う目的にとって正しいことを確認しておかないと、何を確認しているのかわからなくなります。

 次のように_tmain関数を書き換え、実行します。実行中、タスクマネージャーの[プロセス]タブで実行しているプロセスのスレッド数と、CPU使用率を見ます。スレッド数は、初期状態では表示されていないので、[表示]メニューから[列の選択]を選び、一覧から[スレッド数]を探してチェックします。シングルスレッドの方は、スレッド数が“1”、CPU使用率は“(100/CPU数)%”であることを確認します。また、マルチスレッドの方は、スレッド数が“CPU数”、CPU使用率が“100%”であることを確認します。もし、マルチスレッド側のスレッド数が“1”の場合、プロジェクトのプロパティで、[構成プロパティ]→[C/C++]→[言語]を開き、[OpenMPサポート]が「はい(/openmp)」になっていることを確認してください。

シングルスレッドであることを確認する
int _tmain(int argc, _TCHAR* argv[])
{
    GetPrimeNumbers(300000);
    return 0;
}
マルチスレッドであることを確認する
int _tmain(int argc, _TCHAR* argv[])
{
    GetPrimeNumbersParallel(300000);
    return 0;
}

アルゴリズム2:FizzBuzz問題

 FizzBuzz問題はご存知ですね。1から任意の数までについて出力します。しかし、3の倍数の時は「Fizz」、5の倍数の時は「Buzz」と出力します。これを単純にコード化すると、次のようになります。

FizzBuzz問題のコード
void FizzBuzz(const int この数まで)
{
    for (int i = 1; i <= この数まで; ++i) {
        if (i % 15 == 0) {
            std::cout << "FizzBuzz ";
        } else if (i % 3 == 0) {
            std::cout << "Fizz ";
        } else if (i % 5 == 0) {
            std::cout << "Buzz ";
        } else {
            std::cout << i << " ";
        }
    }
}

 しかし、こうすると、出力結果でコンソールが大変なことになります。また、このように出力すると並列化したときに「実行順」が問題になります。そのため、ここでは文字列領域を確保し、そこに入れていくことにします。これも動作の検証をして、並列化したコードも作ります。ここでは動作検証は省きますが、実際にやってみる場合は、しっかり検証してください。

FizzBuzz問題のコード(文字列格納型)
#define FIZZBUZZMAX 5000000
char FB結果[FIZZBUZZMAX][12];
void FizzBuzz(const int この数まで)
{
    for (int i = 1; i <= この数まで; ++i) {
        if (i % 15 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "FizzBuzz");
        } else if (i % 3 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Fizz");
        } else if (i % 5 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Buzz");
        } else {
            sprintf_s(FB結果[i], sizeof(FB結果[i]), "%d", i);
        }
    }
}
void FizzBuzzParallel(const int この数まで)
{
    #pragma omp parallel for
    for (int i = 1; i <= この数まで; ++i) {
        if (i % 15 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "FizzBuzz");
        } else if (i % 3 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Fizz");
        } else if (i % 5 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Buzz");
        } else {
            sprintf_s(FB結果[i], sizeof(FB結果[i]), "%d", i);
        }
    }
}

アルゴリズム3:バブルソート

 3つ目のアルゴリズムは、バブルソートです。

 バブルソートのアルゴリズムをおさらいしましょう。配列に数値が並んでいます。この数値を、小さい順に並べるとします。まず、配列の1番後ろと、その1つ手前を比べます。後ろにある方が大きければ、入れ替えます。次に、後ろから2番目と、その1つ手前を比べます。そして、1つ手前の方が大きければ入れ替えます。これを、最初の要素まで繰り返します。最初までくると、「1番小さい数値」が決定します。そこで今度は、「2番目に小さい数値」を決定すべく、また1番後ろから比較を繰り返します。

バブルソートのコード
#define BSORTNUM 100000
int BS配列[BSORTNUM];
void BubbleSort(int* 対象配列, const int 配列数)
{
    for (int 決定済み数 = 0; 決定済み数 < 配列数; ++決定済み数) {
        for (int y = 配列数 - 1; y > 決定済み数; --y) {
            if (対象配列[y] < 対象配列[y - 1]) {
                int tmp = 対象配列[y];
                    対象配列[y] = 対象配列[y - 1];
                    対象配列[y - 1] = tmp;
            }
        }
    }
}
void BubbleSortParallel(int* 対象配列, const int 配列数)
{
    for (int 決定済み数 = 0; 決定済み数 < 配列数; ++決定済み数) {
        #pragma omp parallel for
        for (int y = 配列数 - 1; y > 決定済み数; --y) {
            if (対象配列[y] < 対象配列[y - 1]) {
                int tmp = 対象配列[y];
                対象配列[y] = 対象配列[y - 1];
                対象配列[y - 1] = tmp;
            }
        }
    }
}

 さて、これも検証しておきます。すると、並列化した方で、結果がおかしいことが分かります。以下の結果は、BSORTNUMを100として検証したときの結果です。「BS配列」は、BSORTNUM~1で初期化するようにしました。

バブルソートの検証をするコード
void Shuffle(int *対象配列, const int 配列数)
{
    for (int i = 配列数 - 1; i >= 0; --i) {
        対象配列[i] = 配列数 - i;
    }
}
int _tmain(int argc, _TCHAR* argv[])
{
    for (int i = 0; i < 1; ++i) {
        Shuffle(BS配列, BSORTNUM);
        std::cout << "ソート前\n";
        for (int i = 0; i < BSORTNUM; ++i) {
            std::cout << BS配列[i] << "\t";
        }
        std::cout << "\n----------\nソート後\n";
        BubbleSortParallel(BS配列, BSORTNUM);
    }
    for (int i = 0; i < BSORTNUM; ++i) {
        std::cout << BS配列[i] << "\t";
    }
    return 0;
}
実行結果(一例)
ソート前
88      25      18      83      54      90      79      2       49      73
94      92      13      14      97      52      96      75      27      38
100     39      61      40      59      24      7       26      29      30
42      31      23      55      53      36      37      91      11      98
62      74      43      66      71      46      47      95      58      16
51      45      76      6       99      17      19      12      82      60
33      41      35      64      84      5       67      22      68      70
50      72      10      65      80      86      77      44      9       78
81      85      93      32      87      69      8       1       89      3
20      15      4       63      48      56      28      21      34      57
----------
ソート後
1       2       3       4       4       5       6       7       8       9
10      11      12      13      14      15      16      17      18      19
20      21      21      22      23      24      25      26      27      28
29      30      31      33      34      35      36      37      38      39
40      41      42      43      44      45      46      47      48      49
88      50      51      83      54      90      79      52      53      73
94      92      55      56      97      57      96      75      58      59
100     60      61      62      63      64      65      66      67      68
69      70      71      72      74      76      77      91      78      98
80      81      82      84      86      87      89      95      93      99

 検証するクセを付けておいてよかったでしょ?

 実は、バブルソートのアルゴリズムは、並列化に向いていません。複数の並列に行われる処理が、同じ領域を書き換えようとするためです。上の実行結果では、「4」が2回現れています。代わりに、32がなくなっています。「4」のことを除くと50までは正しいように思われますが、51以上は並び方がおかしいです。これは「対象配列[y]」と「対象配列[y-1]」の比較入れ替えで、複数のスレッドが、yの値が同じところを操作しようとしたためです。

 これを防ぐために、バブルソートのアルゴリズムについて、調べなければなりません。先に書いているように、バブルソートでは、検査をする場所を、順番に調べなければなりません。単純に並列化すると、実行する順序は保障されません。実行順序が保障されない、すなわち検査する順序が前後すると、並びがおかしくなるケースが発生します。

 OpenMPでは、ordered句とorderedディレクティブを使うことで、順番に実行される事を保障します。まず、forディレクティブにordered句を指定して、「順番に実行されるブロックがある」ことを指示します。次に、orderedディレクティブによって、順番に実行されるブロックを明示します。

バブルソートのコード(順序づけとマーク)

void BubbleSortParallel(int* 対象配列, const int 配列数)
{
    for (int 決定済み数 = 0; 決定済み数 < 配列数; ++決定済み数) {
        #pragma omp parallel for ordered
        for (int y = 配列数 - 1; y > 決定済み数; --y) {
            #pragma omp ordered
            if (対象配列[y] < 対象配列[y - 1]) {
                int tmp = 対象配列[y];
                対象配列[y] = 対象配列[y - 1];
                対象配列[y - 1] = tmp;
            }
        }
    }
}
実行結果(一例)
ソート前
63      2       3       100     5       81      16      82      86      53
11      1       13      14      34      35      58      74      19      40
8       22      23      24      73      18      27      28      9       30
55      56      33      76      99      12      98      4       26      90
68      37      57      44      67      95      47      48      32      69
51      52      78      54      80      38      94      17      59      60
72      49      43      64      65      66      45      41      50      70
88      7       25      84      77      15      62      10      36      92
6       21      83      61      85      29      87      42      89      96
91      20      93      79      46      31      97      71      39      75
----------
ソート後
1       2       3       4       5       6       7       8       9       10
11      12      13      14      15      16      17      18      19      20
21      22      23      24      25      26      27      28      29      30
31      32      33      34      35      36      37      38      39      40
41      42      43      44      45      46      47      48      49      50
51      52      53      54      55      56      57      58      59      60
61      62      63      64      65      66      67      68      69      70
71      72      73      74      75      76      77      78      79      80
81      82      83      84      85      86      87      88      89      90
91      92      93      94      95      96      97      98      99      100

 さて、一通りできました。ここまでのコードを、もう一度掲示しておきます。

検証コード(全体)
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <math.h>
#include <omp.h>
#include <sys/types.h>
#include <sys/timeb.h>
#include <time.h>
#include <stdlib.h>
#define PRIMENUMBERS    500000
#define FIZZBUZZMAX     30000000
#define BSORTNUM        30000
#define CHECKNUM        5
char FB結果[FIZZBUZZMAX][12];
int BS配列[BSORTNUM];
int PR配列[PRIMENUMBERS];
#define TmToSec(X) ((X).time + (X).millitm / 1000.0)
#define DiffTmSec(START, STOP) (TmToSec(STOP) - TmToSec(START))
// 素数かどうか、判別する
// 戻り値:0…素数ではない / 0以外…素数
int IsPrimeNumber(const int 調べる数)
{
    for (int i = 2; i < 調べる数; ++i) {
        if (調べる数 % i == 0) {
            return 0;
        }
    }
    return 1;
}
int IsPrimeNumberFast(const int 調べる数)
{
    if (調べる数 == 2) { return 1;}
    if (調べる数 % 2 == 0) { return 0;}
    int limit = (int) sqrt((double)調べる数);
    for (int i = 3; i < limit; i += 2) {
        if (調べる数 % i == 0) {
            return 0;
        }
    }
    return 1;
}
int GetPrimeNumbers(const int この数まで,
    int *素数配列 = NULL, const int 配列数 = 0)
{
    int count = 0;
    for (int i = 2; i <= この数まで; ++i) {
        if (IsPrimeNumber(i) != 0) {
            ++count;
            if (素数配列 != NULL && i < 配列数) {
                素数配列[i] = i;
            }
        }
    }
    return count;
}
int GetPrimeNumbersParallel(const int この数まで,
    int *素数配列 = NULL, const int 配列数 = 0)
{
    int count = 0;
    #pragma omp parallel for reduction(+:count) //schedule(guided)
    for (int i = 2; i <= この数まで; ++i) {
        if (IsPrimeNumber(i) != 0) {
            ++count;
            if (素数配列 != NULL && i < 配列数) {
                素数配列[i] = i;
            }
        }
    }
    return count;
}
void FizzBuzz(const int この数まで)
{
    for (int i = 1; i <= この数まで; ++i) {
        if (i % 15 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "FizzBuzz");
        } else if (i % 3 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Fizz");
        } else if (i % 5 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Buzz");
        } else {
            sprintf_s(FB結果[i], sizeof(FB結果[i]), "%d", i);
        }
    }
}
void FizzBuzzParallel(const int この数まで)
{
    #pragma omp parallel for
    for (int i = 1; i <= この数まで; ++i) {
        if (i % 15 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "FizzBuzz");
        } else if (i % 3 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Fizz");
        } else if (i % 5 == 0) {
            strcpy_s(FB結果[i], sizeof(FB結果[i]), "Buzz");
        } else {
            sprintf_s(FB結果[i], sizeof(FB結果[i]), "%d", i);
        }
    }
}
void BubbleSort(int* 対象配列, const int 配列数)
{
    for (int 決定済み数 = 0; 決定済み数 < 配列数; ++決定済み数) {
        for (int y = 配列数 - 1; y > 決定済み数; --y) {
            if (対象配列[y] < 対象配列[y - 1]) {
                int tmp = 対象配列[y];
                対象配列[y] = 対象配列[y - 1];
                対象配列[y - 1] = tmp;
            }
        }
    }
}
void BubbleSortParallel(int* 対象配列, const int 配列数)
{
    for (int 決定済み数 = 0; 決定済み数 < 配列数; ++決定済み数) {
        #pragma omp parallel for ordered
        for (int y = 配列数 - 1; y > 0; --y) {
            #pragma omp ordered
            if (対象配列[y] < 対象配列[y - 1]) {
                int tmp = 対象配列[y];
                対象配列[y] = 対象配列[y - 1];
                対象配列[y - 1] = tmp;
            }
        }
    }
}
void Shuffle(int *対象配列, const int 配列数)
{
    for (int i = 配列数 - 1; i >= 0; --i) {
        対象配列[i] = 配列数 - i;
    }
}
int _tmain(int argc, _TCHAR* argv[])
{
    struct _timeb startTm, stopTm;    // ストップウォッチ用
    std::cout << "素数の数を求める範囲\t" << PRIMENUMBERS << "\n";
    std::cout << "FizzBuzz を求める範囲\t" << FIZZBUZZMAX << "\n";
    std::cout << "Bubble Sort の要素数\t" << BSORTNUM << "\n";
    std::cout << "繰り返し回数\t" << CHECKNUM << "\n\n";
    /**/
    double bsSeri = 0.0;
    for (int i = 0; i < CHECKNUM; ++i) {
        Shuffle(BS配列, BSORTNUM);
        _ftime_s(&startTm);
        BubbleSort(BS配列, BSORTNUM);
        _ftime_s(&stopTm);
        std::cout << "BubbleSort シリアル\t"
            << DiffTmSec(startTm, stopTm) << " sec.\n";
        bsSeri += DiffTmSec(startTm, stopTm);
    }
    /**/
    double bsPall = 0.0;
    for (int i = 0; i < CHECKNUM; ++i) {
        Shuffle(BS配列, BSORTNUM);
        _ftime_s(&startTm);
        BubbleSortParallel(BS配列, BSORTNUM);
        _ftime_s(&stopTm);
        std::cout << "BubbleSort パラレル\t"
            << DiffTmSec(startTm, stopTm) << " sec.\n";
        bsPall += DiffTmSec(startTm, stopTm);
        for (int j = 0; j < BSORTNUM - 1; ++j) {
            if (BS配列[j] + 1 != BS配列[j+1]) {
                std::cout << "wrong:" << j << "\n";
            }
        }
    }
    /**/
    double fbSeri = 0.0;
    for (int i = 0; i < CHECKNUM; ++i) {
        _ftime_s(&startTm);
        FizzBuzz(FIZZBUZZMAX);
        _ftime_s(&stopTm);
        std::cout << "FizzBuzz シリアル\t"
            << DiffTmSec(startTm, stopTm) << " sec.\n";
        fbSeri += DiffTmSec(startTm, stopTm);
    }
    double fbPall = 0.0;
    for (int i = 0; i < CHECKNUM; ++i) {
        _ftime_s(&startTm);
        FizzBuzzParallel(FIZZBUZZMAX);
        _ftime_s(&stopTm);
        std::cout << "FizzBuzz パラレル\t"
            << DiffTmSec(startTm, stopTm) << " sec.\n";
        fbPall += DiffTmSec(startTm, stopTm);
    }
    /**/
    int cnt;
    double pnSeri = 0.0;
    for (int i = 0; i < CHECKNUM; ++i) {
        _ftime_s(&startTm);
        cnt = GetPrimeNumbers(PRIMENUMBERS);
        _ftime_s(&stopTm);
        std::cout << "素数 シリアル " << cnt << "個\t"
            << DiffTmSec(startTm, stopTm) << " sec.\n";
        pnSeri += DiffTmSec(startTm, stopTm);
    }
    double pnPall = 0.0;
    for (int i = 0; i < CHECKNUM; ++i) {
        _ftime_s(&startTm);
        cnt = GetPrimeNumbersParallel(PRIMENUMBERS);
        _ftime_s(&stopTm);
        std::cout << "素数 パラレル " << cnt << "個\t"
            << DiffTmSec(startTm, stopTm) << " sec.\n";
        pnPall += DiffTmSec(startTm, stopTm);
    }
    std::cout << "\n";
    std::cout << "BubbleSort シリアル平均 "
        << (bsSeri / (float)CHECKNUM) << " sec.\n";
    std::cout << "BubbleSort パラレル平均 "
        << (bsPall / (float)CHECKNUM) << " sec.\n";
    std::cout << "FizzBuzz シリアル平均 "
        << (fbSeri / (float)CHECKNUM) << " sec.\n";
    std::cout << "FizzBuzz パラレル平均 "
        << (fbPall / (float)CHECKNUM) << " sec.\n";
    std::cout << "素数 シリアル平均 "
        << (pnSeri / (float)CHECKNUM) << " sec.\n";
    std::cout << "素数 パラレル平均 "
        << (pnPall / (float)CHECKNUM) << " sec.\n";
    std::cout << "\n";
    std::cout << "BubbleSort 処理効率 " << (bsSeri / bsPall) << "\n";
    std::cout << "FizzBuzz 処理効率 " << (fbSeri / fbPall) << "\n";
    std::cout << "素数 処理効率 " << (pnSeri / pnPall) << "\n";
    /**/
    return 0;
}
実行結果(一例)
素数の数を求める範囲    500000
FizzBuzz を求める範囲   30000000
Bubble Sort の要素数    30000
繰り返し回数    5
BubbleSort シリアル     1.326 sec.
BubbleSort シリアル     1.201 sec.
BubbleSort シリアル     1.497 sec.
BubbleSort シリアル     1.28 sec.
BubbleSort シリアル     1.216 sec.
BubbleSort パラレル     31.934 sec.
BubbleSort パラレル     33.521 sec.
BubbleSort パラレル     34.381 sec.
BubbleSort パラレル     33.757 sec.
BubbleSort パラレル     33.679 sec.
FizzBuzz シリアル       5.556 sec.
FizzBuzz シリアル       5.291 sec.
FizzBuzz シリアル       5.322 sec.
FizzBuzz シリアル       5.337 sec.
FizzBuzz シリアル       5.291 sec.
FizzBuzz パラレル       3.106 sec.
FizzBuzz パラレル       3.09 sec.
FizzBuzz パラレル       3.106 sec.
FizzBuzz パラレル       3.09 sec.
FizzBuzz パラレル       3.121 sec.
素数 シリアル 41538個   52.922 sec.
素数 シリアル 41538個   52.735 sec.
素数 シリアル 41538個   52.804 sec.
素数 シリアル 41538個   52.737 sec.
素数 シリアル 41538個   52.939 sec.
素数 パラレル 41538個   39.115 sec.
素数 パラレル 41538個   40.582 sec.
素数 パラレル 41538個   40.519 sec.
素数 パラレル 41538個   39.583 sec.
素数 パラレル 41538個   40.921 sec.
BubbleSort シリアル平均 1.304 sec.
BubbleSort パラレル平均 33.4544 sec.
FizzBuzz シリアル平均 5.3594 sec.
FizzBuzz パラレル平均 3.1026 sec.
素数 シリアル平均 52.8274 sec.
素数 パラレル平均 40.144 sec.
BubbleSort 処理効率 0.0389784
FizzBuzz 処理効率 1.72739
素数 処理効率 1.31595

検証

 さあ、結果を検証しましょう。それぞれのアルゴリズムの主要な要素数を変更して、実行しました。結果の一例を、表形式で表します。

Bubble Sortの計測結果
要素数 10000 20000 30000
  シリアル パラレル シリアル パラレル シリアル パラレル
1回目 0.4060 6.1150 1.6380 24.0090 3.6510 54.057
2回目 0.4210 6.2410 1.6530 25.3200 3.8530 56.287
3回目 0.4060 6.2560 1.7010 25.3660 3.9000 56.474
4回目 0.4050 6.2250 1.6380 25.1940 3.6820 56.443
5回目 0.4210 6.3180 1.6690 25.3040 3.8070 56.459
平均 0.4118 6.2310 1.6598 25.0386 3.7786 55.944
処理効率 0.06608891 0.066289649 0.067542543
FizzBuzzの計測結果
要素数 10000000 20000000 30000000
  シリアル パラレル シリアル パラレル シリアル パラレル
1回目 3.4790 1.9350 7.7220 3.8070 11.384 5.9430
2回目 3.4330 1.8870 7.9250 3.8370 10.920 5.8820
3回目 3.4320 1.8570 7.3910 3.8380 10.921 5.8190
4回目 3.4320 1.9190 7.3950 3.8220 10.874 5.8190
5回目 3.4480 1.9030 7.5290 3.8220 10.921 5.8040
平均 3.4448 1.9002 7.5924 3.8252 11.004 5.8534
処理効率 1.812861804 1.984837394 1.87993303
素数(の数)を求めるの計算結果
要素数 100000 300000 500000
  シリアル パラレル シリアル パラレル シリアル パラレル
1回目 2.6990 2.3400 22.7450 15.5390 52.4810 38.5330
2回目 2.6990 2.3240 21.2480 15.4930 52.4970 38.7670
3回目 2.7140 2.3250 20.9510 15.4150 52.6060 39.0010
4回目 2.7150 2.3870 21.0290 15.4920 52.4500 38.9240
5回目 2.6980 2.4960 20.9980 15.8210 52.5430 38.8140
平均 2.7050 2.3744 21.3942 15.5520 52.5154 38.8078
処理効率 1.139235175 1.375655864 1.353217652

 バブルソートについては、並列化をする方が遅い結果となりました。これは、並列化して実行する処理のほとんどを順次実行としたため、順番を守るために待ち合わせが発生しているためと考えられます。なお、この例ではループを単純に並列化しようとしたため、この様な結果になっています。パイプライン並列化する等、並列化の方法を変更すると、並列化の効率が良くなります。

 素数(の数)を求めると、FizzBuzzで、並列化による効果が異なります。FizzBuzzでは1.8倍の効果が出ていますが、素数(の数)を求めるでは、1.3倍にとどまっています。並列化で実行されるループの中の処理が、ループの後ろほど数が多いことが原因です。

 素数(の数)を求める処理では、「2以下の素数の数」「3以下の素数の数」…「100以下の素数の数」と数が進むにつれて、だんだんと調べる数が多くなります。Open MPのforディレクティブでは、1つのスレッドが実行するループの回数を制御します。例えば、全体で千回のループであれば、(1000/論理CPU数)回のループを(論理CPU数)回繰り返すような動作をします。この(論理CPU数)回の繰り返しを、並列に処理するわけです。今、50万以下の素数を求めるわけですが、論理CPU数が2の環境では、「25万未満」と、「25万以上50万以下」の2つに分けて実行します。すると、「25万未満」を担当するスレッドは、「25万以上50万以下」を担当するスレッドよりも、処理の時間が圧倒的に短くなります。すなわち、いずれかのスレッドの負荷だけが上がり、負荷の平準化がされないために、効率が上がらないのです。これを、「100ずつ」に区切って5,000回のタスクに分けると、1つめのタスクと5,000個目のタスクでは処理にかかる時間が大きく異なりますが、1つめのタスクと2つめのタスクでは、わずかな差しかありません。2つのスレッドに負荷を平準化して掛けることにより、処理効率を上げることができます。今回のコードでは、こういう処理をしていないため、1.3倍の効率しか出ていません。schedule句によって、「ループの回数を小さくする(schedule(static))」か、「ループの後ろの方ほどループ回数を小さくする(schedule(guided))」ことで、実行効率を上げることが出来ます。

まとめ

 この結果から、何でもかんでも並列化すれば、等しく速くなるわけではない、ということが分かります。並列化に向いているアルゴリズム、向いていないアルゴリズムがあります。また、並列化にむいているアルゴリズムであっても、十分に検討しなければ思わぬバグを作り込んだり、思ったほど効率が上がらないことになります。事前にしっかりと調査し、コードの設計をすることが必要です。

並列化する前に

  1. パフォーマンスを計測し、高速化しなければならないか、検討する
    • 10ミリ秒で終わる処理を5ミリ秒にしたところで、どれほどの意味があるでしょうか。1分かかるところを40秒で処理できるようにするのは、意味があるでしょう。すなわち、「何倍」という倍率よりも、体感できる時間を短くする方が大事です。
  2. より効率化された書き方がないか、調査する
    • 例示した「素数か判定する」ルーチンは、判定に使う数は判定したい数の平方根を超えない整数まででかまいません(IsPrimeNumberFast関数を参照)。並列化ではたかだか論理CPU数倍にしかなりませんが、IsPrimeNumberFast関数を使うと、実行効率は100倍以上になります。
    • ここでは、「並列化に向かないアルゴリズム」の説明として、複数のスレッドが同時に同じ領域を変更する例として、バブルソートを選びました。ソートをするなら、クイックソートやマージソートなど、より効率的なアルゴリズムがあります。
  3. 処理を、より単純化する
  4. 並列化できる箇所を明確にする
    • 本文では、いくつかの並列化できない箇所を、予め廃止するか並列化しない箇所へ退避させています。コンソール出力をやめる、メモリの確保を前もって行うなどです。
  5. 並列に動作すると不具合を引き起こす箇所がないか、検証する
    • 処理順序が一定であることに依存しないか、検証します。FizzBuzz問題を、ループ内でコンソールに出力すると、処理順序が一定ではないことが問題になります。そのため、決められた位置へ格納するというアルゴリズムに変更しています。
    • 複数のスレッドで、同じ領域を書き換えることがないか、検証します(「生産者と消費者問題」で検索して下さい)。
  6. 不具合を起こす箇所を一か所にまとめ、クリティカルブロックとする
    • デッドロックしないように、慎重に設計します(「哲学者の食事問題」で検索して下さい)。
  7. クリティカルブロックの処理時間と、並列化している箇所の処理時間を比較する
  8. クリティカルブロックの処理時間が長い場合、並列化をあきらめる

謝辞

 本記事は、一旦ブログにて公開し、コミュニティの皆さまよりご意見をいただいて、より読みやすいように修正しました。ご協力くださった方々に、この場を借りてお礼申し上げます。

投稿日時 : 2010年2月5日 23:42
コメント
  • # re: 処理並列は、処理速度向上をもたらすのか?
    Jitta
    Posted @ 2010/03/02 20:59
    アトミック、間違ってるなぁ。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/26 21:47
    なぜ素数の計算では本質ではないところにCriticalばっかり使って、「どうして遅くなるのか分かりにくい」ようなやり方にしてるんでしょうか?
    これだとアルゴリズムの問題なのかクリティカルな部分が問題なのかわからないと思います。
    クリティカルな処理が素数を求めるアルゴリズムの本質的な部分であるならともかく、単に結果を取り出すための部分に過ぎないですよね?
    この内容なら普通はリダクションでクリティカル部分はなくせるでしょうし、
    アルゴリズムでいうならスケジュールをデフォルトのstaticではないものにする、
    もしくはチャンクサイズを指定するだけで結果は大幅に変わるでしょう。

    多分ソースコードを少し変更すればという部分がこういう話だとは思いますが、何とも恣意的な検証内容に思えます。
    OpenMpをうまく使ってないから遅いという結果をもって、なんでも早くなるわけではないという証明にするのは、ちょっと違和感があります。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/26 21:53
    >求めた素数は配列へ格納します。配列を動的に拡張すると、メモリを確保するという並列化できない要素が出てくるため、呼び出し側で確保しておくこととします。

    これも違和感があります。こうしているにもかかわらず結局並列化できなくなっていますよね?

    >10ミリ秒で終わる処理を高速化したところで、どれほどの意味があるでしょうか。1分かかるところを20秒で処理できるようにするのは、意味があるでしょう。

    これも言い切ってしまうのには違和感があります。
    どの程度の時間なら意味があるかは状況によりますから、一概に絶対的な時間を持って意味がある、ないと結論するのはおかしいです。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/26 22:04
    もう一つ、これはまあどうでもいいことではありますが、

    >昨今あちらこちらで声高に繰り返される、「並列処理化すれば速くなる」に疑問を持ったため、それを検証することにします。

    そんなになんでも早くなるみたいに言われてますかね?
    むしろ、なんでも早くなるわけではないからということを書いてる記事とかをよく見る気がするんですが。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    Jitta
    Posted @ 2010/06/27 19:25
    コメントありがとうございます。

    > なぜ素数の計算では本質ではないところにCriticalばっかり使って、「どうして遅くなるのか分かりにくい」ようなやり方にしてるんでしょうか?

     素数の計算には、Critical は使用していません。Bubble Sort には、使用しています。素数の計算には、「書き換える」という要素がありません。従って、クリティカル セクションを用いる必要がありません。本文中でも、そのように書いていたと思うのですが。
     素数の計算で、クリティカル エリアをもうけている箇所を指摘していただけますか。使用しているなら、使用していることを意識していません。それは、大きな間違いです。よろしくお願いします。


    > >求めた素数は配列へ格納します。配列を動的に拡張すると、メモリを確保するという並列化できない要素が出てくるため、呼び出し側で確保しておくこととします。
    >
    > これも違和感があります。こうしているにもかかわらず結局並列化できなくなっていますよね?

     ここも、ご指摘の内容がよくわかりません。並列化するために、配列を作成しています。ですから、私は「並列化できている」と思っています。なぜ、並列化できていないのでしょうか。

     素数を求める計算で、パラレル…というより、ココは「二重化」としておく方がいいかもしれません。素数を求める計算で、二重化したものが、シングル スレッドに比べて効率が上がらないのは、OpenMP のスレッド スケジュールに関係があります。ここのコードのように、何も追加指定していない場合、50万回のループを、1~25万(A)と、25万1~50万(B)の二つに分けます。素数を求める関数は、1~求めた行数までループします。これによって、(A)は「1~25万の和回」ループするのに対して、(B)は「25万1~50万の和回」ループします。この、素数を求めるためのループ回数の差が、効率が上がっていない原因になっています。
     これを、schedule を指定して、1タスクのループ回数を1000程度で区切れば、1.6~1.7倍にまで効率化されます。
     もう一つ、今は素数を求める数のループをマルチ スレッド化していますが、素数を求めるループ(GetPrimeNumbers 内のループ)をマルチ スレッド化する方法もあります。こちらについては、執筆当時は思いついていませんでした。もしかして、先の「なぜ素数の計算では本質ではないところにCriticalばっかり使って」とは、このことを指していますか?


    > そんなになんでも早くなるみたいに言われてますかね?

     諸般の事情で直接「これ」と申し上げる訳にはいかないのですが、そういう記事を目にしたから書いた記事です。


    > どの程度の時間なら意味があるかは状況によりますから、一概に絶対的な時間を持って意味がある、ないと結論するのはおかしいです。

     はい、おっしゃるとおりです。
     ここの意図は、「誤差の範囲とも言えるような時間を高速化したところで、どのような意味があるか。しかし、体感できる早さなら、意味があるだろう」という意図でした。なお、ここは、実際のと工事には、このように訂正しています。
    「■10ミリ秒で終わる処理を5ミリ秒にしたところで、どれほどの意味があるでしょうか。1分かかるところを40秒で処理できるようにするのは、意味があるでしょう。すなわち、「何倍」という倍率よりも、体感できる時間を短くする方が大事です。」
    http://codezine.jp/article/detail/4914?p=5
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/27 20:08
    >素数の計算には、Critical は使用していません。

    なんかわざとはぐらかされているように感じてしまうのですが。
    計測範囲内の、並列処理ブロック内でクリティカル部分を作っているのですから、その部分について言っていることぐらいわかると思います。
    私が、一回の素数計算の内部でクリティカルを使っているなどとわざわざ限定して書いたなら別ですが、
    ----
    なぜ素数の計算では本質ではないところにCriticalばっかり使って、
    ----
    ----
    クリティカルな処理が素数を求めるアルゴリズムの本質的な部分であるならともかく、単に結果を取り出すための部分に過ぎないですよね?
    ----
    本質じゃない部分、結果を取り出す部分とわざわざ書いてるんですから、普通は意味を読み取れるでしょう。

    それとも、処理時間計測範囲内の、並列処理ブロック内で、クリティカルを使っているにもかかわらず、一つの計算内では使っていないから結果に影響はないとでもいうのでしょうか?

    >素数の計算には、「書き換える」という要素がありません。従って、クリティカル セクションを用いる必要がありません。本文中でも、そのように書いていたと思うのですが。

    一つの計算処理内ではクリティカルは必要ないアルゴリズムなのに、処理時間計測範囲内の並列処理ブロック内でクリティカルを使えば、結果がアルゴリズムのせいなのかクリティカル部分の影響もあるのかわかりにくくなるでしょう。
    そういうことを指摘しています。

    実際には数が大きくなればほとんど影響はなくなるであろうことは想像はできますが、検証しているのですから、最初からそういう影響は排除するのが普通ではないですか?


    もっと言えば、現在素数の計算のところで並列化しているのは、「素数の計算」じゃありません。
    並列化されているのは複数の素数の計算のループです。
    素数計算アルゴリズムは並列化されていません。

    「素数計算」の繰り返し部分を並列化しており、その並列化範囲内でクリティカルを使っているのですから、並列化されていない部分が含まれているというのは明らかでしょう。
    そしてそれは全体のアルゴリズムの本質でさえなく、ただ配列等に結果を取り出す処理に過ぎないでしょう。

    なぜ、本質ではない部分にクリティカルなどを使って、検証結果を分かりにくくしているのですか?


    >並列化するために、配列を作成しています。ですから、私は「並列化できている」と思っています。なぜ、並列化できていないのでしょうか。

    配列へのアクセスが並列化されていると本気でおっしゃっていますか?
    もちろん、確保処理は削除はされていますが、配列の使用はどう見ても並列化されていないでしょう。


    私は、実際のパフォーマンスにどの程度影響があるかという点はともかくとして、書いていることが正確ではない、処理速度の検証という意味で適切ではない、という観点で指摘しています。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/27 20:18
    配列のアクセスは、実際の計測ではNULLだからそもそもアクセスすらないということですね?
    意味はわかりました。
    でも事前に確保してNULLじゃなくせば並列化されていないですよね?

    >求めた素数は配列へ格納します。配列を動的に拡張すると、メモリを確保するという並列化できない要素が出てくるため、呼び出し側で確保しておくこととします。

    どう読んでも、事前に確保しておくから、並列化できているといっているようにしか読めません。
    事前に確保した場合、アクセス部分は並列化できていないのですから、記述が不自然でしょう。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/27 20:26
    あくまで、配列の確保という処理だけに関して言っているというなら、言っていること自体は正しいですが、記事から意図が非常に読み取りにくいです。

    どうせ配列アクセスが並列化できないなら、中で確保しても大して違いはないでしょう。
    今回はNULLなら無視という条件だから違うだけであって。

    結局のところ、記事のこの部分で何を言いたいのか、どういう意味があるのかとても分かりにくい、ということです。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/27 20:44
    それから、schedule の話です。
    この記事は、並列化したからってなんでも早くなるわけではないことを検証する、ということなのですから、
    OpenMPの使い方が適切でないため(十分に)早くならないというのは、検証として意味があるのか、というのが疑問なのです。

    そういう話があってもいいとは思いますが、であればきちんとscheduleを適切に設定して、適切に並列化した場合の結果も載せるべきと思います。

    使い方が適切でないものをもってなんでも早くなるわけじゃないなどと言ってしまうと、例えば配列の合計計算でさえ、並列化しても意味がないなどと言えてしまいます。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/27 21:02
    >「■10ミリ秒で終わる処理を5ミリ秒にしたところで、どれほどの意味があるでしょうか。1分かかるところを40秒で処理できるようにするのは、意味があるでしょう。すなわち、「何倍」という倍率よりも、体感できる時間を短くする方が大事です。」

    あまり意図が通じていない気がします。
    5ミリ秒が誤差といえる些細な違いかどうかは、状況によるということです。
    今どきのコンピューティング環境で5ミリ秒の計算処理は膨大な時間とも言えます。
    この処理が膨大な回数実行される処理なら、十分に意味がある可能性はありますし、
    1時間かかるバッチ処理の一部の処理を20秒短縮してもあまり意味はないでしょう。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/28 20:30
    もう一つ忘れてましたが、バブルソートの並列化にバグがあるような気がします。
    forにorderedがついてますが、これは何をしてるつもりでしょう?
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/06/29 21:37
    さらにどうでもいい(というか本題とは関係ない)ですけど、シャッフルの仕方がちょっと気持ち悪いです。
    もうちょっと普通のやりかたした方がいいような。

    今回困るわけではないですが、この方法ではうまくシャッフルされない場合がありますし、乱数の使い方もよくない使い方、ですよね。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/02 10:14
    最初のバブルソートの51以降の結果がおかしいのは、

    >これは、「対象配列[y]」と「対象配列[y-1]」の比較入れ替えで、複数のスレッドが、y の値が同じところを操作しようとしたためです。

    だそうですが、本当にそうですか?
    違いますよね、そもそも外側のループが順番通りじゃないのがメインの理由でしょう。
    入れ替え操作が競合してる可能性はもちろんありますが、
    こっちの影響なら51以降とかみたいな特徴にはならないでしょう。
    考察がちょっといい加減な気がします。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/02 10:51
    OpenMPのメモリモデルでは、きちんと同期されてなければ結果はめちゃくちゃになってもおかしくはないとは思いますが、
    その場合はその場合で、yの値が同じ所を更新したから、という理屈では通らなくなるでしょう。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/02 13:07
    ちょっと慎重さが足りなかったので、2つ前の書き込みの、
    外側のループが順番通りじゃないのがメインの理由と言い切ったのは取り消します。
    現時点ではなんとも言えない、分からない、ですね。
    きちんと考えたら想像は出来るかも知れませんが。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/02 22:36
    >> そんなになんでも早くなるみたいに言われてますかね?
    >
    > 諸般の事情で直接「これ」と申し上げる訳にはいかないのですが、そういう記事を目にしたから書いた記事です。

    記事なのにこれと言えない事情があるような物なのでしょうか…?
    あちらこちらで声高に繰り返されているんでしょう?

    正直言っていまいち納得いきません。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/03 9:38
    >もう一つ忘れてましたが、バブルソートの並列化にバグがあるような気がします。
    >forにorderedがついてますが、これは何をしてるつもりでしょう?

    バブルソートの計測部分にソート結果の検証を組み込んでみましたが、あっさりエラーになりました。
    正しくソートできていませんね。
    なぜか分かりますか?
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/03 9:41
    もう一つ、マルチスレッドの動作の検証において、目で見て確かめる程度の検証ではあまり意味がないことも分かりますか?
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/03 10:12
    ソートパフォーマンスを計測するなら、同じ状態にシャッフルした配列で計測するべきだと思いますが、なぜ毎回新たにシャッフルして状態を変えてしまっているのですか?
    計測したい部分以外の条件は同一にしないとって、貴方が言っていることですよね?
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/04 10:24
    おや?
    OpenMPは parallel for のループ変数そのものなど、一部の例外を除いては、変数は暗黙でスレッド間での共有変数になるようですね。
    だとすると、必要なprivate指定もごっそり抜けてませんか?
    2重ループの内側のループ変数とか。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/04 14:12
    実は、デバッガ上で実行しないと、どうしてもメモリアクセス違反で落ちてしまって
    まともに動かせなかったので何でだろうと思ってたんですが、
    ひょっとしてこれが原因だったんですかね。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/04 19:53
    申し訳ない、変数の共有に関してはやっぱりちょっと違うようですね…確認が取れてないので保留です。
    あと、メモリアクセス違反が出るのはVisual StudioでReleaseビルドの時ですね。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/04 20:59
    >シングル スレッドの方は、スレッド数が“1”、CPU 使用率は“(100/CPU数)%”である事を確認します。また、マルチ スレッドの方は、スレッド数が“CPU数”、CPU 使用率が“100%”であることを確認します。

    これもいい方法とは思えませんね。
    >CPU 使用率が“100%”であること
    素数計算の部分なんて、スレッド間の負荷が均一でないと自分で書いておきながら、100%であることを確認するってのはどうでしょう。
    そりゃ最初の方は100%になる可能性は高いでしょうが、そもそも方法としてこんなのは何の確認にもなってないのは明らかでしょう。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/04 21:11
    >?複数のスレッドで、同じ領域を書き換えることがないか、検証します。(「生産者と消費者」問題)

    こういうのは普通「生産者と消費者」問題とは言わんでしょう。
    ついでに書き換えだけが問題のように読めてしまいます。
    書き込みは単一スレッドであっても同時に読み込みがあれば競合です。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    Jitta
    Posted @ 2010/07/16 22:01
    > 計測範囲内の、並列処理ブロック内でクリティカル部分を作っているのですから、その部分について言っていることぐらいわかると思います。

     失礼しました。GetPrimeNumbers のほうですね。
     count に atomic を指定しているのは、reduction を知らなかったからです。配列の方は、このコードでは通らないようにしています。
     時間的には、atomic をとっても変わりません。それは、他のところで指摘されているように、schedule で調整すべきだからです。そのことは、本文で少し触れています。触れただけなのは、dynamic と static の違いがわからなかったからです。
     それもそうなのですが、私としては、「2.より効率化された書き方がないか、調査する。」のほうが有効と考えています。ここをどうがんばっても、論理 CPU 数倍にしかなりません。IsPrimeNumberFast では、190倍になります。そのことも、本文では触れています。触れるだけなのは、コードを変えて試してみればわかるからです。
     配列は、確かに、おっしゃるとおりですね。これを書いたときには、これしか思いつかなかったのです。もうちょっと落ち着けば、他にも方法があるのに。

     バブルソートの ordered は、色々試していた取り忘れです。ちょっと確認ができていないので尋ねますが、ordered がついていることがバグなのでしょうか。バブルソートのコード自体にバグがあるのでしょうか。
     ここでバブルソートを使っているのは、複数のスレッドから同じ領域を書き換える例を他に思いつかなかったからです。他のエントリに書いていますが、明示的にマルチスレッドを使ったことは、一度しかありません。OpenMP については、急ごしらえでリファレンスを読んだ程度です。

     乱数については、rand 関数が生成する乱数が、乱数としてよいものではないと言うことでしょうか。それとも、剰余計算をしていることでしょうか。RAND_MAX と除数の倍数の差がある分、偏りとなるのはわかっています。これまでの業務では乱数を使ったことがなく、PC-6001 では1以下の実数が出てきていたので、使いにくく思っています。
     シャッフルについて、「普通」とは、何でしょう?この場合、同じ数が何度出ようと関係ないので、単純に放り込んでいってもよかったのですが、「カードゲームで同じカードが複数回でないように」という質問がコミュニティで何度かあがっていますので、そういうときに「入れ替えしている奴がいたな」程度に見てもらえれば、と思っています。まぁ、考え方が甘くて、奇数の時に移動しないところがあるのは、コードに記しているとおりです(ご指摘の内容と同じだと思います)。個人的には、シードに0を入れるべきだったと思っています(同じ結果が返るので、検証しやすい)。

     私が、他の人に教えることができるなどとは思っておらず、こういうと責任転嫁のようにとられるかもしれませんが、先にここに書いておかしいところの指摘をしていただこうという意図があります。なお、他のエントリの「動いているからいいか」も、同じ意図です。まんまと釣られてくださって…あ、いや、指摘だけなので、半分ですね。


     申し訳ないですが、細かく分割するのではなく、1つにまとめていただくと、うれしいです。たいていの場合、私は一日に1度、22時前後にしかメールを見ません(ここのお知らせが来るアドレスは、ウェブメールを持っていないのと、職場ではウェブメールにアクセスできないので)。書き込みができる PC の前に座るのも、それくらいです。それも、子どもの寝る時間、妻の帰宅時間によって変わります。今日は「ハウルの動く城」を見ているので、時間を作れています。トップページだと、通勤時に携帯から見ることができますが。そういうわけで、返答が遅いのはご容赦ください。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/17 0:53
    > count に atomic を指定しているのは、reduction を知らなかったからです。配列の方は、このコードでは通らないようにしています。
    > 時間的には、atomic をとっても変わりません。それは、他のところで指摘されているように、schedule で調整すべきだからです。そのことは、本文で少し触れています。触れただけなのは、dynamic と static の違いがわからなかったからです。

    私は、
    >実際のパフォーマンスにどの程度影響があるかという点はともかくとして、書いていることが正確ではない、処理速度の検証という意味で適切ではない、という観点で指摘しています。
    と書いたように、目的に対して適切か不適切かという観点で指摘しています。

    そりゃ、特に数が大きくなればほぼ影響がないであろうことは推測できますよ。
    でもね、実際に結果がどうなるかということに関して、特にマルチスレッドでは、推測はとても危険なのですよ。

    本来必要ではないatomicを残しておく理由がありません、あるとすればそれは手抜きです。
    また、この記事はマルチスレッドに精通した読者を前提としているわけではないでしょう?
    atomicを使っているコードを見たとき、パフォーマンスが出ない理由がatomicではなくスケジュールの問題であると確証を得られますか?
    実際に試してみた結果、やっぱり影響はなかったから問題ないじゃん、などというのは意味がありません。
    順番が逆です。実際に試すまでは確証などないのです。

    > それもそうなのですが、私としては、「2.より効率化された書き方がないか、調査する。」のほうが有効と考えています。ここをどうがんばっても、論理 CPU 数倍にしかなりません。IsPrimeNumberFast では、190倍になります。そのことも、本文では触れています。触れるだけなのは、コードを変えて試してみればわかるからです。

    そんなことはわかっています。
    で、190倍になれば並列化は不要ですか?
    もっと大きな範囲の数値ならどうなりますか?

    ここでは本格的に触れるつもりはなかったということで分からないではないですが、この記事は元々、並列化で必ずしも同等に高速化できるわけではない、ということを検証するというものなのでしょう?
    OpenMPのオプションを変えるだけで同等程度に高速化できるものに対して、これで検証になっているというのですか?
    これで確認できる事柄は「Jittaさんは有効な並列化ができなかった」ということだけです。

    まあそもそもでいうと、この記事の目的そのものに無理があるんですけどね。
    同等に高速化できないことを「検証」するなんて、ちょっと無理があるでしょう。
    検証する、などという書き方でなければまだいいのですが。
    記事が無意味というつもりはありません。書き方に少し問題があるとは感じます。


    > バブルソートの ordered は、色々試していた取り忘れです。ちょっと確認ができていないので尋ねますが、ordered がついていることがバグなのでしょうか。バブルソートのコード自体にバグがあるのでしょうか。
    > ここでバブルソートを使っているのは、複数のスレッドから同じ領域を書き換える例を他に思いつかなかったからです。他のエントリに書いていますが、明示的にマルチスレッドを使ったことは、一度しかありません。OpenMP については、急ごしらえでリファレンスを読んだ程度です。

    OpenMPをよく知らないこと自体を責めるつもりはない(そもそも私なんて記事を最初に読んだ時点ではほとんど知らなかった)のですが、
    知らなくても、いや知らないならなおさら、コードが予想通りに動いていることを慎重に確認する必要があるでしょう。
    OpenMPをよく知らないことを責めてるんではなくて、よく知らないものを、十分に確認もせずに適当なコードを書いていることを責めているんですよ。

    で、ordered はつまり意図的ではなく、ないつもりだったということですね?
    そうすると、今度はなんでそれでソートがうまくいくと思っているのかが理解できません。
    orderdがあろうとなかろうとバグがあります。

    本当にバグの理由が分からないのですか?
    まあ、最初の(Criticalがない)並列化コードで、ソートがうまく動作しない理由を、入れ替えが同時に起こっているからだけだと思っているようですので、推して知るべしですかね。
    並列化によってforループがどのように実行されるのかもう一度よく考えればわかると思いますよ。
    まあ、別の方のブログのコメントですが、細かい話も書きましたし、そこのブログの記事にもなってるので探せば見つかるでしょう。

    > 乱数については、rand 関数が生成する乱数が、乱数としてよいものではないと言うことでしょうか。それとも、剰余計算をしていることでしょうか。RAND_MAX と除数の倍数の差がある分、偏りとなるのはわかっています。これまでの業務では乱数を使ったことがなく、PC-6001 では1以下の実数が出てきていたので、使いにくく思っています。

    乱数の使い方がよくないという指摘はまあおまけです。剰余計算について言っています。


    > シャッフルについて、「普通」とは、何でしょう?この場合、同じ数が何度出ようと関係ないので、単純に放り込んでいってもよかったのですが、「カードゲームで同じカードが複数回でないように」という質問がコミュニティで何度かあがっていますので、そういうときに「入れ替えしている奴がいたな」程度に見てもらえれば、と思っています。まぁ、考え方が甘くて、奇数の時に移動しないところがあるのは、コードに記しているとおりです(ご指摘の内容と同じだと思います)。個人的には、シードに0を入れるべきだったと思っています(同じ結果が返るので、検証しやすい)。

    そもそもシャッフルって、定石的で、かつうまくシャッフルできるアルゴリズムがありますよね?
    しかも、この記事の方法よりずっとシンプルですよね?
    わざわざ変なやり方する意味がありません。

    また、この記事のシャッフルでは、シャッフルのたびに入れ替え回数が最悪倍程度変わる可能性がある(シャッフル毎のばらつき度合いに差が出やすい)、
    そもそもばらつきに思い切り偏りがある、入れ替わらない要素が出やすいなど、わざわざ複雑なやり方をしているのに、いい部分が全くありません。
    どれくらい出来が悪いかは、記事のシャッフル後のデータをみれば一目瞭然でしょう。

    また、もっとまんべんなくシャッフルしていれば、ひょっとしたら少ない数のソートでもバグに気づいたかもしれませんよ。
    つまり、おかしなシャッフルの仕方のせいで、異常を見つけられる可能性を下げてしまっているという害もあるかもしれません。

    > 私が、他の人に教えることができるなどとは思っておらず、こういうと責任転嫁のようにとられるかもしれませんが、先にここに書いておかしいところの指摘をしていただこうという意図があります。なお、他のエントリの「動いているからいいか」も、同じ意図です。まんまと釣られてくださって…あ、いや、指摘だけなので、半分ですね。

    釣りなのですか?
    自分の記事では間違いがあっても指摘を待つという姿勢で良しとしているということですか?
    つまり、Jittaさんの記事は疑ってみるべき、そもそも確証のないことでもなんでもとりあえず書いてみてるスタンスだってことですか?

    この記事はCodeZineに載ってるものでもほとんど同じ問題があるように思われますが、全然うまくいってないということでしょう。

    あなたの普段の言動から見て、ちょっとこの姿勢は理解できませんね。

    > 申し訳ないですが、細かく分割するのではなく、1つにまとめていただくと、うれしいです。

    コメントが増えてしまったのはあまり良いとは思ってませんが、ただ、気づいたことを指摘するのに、他にもないかと探して見つけてから書くのか?とか考えれば、ちょっと無理なことを言っているとも思いませんか?
    まあ、なるべく意識はするようにします。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/23 1:29
    >count に atomic を指定しているのは、reduction を知らなかったからです。配列の方は、このコードでは通らないようにしています。
    >時間的には、atomic をとっても変わりません。

    そもそも私がこの辺りを指摘したのは、記事内で、

    >FizzBuzz では1.8倍の効果が出ていますが、素数(の数)を求めるでは、1.3倍にとどまっています。
    >これは、素数の数をカウントするためにクリティカル エリア(アトミックな計算)があることと、並列化で実行されるループの中の処理が、ループの後ろほど数が多いことが原因です。

    と書かれていたからです。あなた自身が、atomicも遅い原因だと言っています。
    だから私は、なぜアルゴリズムの本質と無関係なatomicを残して、遅い原因を分かりにくくしてしまっているのか?ということを聞いたのです。

    reductionを知らずatomicをなくせなかったのなら、結果をcountにカウントすることをあきらめるべきであって、atomicを残すべきではありません。
    結果のカウントをなくしてもアルゴリズムの本質に影響はありませんが、atomicを消すことをあきらめたら、計測結果に影響がある可能性を残すことになります。

    正直言って、あなたのこの辺の感覚はずれているとしか思えません。
    大事なところを深く考えずにいい加減に澄ましているように感じるのです。

    例えばシャッフルで入れ替え回数までランダムに決めている部分も同様です。深く考えずになんとなくやってるとしか思えません。
    深く考えたならそんなことはしないはずだからです。デメリットしかありませんから。

    検証の考え方が大事だとかそういうことを言いながら、根本的にあなたは考えがいい加減じゃありませんか?

    あと、おかしすぎて前の書き込み時は読み違えてしまっていましたが、この記事で言ってる検証とは、
    「並列化すれば速くなる」という仮定を検証する、ということなんですね…
    つまり記事の結論としては、検証は失敗であったということになるのですよね?
    検証に失敗したということは、仮説が証明できなかったというだけのことで、その逆を証明したことにはならないはずですが、この記事としてはそれでいいのですか?

    確かどこかで、「検証の設計」というものを分かってほしくて、そこに重点を置いてこの記事を書いた、というようなことを言ってたと思いますが、
    この記事で本当に、検証の設計?というものをきちんと説明できているのでしょうか?
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/07/30 22:07
    修正途中なのでしょうからまた変わるのだろうとは思いますが、修正された部分で気になる点がありますのでコメントします。


    > for (int 決定済み数 = 0; 決定済み数 < 配列数; ++決定済み数) {
    > #pragma omp parallel for
    > for (int y = 配列数 - 1; y > 決定済み数; --y) {

    バブルソートの並列化で、内側のループを並列化するようにしたのは何か理由があるんでしょうか?
    しかも#pragma omp parallel forという形で丸ごとループ内に入っているため、配列要素数分スレッドの生成からやり直す感じになりませんか?
    せめて#pragma omp parallelはループの外側に出して、#pragma omp forだけをループ内で指定すべきではないですか?
    もちろん普通は内側のループで並列化なんてことをまずしないと思いますが。

    このままだと、わざわざ非常に効率悪いやり方にしているよう見えてしまいます。


    >実は、バブルソートのアルゴリズムは、並列化に向いていません。複数の並列に行われる処理が、同じ領域を書き換えようとするためです。

    >複数のスレッドが同時に同じ領域を変更する例として、バブルソートを選びました。

    と書かれてますが、修正された版のコードでは並列化は内側のループのため、同じ領域の書き換えはまず滅多に発生しません。


    >バブルソートについては、並列化をする方が遅い結果となりました。これは、並列化して実行する処理のほとんどを順次実行としたため、順番を守るために待ち合わせが発生しているためと考えられます。

    この理由だけだと、並列化の効果がなくなって逐次実行と同程度の速度になると予想されるはずですよね?
    ※もちろんなぜ極端に遅いかはわかってますよ。

    理屈に合わないレベルのパフォーマンス低下なのですから、少しはその理由などについて言及すべきと思います。

    ※そもそも外側のループで並列化していれば、ほぼ理屈通りになるはずですけどね。


    >・複数のスレッドで、同じ領域を書き換えることがないか、検証します(「生産者と消費者問題」で検索して下さい)。

    私の指摘の意味が通じていないのかもしれませんが、単なる競合のことを「生産者と消費者問題」などとは言いません。
    検索しても適切な解説には行き当たらないと思いますが。


    >5.並列に動作すると不具合を引き起こす箇所がないか、検証する
    > ・処理順序が一定であることに依存しないか、検証します。
    > :
    >6.不具合を起こす箇所を一か所にまとめ、クリティカルブロックとする

    これだと、処理順序が原因の問題もクリティカルブロックで解決するように読めてしまいます。
    クリティカルブロックで解決できるのは、普通は競合の問題だけです。


    >6.不具合を起こす箇所を一か所にまとめ、クリティカルブロックとする
    > ・デッドロックしないように、慎重に設計します(「哲学者の食事問題」で検索して下さい)。
    普通、クリティカルブロックを単に使うだけではデッドロックは発生しません。
    特にここでは、「不具合を起こす箇所を一か所にまとめ」という書き方なのですから、いきなりデッドロックに触れて「哲学者の食事問題」を出されても意味が通じにくいと思います。
  • # re: 処理並列は、処理速度向上をもたらすのか?
    なぜ?
    Posted @ 2010/08/06 2:25
    >せめて#pragma omp parallelはループの外側に出して、#pragma omp forだけをループ内で指定すべきではないですか?

    おーっと、これはOpenMPの動きを勘違いしてるような気がしますね、失礼。
    ただ、なぜ内側で並列化?という疑問は変わらないです。
タイトル
名前
Url
コメント