マルチスレッドの方がマルチプロセスより遅くなることがある
という話。いや、大した話では無いのですが。
マルチスレッドプログラムはデフォルトですべてのメモリ空間を全スレッドで共有します。ので、共有されるメモリを操作する場合はロック・アンロックが必要です。というのは当たり前なのですが、このため、多くのライブラリコールの内部でロック・アンロックが行われています。
ライブラリコールなんてそんなにたくさんしないよ、とか思うのですが、ヒープ上のメモリを確保するライブラリコールであるところのmallocの内部でもロック・アンロックが行われています(そのはず)。プロセスがOSからもらってきたメモリ領域を(安直にはフリーリストか何かで)切り分けて使っているはずなので、当然と言えば当然ですね。
で、mallocなんてそんなにたくさん呼ばないよ、とか思うのですが、C++でnewを使うと内部的にはmallocが呼ばれてしまいます。newを大量に呼んでいるプログラムだと、このロック・アンロックが性能に影響するかも???
で、試してみた。
pthread版(pthread.cc)
#include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> class Foo{ int i; }; int main(int argc, char* argv[]) { void* foo(void *); if(argc < 2) {fprintf(stderr,"pthread count\n"); return 0;} int count = atoi(argv[1]); pthread_t thr; int i; Foo* x; pthread_create(&thr, NULL, foo, (void*)count); #ifdef MULTI for(i = 0; i < count; i++) x = new Foo(); #endif pthread_join(thr,NULL); return 0; } void* foo(void *arg) { Foo* x; for(int i = 0; i < (int)arg; i++) x = new Foo(); pthread_exit(NULL); }
fork版(fork.cc)
#include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> class Foo{ int i; }; int main(int argc, char* argv[]) { if(argc < 2) {fprintf(stderr,"fork count\n"); return 0;} int count = atoi(argv[1]); pid_t pid; Foo *x; int i; if((pid = fork()) == 0){ for(i = 0; i < count; i++) x = new Foo(); _exit(0); } else { #ifdef MULTI for(i = 0; i < count; i++) x = new Foo(); #endif int tmp; wait(&tmp); } return 0; }
どちらも、ただスレッドまたはプロセスを1つ作成して、newを呼ぶだけです。MULTIが定義されている場合は、もとのプロセスと作成したプロセスの両方でnewを実行します。定義されていない場合は作成したプロセスのみで実行します。
で、実行結果(on Macbook)。まずはMULTIを定義しないで一つのプロセスだけで:
$ g++ -O2 -o pthread pthread.cc $ time ./pthread 10000000 real 0m1.226s user 0m0.920s sys 0m0.270s $ g++ -O2 -o fork fork.cc $ time ./fork 10000000 real 0m0.945s user 0m0.609s sys 0m0.269s
ん、fork版の方がやはり少しだけ速いですね。では次にMULTIを定義して、2つのプロセスでnewを実行してみます。
$ g++ -O2 -DMULTI -o pthread pthread.cc $ time ./pthread 10000000 real 0m7.067s user 0m12.118s sys 0m1.331s $ g++ -O2 -DMULTI -o fork fork.cc $ time ./fork 10000000 real 0m1.026s user 0m1.216s sys 0m0.613s
おお、ずいぶんpthread版は遅くなりました。
fork版の方は、userが倍くらい、realは同じくらいと、うまく並列に動いているように見えます。
pthread版の方は、userが激しく大きくなっています。これは、おそらくロック・アンロックがスピンロックで実現されていて、同時にロックを取ろうとして待ちがおこっているのでしょう。
以前Hoardというメモリアロケータを紹介したことがありますが、こういうのを使うと改善されるんだとは思います。
まぁ、メモリ確保時間がプログラム実行時間で問題になるというのは、プログラムの作りで何かが間違っているような気がするので、それほど問題にはならないとは思いますが。:-)