彷徨えるフジワラ

年がら年中さまよってます

OS毎のシステムコール実行性能 〜 その2

前回は、fork および execve システムコールの実行性能を、OS毎に計測して比較してみました。

fork を比較したのなら、当然 vfork も比較したくなるのが人情ですよね?(笑)

vfork の性能比較

確認用プログラムの手順は、vfork を呼び出す以外は、fork の性能比較時と全く同じです。

  1. 引数文字列を整数に変換
  2. 0 なら終了
  3. それ以外なら、vfork を実施
  4. 子プロセスは、即時 _exit
  5. 親プロセスは、子プロセスを wait
  6. 指定整数から 1 を減じて、手順2から繰り返し

繰り返し回数 0x10000 = 約6万4千の実行に要する時間 (単位:秒) を計測してみると…… Mac OS X での実行性能が異常に速い!

あまりの速さに、「NOT IMPLEMENTED で即時終了」を疑って dtruss コマンドでシステムコール呼び出しを確認した程です。fork/execve での鈍足振りからは、にわかには信じられない値です。

---- MacOSX Linux Solaris
vfork 0.52 6.03 5.68
fork 27.36 9.35 24.32
対fork比 1.9% 64.5% 23.4%

fork 比は 19% ではありません。1 . 9 %、イッテンキューパーセントです。約 1/50 です。

また、fork からの性能向上も凄いのですが、単純に所要時間の値そのものも、他のOSと比較して圧倒的に小さくなっています。文字通り、桁が違います。

Mac OS X における forkvfork の圧倒的な実行性能差の原因は、dtruss の出力から推測することが出来ます。

fork 実行性能測定用のプログラムを実行した場合、dtruss -f (= fork 先プロセスへの追従を指示) でのトレース結果は以下の様になります。

        PID/THRD  SYSCALL(args)                  = return

(※ 親プロセス側)
37809/0x21dcb8:  fork()          = 37812 0
37809/0x21dcb8:  wait4(0xFFFFFFFF, 0x7FFF6D86EC78, 0x0)          = 37812 0

(※ 子プロセス側)
37812/0x21dcbd:  fork()          = 0 0

その一方で、vfork 実行性能測定用のプログラム実行を dtruss -f でトレースした結果は以下のようになります。

        PID/THRD  SYSCALL(args)                  = return

(※ 親プロセス側)
37826/0x21dd3e:  vfork()                 = 37827 0
37826/0x21dd3e:  wait4(0xFFFFFFFF, 0x7FFF63D9BC68, 0x0)          = 37827 0

(※ 子プロセス側)
37827/0x21dd3e:  vfork()                 = 37827 0

fork では、親プロセスと子プロセスでスレッドが異なりますが、vfork では、両者のスレッドが同一になっているのがわかるでしょうか? (vfork の場合、dtruss には子プロセス側の戻り値が何故か非ゼロに見える模様なので注意)

つまり、Mac OS Xvfork では、子プロセス側では新規スレッドを生成せずに、親プロセスのスレッドを間借りしたまま実行を続けている、ということになります。

そもそも vfork の仕様が、fork からの性能改善のために:

  • 親プロセスと仮想アドレス空間を分離しなくても良い
  • 子プロセスでの execve あるいは _exit() までは、親プロセス側に制御が戻らなくても良い

上記の様な挙動を許容していますので、カーネル内部での挙動が:

  • プロセス管理情報だけは新規作成
  • 仮想アドレス空間は、親プロセスのものをそのまま利用 (多分管理情報のコピー等もしてないのでは?)
  • スレッドも生成せず、親プロセスの vfork 実施スレッドをそのまま利用

という感じで、新規リソースの生成等を必要最小限に抑えているのだと推測されます。

ちなみに、対 fork 比で 1/4 まで所要時間を短縮している Solarisvfork ですが、それでも Mac OS X の所要時間とは一桁違います。

以下の様な dtrace スクリプトを使って調べてみたところ、Solarisvfork では子プロセス側のスレッドは新規生成されたものが使用されていました。

/*
 * USAGE: dtrace -s get-forked-result -c "command to be traced" command-name
 */

syscall::*fork*:return
/execname == $1/
{
    printf("% 8d @thread=%p", arg1, curthread);
}

vfork を使用する局面では、_exit での終了よりも、execve が実施される = 新規プログラム実行のためにスレッドが必要になる可能性が圧倒的に高いことを考えると、いずれ払うであろうスレッド新規生成コストを節約しても意味は無い、という判断なのではないでしょうか? > Solaris

実際に execve との組み合わせ実行で計測してみると、元々 execve のコストが支配的であるため、forkvfork における圧倒的な性能改善にも関わらず、Mac OS X におけるトータルの性能改善は 17% 程度になっています。

---- MacOSX Linux Solaris
vfork + execve 161.87 25.56 51.11
fork + execve 194.90 31.06 68.04
対「fork + execve」比 83.1% 82.3% 75.1%

vfork/execve でも、合算値・実測値の比較を行ってみましょう。

---- MacOSX Linux Solaris
「vfork」実測 0.52 6.03 5.68
「execve」実測 157.19 22.02 39.41
「vfork」と「execve」の合算 157.71 28.05 45.09
「vfork + execve」実測 161.87 25.56 51.11
「実測」-「合算」(「実測」比) 4.16 (2.6%) -2.49 (9.7%) 6.02 (11.8%)

Linuxfork の際と同様に、こちらでも負値ですね…… 個別実施と比較して、同時実施で増加するオーバヘッド等が殆ど無いのかもしれません。

Solaris における実測値・合算値の差は、vfork で省力化した分の子プロセスの資源管理周りの処理が、実際に子プロセス側で execve を実施するに当たって計上された分であろうことは、想像が付きます。

その一方で、Mac OS X では、fork + execve 時 (+10.35, 5.3%) よりも、実測値・合算値の差が小さくなっています。vfork での処理省力化を考えると、こちらもそこそこオーバヘッド増加があって然るべきな気がするのですが……

この辺に関しては、Mac OS Xカーネル実装がどうなっているのか調べてみたいところです。

Mac OS X のベースになっている Mach カーネルは、ページフォールト処理周りもユーザ空間で処理可能な、マイクロカーネルアーキテクチャを採用していますから、コンテキストスイッチ周りのオーバヘッド等々が関係してくる可能性はあります。

これまでに実施した性能計測でも、実処理時間に占める user 消費時間の高さが特徴的でしたから、この辺が挙動の疑問点の原因を突き止める糸口になるかもしれません。

以下、次回に続く。

備考

カーネルソースコード中のコメントによると、Solarisvfork システムコール実装は:

  • 内部的には「フラグ値違いの fork 呼び出し」と等価
  • libc からの呼び出しは、既に「フラグ値違いの fork 呼び出し」実装に移行済み (= vfork システムコールは呼び出されない)
  • Solaris 10 コンテナで実行されるプログラム向けに vfork システムコール自体は残してある

という状況にある模様です。

そのため、dtrace スクリプトの syscall プローブの捕捉対象に vfork を列挙しても、一般のプログラムからの vfork 呼び出しを捕捉する事はできません。

先述したように、システムコールとしての vfork は残っているため、vfork に対するプローブ設定自体はできてしまい、実際には呼び出しを捕捉出来ないのに、一見すると dtrace スクリプトには問題が無いように見えてしまいます。

また、truss コマンドの出力上は、vfork として識別されていますので、更に話をややこしくさせている印象が……

本文中で引用した dtrace スクリプト中の syscall プローブで、対象システムコールの記述を *fork* 形式にしているのは、forksys (fork システムコールの実体) と vfork の両方にプローブを設定するためです。

syscall::*fork*:return
/execname == $1/
{
    printf("% 8d @thread=%p", arg1, curthread);
}

システムコールの正式名称は /etc/name_to_sysnum ファイルでも確認できます。