MacOS Xのfork/execが遅い件について

オフトピ*1だが、「libtaskとPthreadの比較」で軽く触れた、MacOS Xのforkが遅い件についてもう少し調べてみた。

前回使ったプログラムはこんな感じのもの。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

enum {
  ITER = 100
};

void
do_fork(void)
{
  pid_t pid;

  switch (pid = fork()) {
  case -1:
    perror("fork");
    exit(1);

  case 0: /* child */
    exit(1);

  default:
    while (wait(0) != pid);
  }
}

int
main(int argc, char **argv)
{
  int i;
  struct timeval tv;
  double begin, end;

  gettimeofday(&tv, NULL);
  begin = tv.tv_sec + tv.tv_usec * 1e-6;

  for (i = 0; i < ITER; i++) {
    do_fork();
  }

  gettimeofday(&tv, NULL);
  end = tv.tv_sec + tv.tv_usec * 1e-6;
  printf("%g\n", (end - begin) / ITER);

  return 0;
}

今回は、BYTE UNIX Benchmark 4.1.0のシステムベンチマークで比較してみた。

MacOS X 10.4、Core 2 Duo/2.0GHzの場合。

TEST                                        BASELINE     RESULT      INDEX

Execl Throughput                               188.3      435.8       23.1
File Copy 1024 bufsize 2000 maxblocks         2672.0    48484.0      181.5
File Copy 256 bufsize 500 maxblocks           1077.0    13844.0      128.5
File Read 4096 bufsize 8000 maxblocks        15382.0   315259.0      205.0
Pipe Throughput                             111814.6   439237.6       39.3
Pipe-based Context Switching                 15448.6   107754.3       69.8
Process Creation                               569.3      887.4       15.6
Shell Scripts (8 concurrent)                    44.8      270.5       60.4
System Call Overhead                        114433.5   277117.6       24.2
                                                                 =========
     FINAL SCORE                                                      57.5

Linux CentOS 5.2、Pentium 4/3.0GHz dualの場合。

TEST                                        BASELINE     RESULT      INDEX

Execl Throughput                               188.3     3692.7      196.1
File Copy 1024 bufsize 2000 maxblocks         2672.0    59336.0      222.1
File Copy 256 bufsize 500 maxblocks           1077.0    16637.0      154.5
File Read 4096 bufsize 8000 maxblocks        15382.0   619832.0      403.0
Pipe Throughput                             111814.6   538893.8       48.2
Pipe-based Context Switching                 15448.6   134891.3       87.3
Process Creation                               569.3    11984.3      210.5
Shell Scripts (8 concurrent)                    44.8      672.0      150.0
System Call Overhead                        114433.5   592053.3       51.7
                                                                 =========
     FINAL SCORE                                                     138.4

やはりexeclスループットで8.3倍、プロセス生成で13.5倍も性能差がある。ハードウェアが全く違うので、一概に比較できないが、システムコール自体のオーバヘッドが2.1倍なので、fork/execは明らかに遅いといえる。

ということで、ktraceを使って実行時間をトレースしてみる。

$ ktrace ./bench.fork
$ kdump -R -f ktrace.out

wait4を呼出してから戻るまで(ktraceのオーバヘッドで遅くなっているが)3ミリ秒近くかかっている。これはwait4に処理時間を食っているわけではなく、この間に子プロセスが動いているのだろうが、なぜこんなに時間がかかるのか。やっぱりよくわからない。

  9775 bench.fork 0.000442 CALL  fork
  9775 bench.fork 0.000119 RET   fork 9776/0x2630
  9775 bench.fork 0.000119 CALL  wait4(0xffffffff,0,0,0)
  9775 bench.fork 0.003081 RET   wait4 9776/0x2630

じゃぁ、他のプロセスの影響を極力なくそうと、シングルユーザモード*2で測定してみた。execlスループットは435.8 lpsから828.0 lps、プロセス生成は887.4 lpsから1856.8 lpsと、2倍前後改善した。それでも遅いなぁ。

*1:オプトピついでに、今回MacOS Xが静的リンクに対応していないことを初めて知った。

*2:コマンドキーと's'を同時に押しながら起動する。