前回の記事で「複数のスレッドが共有する変数をインクリメントするときには排他制御が必要」という話をしました。その結果、正しい値を得ることには成功したものの、性能が著しく落ちるという知見を得ました。今回は性能が出ないマルチスレッドプログラムに対して割り当てる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のロック獲得と開放について簡単な実験します。