誤った並列化による性能劣化(その1)

前回の記事で「複数のスレッドが共有する変数をインクリメントするときには排他制御が必要」という話をしました。その結果、正しい値を得ることには成功したものの、性能が著しく落ちるという知見を得ました。今回は性能が出ないマルチスレッドプログラムに対して割り当てるCPU数をあえて制限してみます。

使うプログラム”mutex_test.c”はC言語で書かれたマルチスレッドプログラムで、起動すると10個のスレッドを起こし、それぞれのスレッドが共有変数を100万回カウントアップします。カウントアップするクリティカルセクションには複数のスレッドが同時に侵入することがないようmutexで保護しています。

#include <stdio.h>
#include <pthread.h>


struct params {
    int *i; 
    pthread_mutex_t *mut;
};

void *func(void *arg) {
    int j;
    struct params *prm = (struct params *) arg;

    for (j = 0; j < 1000000; j++) {
        pthread_mutex_lock(prm->mut);
        (*(int *)prm->i)++; // counts up
        pthread_mutex_unlock(prm->mut);
    }   

    return NULL;
}

int main() {
    int n = 0, x = 0;
    pthread_t th[10];
    pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
    struct params prm = {&x, &mut};

    for (n = 0; n < 10; n++)
        pthread_create(&th[n], NULL, func, &prm);

    for (n = 0; n < 10; n++)
        pthread_join(th[n], NULL);

    printf("%d\n", x); 

    return 0;
}

このソースをコンパイルしてから、timeコマンドを噛ませて実行すると、

$ gcc mutex_test.c -lpthread -Wall
$ time ./a.out 
10000000

real	0m2.117s
user	0m1.272s
sys	0m14.747s

こんな結果が返ります。1000万回のカウントアップにユーザCPU時間1.3秒とシステムCPU時間14.7秒の合計で、16秒ものCPU時間を消費しています。使っているマシンのCPUは Intel Xeon E31230 で動作周波数3.20GHz、4コア8スレッドなので、単純計算すると約2000億クロックサイクル(16秒*3.20GHz*4コア)を消費しています。つまり整数型の値を1回インクリメントするにあたって2万クロックサイクルという極めてハイコストな処理が発生しているわけです。毎回均一に2万クロックサイクルを消費するわけではないので平均にあまり意味はありませんが、感覚的にエクスペンシブです。

見方を変えてみましょう。インクリメント自体にそんなコストがかかるわけがありません。コンテキストスイッチングやスレッド安全確保のための排他制御のオーバヘッドがいかに大きいということです。このオーバヘッドに見合うだけの処理をしなければ、並列化によって性能が落ちます。

では試しに、tasksetコマンドを使ってこのプログラムが使用できるCPU数を変えて傾向を見てみます。まず全てのCPUを使った場合、

$ time taskset -c 0,1,2,3,4,5,6,7 ./a.out 
10000000

real	0m2.143s
user	0m1.340s
sys	0m14.888s

16秒。これはなにも制限していないことと変わらないので、結果も同程度です。次に、CPU数を4つに制限した場合、

$ time taskset -c 0,1,2,3 ./a.out 
10000000

real	0m0.978s
user	0m0.912s
sys	0m2.778s

4秒。CPU時間の消費が大幅に減りました。では次に、CPU数を1つに制限した場合、

$ time taskset -c 0 ./a.out 
10000000

real	0m0.213s
user	0m0.209s
sys	0m0.004s

0.213秒。おわかりいただけたでしょうか。75倍高速になりました。同じプログラムなので無駄に10スレッドが起動してコンテキストスイッチングが発生しますし、各スレッドがロック獲得のための待ち行列に並ぶ可能性もありますが、CPU間での処理移譲の可能性を排除しただけでこれだけ処理が効率化します。

頑張って並列化プログラミングしてみたものの、実は1つのCPUを割り当てたほうが速いなんていう設計では目も当てられません。ボトルネックを見極めましょう。あるいはもし誰かが作った重たいプロセスに、1つのCPUのみを割り当ててみると、すてきな結果を得られるかもしれません。

次回は、今回はmutexのロック獲得と開放について簡単な実験します。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*