OS毎のシステムコール実行性能 〜 その5
- その1: fork, execve の性能計測
- その2: vfork の性能計測
- その3: fstat, lstat の性能計測
- その4: getpid, gettimeofday の性能計測
- その5: time, umask の性能計測
- その6: まとめ
time の性能比較
「システムコール実行性能の比較」という点では微妙ですが、前回の gettimeofday
で乗りかかった船 (笑) ですから、time
の実行性能も比較してみましょう。
以下は、0x4000000 =約6千7百万回の繰り返しで計測したものです (単位: 秒)。なお、time
の引数には NJLL を指定し、結果の書き込みは行わせないようにしています。
---- | Mac OS X | Linux | Solaris |
---|---|---|---|
time | 13.30 | 243.82 | 26.39 |
gettimeofday | 2.86 | 250.78 | 32.94 |
time
は、gettimeofday
ベースでの実装ですので、両者の所要時間は基本的に同等です。上記の計測結果で、gettimeofday
の所要時間の方が若干多目なのは:
- 計測上の誤差 (仮想環境上での 2% の差) and/or
gettimeofday
の方が、結果格納のためのメモリアクセス分だけコスト高
といった理由ではないかと思われます。
一方 Solaris では、前回の備考で触れたように、gettimeofday
が「割り込み」を使って実現されているのに対して、time
は普通のシステムコールとして実現されています。
内部的にはどちらも gethrtime
で取得された時刻情報を使用しているのですが、この実現方式の違い and/or 結果格納のためのメモリアクセス分のコスト差が、上記の様な所要時間差になっているのではないかと思われます。
もう少し突っ込んだ Solaris の話は、例によって後ほど備考にて(笑)。
さて、Mac OS X では、time
も gettimeofday
と同様に、システムコール実施無しで実現 (前回参照) されているのですが、どういうわけか time
の方は測定結果がよろしくありません。
理屈の上では:
- 秒情報部分だけの扱いになる
- 結果格納のためのメモリアクセスが (今回の計測のケースでは) 不要
という点で、time
の方が有利な筈なので、せめて同等程度の値になって欲しいのですが……
流石に無視できない所要時間差でしたので、公開されているソースコードを確認してみたところ、gettimeofday
呼び出しの前後で、fegetenv
および fesetenv
を使用した、浮動小数点演算環境の退避・復旧を行っているのが原因と思われます。
ソースコード上は、FE_DFL_ENV
マクロ定義に応じて、呼び出しの有無が異なりますが、Mac OS X の fsenv.h
ヘッダファイル (少なくとも、x86 環境で読み込まれるアーキテクチャ依存ヘッダファイル) では、FE_DFL_ENV
が定義されているので、まず間違いなく fegetenv
および fesetenv
が呼び出されているでしょう。
「浮動小数点演算環境の退避・復旧」ということは、FPU レジスタ周りのあれやこれやを操作する筈(特権モード移行が必要になるかも?)なので、gettimeofday
と time
の間に、umask
(後述) で計測された「システムコール呼び出しコスト」相当の差があるのも、これなら納得できます。
なお、私の使用してる Mac OS X 10.7.5/Xcode 4.6.3 環境では、 fegetenv
および fesetenv
のオンラインマニュアルが参照できませんでしたが、少なくともウェブ上では参照できるようですので、ひょっとしたら最新版の Xcode では参照できるようになっているのかもしれません。
ちなみに、数千回程度の連続実行では、time
も gettimeofday
もシステムコール実施が検出されないのですが、今回の計測での数千万回単位での連続呼び出しのような、ある程度の負荷が掛かっている状態では、以下の様な呼び出しスタックの延長で、システムコールとしての gettimeofday
が実施されていることが確認できました。
libsystem_kernel.dylib`__gettimeofday+0xa perf-gettimeofday`main+0x80 perf-gettimeofday`start+0x34 perf-gettimeofday`0x3
繰り返し回数 0x4000000 =約6千7百万回に対して数万回 (0.2% 未満) と、システムコール実施の頻度自体は極稀なのですが、「システムコールが全く実施されない」という仮定が、常時成立する訳ではない点には、留意しておいた方が良いかもしれません。
umask の性能比較
byte-unixbench では、System Call Overhead 計測実装の一環として、妥当とは言えない getpid
以外に、umask
が計測対象になっていました。
umask
なら:
- 「プロセスの属性情報変更」という副作用と伴うので、都度システムコール実施される可能性が高い
(同一引数での連続実施の場合、カーネル空間に切り替えずに、ユーザ空間で折り返してしまう可能性がゼロではありませんが……) - 管理情報にマスク値を設定する処理は、十分軽そう
ということから、getpid
よりも上手く行きそうな気がします。
以下は、0x4000000 =約6千7百万回の繰り返しで計測したものです (単位: 秒)。
---- | Mac OS X | Linux | Solaris |
---|---|---|---|
umask | 11.61 | 3.61 | 23.70 |
どの OS でも、繰り返し回数に相当するシステムコール実施が確認できました。計測結果を見る限りでは、Mac OS X でのシステムコール実施は、Linux の3倍強のオーバヘッドがありますね。
とは言え、Mercurial のテストセット全体でのシステムコール実施回数が、上記計測での繰り返しと同等の約 6 千万回 (@Linux) でしたので、各システムコールの実処理性能が、仮に全て同一だとしても、高々 8 秒程度の差にしかなりません。
stat
系システムコールでの性能差と同様に、「体感できる程度の所要時間差」の要因としては、弱そうですね。
以下、次回に続く。
備考
Solaris 上での umask
と getpid
の2つのシステムコールは、どちらの実装も、幾つかの間接参照と値の代入ぐらいしか実施していないので、本来であれば実行性能差が殆ど無い筈なのですが:
---- | Mac OS X | Linux | Solaris |
---|---|---|---|
umask | 11.61 | 3.61 | 23.70 |
getpid | ---- | ---- | 21.78 |
実際に計測してみると、上記のように微妙な性能差が検出されます。
実はこの性能差、実行プログラムのメモリモデルの差に由来するものと思われます。
64bit 環境としてインストールされた Linux では、実行可能ファイル生成時のデフォルトは 64bit バイナリになります。一方 Solaris の場合は、64bit 環境でも 32bit バイナリがデフォルトになります。
32bit バイナリでは、関数呼び出しでの引数受け渡し等がスタック経由(= メモリへの格納が発生)になりますが、64bit バイナリでは、レジスタ経由での引数受け渡しが主体になります。レジスタ経由の受け渡しの方がオーバヘッドが少ないことは、以前実験した際に確認済みです。
getpid
はユーザ空間/カーネル空間共に引数を必要としないので、32bit バイナリと 64bit バイナリの引数受け渡し方法による差が、顕在化しないのだと思われます。
また、umask
呼び出しループで実行されるアセンブラ命令列のバイト数も:
---- | 32bit | 64bit |
---|---|---|
main 側 | 30 | 32 |
libc 側 | 28 | 17 |
トータル | 58 | 49 |
上記のように、64bit バイナリの方が 15% 程少なくなっています。
実際に 64bit バイナリを使って実行性能を計測してみると、getpid
と umask
がほぼ同等の実行性能であることが確認できました。
なお、time
の場合は、32bit/64bit バイナリで有意な性能差を確認できなかったのですが:
- 32bit/64bit バイナリで、実行されるアセンブラ命令列のバイト数に大きな差異が無い (事実)
- 計測時は引数 NULL で呼び出し (事実) ⇒ メモリ格納コストが無い (予想)
- スタック経由/レジスタ経由の引数渡しコスト差の影響が少ないケース (予想)
上記の様な理由によるものではないかと推測しています。
また、time
同様に gettimeofday
も、32bit/64bit バイナリで実行性能差が確認できなかったのですが、gettimeofday
実現で使用している「割り込み」では、割り込みの発生〜結果の受け渡し方法が 32bit/64bit バイナリで差異が無いことが、有意な実行性能差が生じない原因ではないかと思われます。