HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。

新着トピックス

 さて、並列処理を実装するにあたり、もっともプリミティブな方法がスレッドを利用した実装である。Windowsの場合、新しいスレッドを作成する方法にはいくつかあるが、Cでスレッドを作成する場合は_beginthreadex関数を使用するのが基本となる。

 _beginthreadex関数は「process.h」内で定義されており、そのプロトタイプは下記のようになっている。

uintptr_t _beginthreadex(
   void *security,
   unsigned stack_size,
   unsigned ( *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr
);

 詳細についてはライブラリリファレンス等を参照してほしいが、基本的にはstart_address引数に新たなスレッドで実行したい関数を、arglistに関数に引数として与える変数もしくは配列のポインタを与えて_beginthreadex()を呼び出せば良い。それ以外の引数については、特に問題がなければ0もしくはNULLで構わない。

 さて、今回のプログラムではループを使って各画素に対するメディアン処理を繰り返し行っている。このように繰り返し回数が多いループを含むプログラムの場合、このループを分割して並列処理させるのが定石である(図4)。

69c281f6591fea9f22eee488393e9e9c.png
図4 繰り返し処理を並列化する定石

 まず、並列化したい個所を別の関数として分離し、この関数を別スレッドで実行することで並列処理を行うわけだ。この関数を別スレッドで実行する場合、関数には1つの引数しか与えられないので、複数の引数を与える場合はあらかじめそれを構造体として宣言しておき、その構造体のポインタを引数として与える。

 今回のサンプルプログラムで並列実行したい個所は次の部分である。この部分は最大で4重のループとなっているが、このうちもっとも外側のループ(「for(y = 1; y height - 1; y++)」の部分)を分割することを考えよう。つまり、このループを「for(y = 1; y (height - 1)/2; y++)」と「for(y = (height - 1)/2; y height - 1; y++)」という2つのループに分割し、それぞれを並列に処理する、という流れである(並列化後のソースコード全文は記事末にリスト2として掲載)。

    /* フィルタを適用 */
    for(y = 1; y  height - 1; y++) {
        for (x = 1; x  width - 1; x++) {
            /* 対象ピクセルとその近傍8ピクセルのアドレスをソートバッファにコピー*/
            for (j = 0; j  3; j++) {
                for (i = 0; i  3; i++) {
                    tmp_array[3*j + i] = (buf_tmp-array[y-1+j][x-1+i]);
                }
            }
            /* 9ピクセルをソート */
            med_sort(tmp_array, 9);
            /* 中央値となるピクセルの相対位置を取得 */
            dy = get_med_position_y(tmp_array, (buf_tmp-array[y][x]));
            dx = get_med_position_x(tmp_array, (buf_tmp-array[y][x]), width, dy);

            /* 出力バッファの対象ピクセルの値を中央値となるピクセルの値に設定 */
            for (i = 0; i  3; i++) {
                buf_out-array[y][3 * x + i] = buf_in-array[y+dy][3 * (x+dx) + i];
            }
        }
    }

 この処理内ではいくつかの変数が使われているが、この処理を別の関数にまとめる際に引数として与える必要があるものはbuf_tmp、buf_in、buf_outの3つである。そこで、この3つの変数に、ループを開始する値(y_start)および終了する値(y_end)を加えた5つの変数を関数に与える配列として宣言する。そのほかの変数についてはすべてこのループ内で初期化され、また出力にも影響しないので関数内で確保を行えば良い。

typedef struct {
    image_buffer* buf_in;
    image_buffer* buf_out;
    image_buffer* buf_tmp;
    int y_start;
    int y_end;
} thread_param;

 この構造体のポインタを引数として受け取り、並列に処理を実行する関数は次のようになる。なお、_beginthreadexの引数に与える関数は__stdcall形式でなければならない点に注意しよう。

void __stdcall work_thread(thread_param* param) {
    int x, y;
    int dx, dy;
    int i, j;
    JSAMPLE* tmp_array[9];
    const image_buffer* buf_in = param-buf_in;
    const image_buffer* buf_out = param-buf_out;
    const image_buffer* buf_tmp = param-buf_tmp;
    const int y_start = param-y_start;
    const int y_end = param-y_end;
    const int width = param-buf_in-width;

    /* フィルタを適用 */
    for(y = y_start; y  y_end; y++) {
        for (x = 1; x  width - 1; x++) {
            /* 対象ピクセルとその近傍8ピクセルのアドレスをソートバッファにコピー*/
            for (j = 0; j  3; j++) {
                for (i = 0; i  3; i++) {
                    tmp_array[3*j + i] = (buf_tmp-array[y-1+j][x-1+i]);
                }
            }
            /* 9ピクセルをソート */
            med_sort(tmp_array, 9);
            /* 中央値となるピクセルの相対位置を取得 */
            dy = get_med_position_y(tmp_array, (buf_tmp-array[y][x]));
            dx = get_med_position_x(tmp_array, (buf_tmp-array[y][x]), width, dy);

            /* 出力バッファの対象ピクセルの値を中央値となるピクセルの値に設定 */
            for (i = 0; i  3; i++) {
                buf_out-array[y][3*x + i] = buf_in-array[y+dy][3*(x+dx) + i];
            }
        }
    }
}

 実際にスレッドを生成し、関数を呼び出す個所は次のようになる。

    /* 与える引数を設定 */
    for (i = 0; i  2; i++) {
        param[i].buf_in = buf_in;
        param[i].buf_out = buf_out;
        param[i].buf_tmp = buf_tmp;
    }
    param[0].y_start = 1;
    param[0].y_end = (height - 1) / 2;
    param[1].y_start = (height - 1) / 2;
    param[1].y_end = height - 1;

    /* スレッド生成 */
    thread = (HANDLE)_beginthreadex(NULL, 0, (void*)work_thread, (param[0]), 0, NULL);
    work_thread((param[1]));

    /* スレッドの終了を待つ */
    WaitForSingleObject(thread, INFINITE);
    CloseHandle(thread);

 ここでは_beginthreadexでy=1からy=(height-1)/2までの処理を行う別スレッド(スレーブスレッド)を起動させた後、もともとあったスレッド(マスタースレッド)でy=(height-1)/2からy=height-1までの処理を実行するように関数を呼び出している。マスタースレッドでの処理が終了した後は、マスタースレッドはスレーブスレッドが終了するまで待機する(WaitForSingleObject関数)。スレーブスレッドの処理が終了したら、スレッドハンドルを閉じて処理を完了させる。

 このようにして並列化を行ったプログラムを表2のような条件でコンパイルし実行したところ、並列化を行うことで約2倍近い高速化の効果が得られた(表1)。

表1 並列化による処理時間の違い
プログラム実行時間
シングルスレッド版(med_serial.c)並列化前5553ミリ秒
マルチスレッドによる並列化版(med_thread.c)2870ミリ秒
表2 テストの実行環境
要素スペック
CPUCore 2 Duo E6550(2.33GHz)
メモリ2GB
OSWindows Vista Business SP1
コンパイラVisual C++ 2008 Professional Edition(SP1)
テストに使用した画像4000×3000ピクセルのJPEG画像