マルチスレッドの方がマルチプロセスより遅くなることがある

という話。いや、大した話では無いのですが。
マルチスレッドプログラムはデフォルトですべてのメモリ空間を全スレッドで共有します。ので、共有されるメモリを操作する場合はロック・アンロックが必要です。というのは当たり前なのですが、このため、多くのライブラリコールの内部でロック・アンロックが行われています。
ライブラリコールなんてそんなにたくさんしないよ、とか思うのですが、ヒープ上のメモリを確保するライブラリコールであるところの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というメモリアロケータを紹介したことがありますが、こういうのを使うと改善されるんだとは思います。

まぁ、メモリ確保時間がプログラム実行時間で問題になるというのは、プログラムの作りで何かが間違っているような気がするので、それほど問題にはならないとは思いますが。:-)