OS毎のシステムコール実行性能 〜 その2
前回は、fork
および execve
システムコールの実行性能を、OS毎に計測して比較してみました。
fork
を比較したのなら、当然 vfork
も比較したくなるのが人情ですよね?(笑)
- その1: fork, execve の性能計測
- その2: vfork の性能計測
- その3: fstat, lstat の性能計測
- その4: getpid, gettimeofday の性能計測
- その5: time, umask の性能計測
- その6: まとめ
vfork の性能比較
確認用プログラムの手順は、vfork
を呼び出す以外は、fork
の性能比較時と全く同じです。
- 引数文字列を整数に変換
- 0 なら終了
- それ以外なら、
vfork
を実施 - 子プロセスは、即時
_exit
- 親プロセスは、子プロセスを
wait
- 指定整数から 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 における fork
と vfork
の圧倒的な実行性能差の原因は、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 X の vfork
では、子プロセス側では新規スレッドを生成せずに、親プロセスのスレッドを間借りしたまま実行を続けている、ということになります。
そもそも vfork
の仕様が、fork
からの性能改善のために:
- 親プロセスと仮想アドレス空間を分離しなくても良い
- 子プロセスでの
execve
あるいは_exit()
までは、親プロセス側に制御が戻らなくても良い
上記の様な挙動を許容していますので、カーネル内部での挙動が:
- プロセス管理情報だけは新規作成
- 仮想アドレス空間は、親プロセスのものをそのまま利用 (多分管理情報のコピー等もしてないのでは?)
- スレッドも生成せず、親プロセスの
vfork
実施スレッドをそのまま利用
という感じで、新規リソースの生成等を必要最小限に抑えているのだと推測されます。
ちなみに、対 fork
比で 1/4 まで所要時間を短縮している Solaris の vfork
ですが、それでも Mac OS X の所要時間とは一桁違います。
以下の様な dtrace スクリプトを使って調べてみたところ、Solaris の vfork
では子プロセス側のスレッドは新規生成されたものが使用されていました。
/* * 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
のコストが支配的であるため、fork
⇒ vfork
における圧倒的な性能改善にも関わらず、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%) |
Linux は fork
の際と同様に、こちらでも負値ですね…… 個別実施と比較して、同時実施で増加するオーバヘッド等が殆ど無いのかもしれません。
Solaris における実測値・合算値の差は、vfork
で省力化した分の子プロセスの資源管理周りの処理が、実際に子プロセス側で execve
を実施するに当たって計上された分であろうことは、想像が付きます。
その一方で、Mac OS X では、fork
+ execve
時 (+10.35, 5.3%) よりも、実測値・合算値の差が小さくなっています。vfork
での処理省力化を考えると、こちらもそこそこオーバヘッド増加があって然るべきな気がするのですが……
この辺に関しては、Mac OS X のカーネル実装がどうなっているのか調べてみたいところです。
Mac OS X のベースになっている Mach カーネルは、ページフォールト処理周りもユーザ空間で処理可能な、マイクロカーネルアーキテクチャを採用していますから、コンテキストスイッチ周りのオーバヘッド等々が関係してくる可能性はあります。
これまでに実施した性能計測でも、実処理時間に占める user 消費時間の高さが特徴的でしたから、この辺が挙動の疑問点の原因を突き止める糸口になるかもしれません。
以下、次回に続く。
備考
カーネルソースコード中のコメントによると、Solaris の vfork
システムコール実装は:
- 内部的には「フラグ値違いの
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
ファイルでも確認できます。