彷徨えるフジワラ

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

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

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
Linux での time は、gettimeofday ベースでの実装ですので、両者の所要時間は基本的に同等です。

上記の計測結果で、gettimeofday の所要時間の方が若干多目なのは:

  • 計測上の誤差 (仮想環境上での 2% の差) and/or
  • gettimeofday の方が、結果格納のためのメモリアクセス分だけコスト高

といった理由ではないかと思われます。

一方 Solaris では、前回の備考で触れたように、gettimeofday が「割り込み」を使って実現されているのに対して、time は普通のシステムコールとして実現されています。

内部的にはどちらも gethrtime で取得された時刻情報を使用しているのですが、この実現方式の違い and/or 結果格納のためのメモリアクセス分のコスト差が、上記の様な所要時間差になっているのではないかと思われます。

もう少し突っ込んだ Solaris の話は、例によって後ほど備考にて(笑)。

さて、Mac OS X では、timegettimeofday と同様に、システムコール実施無しで実現 (前回参照) されているのですが、どういうわけか time の方は測定結果がよろしくありません。

理屈の上では:

  • 秒情報部分だけの扱いになる
  • 結果格納のためのメモリアクセスが (今回の計測のケースでは) 不要

という点で、time の方が有利な筈なので、せめて同等程度の値になって欲しいのですが……

流石に無視できない所要時間差でしたので、公開されているソースコードを確認してみたところ、gettimeofday 呼び出しの前後で、fegetenv および fesetenv を使用した、浮動小数点演算環境の退避・復旧を行っているのが原因と思われます。

ソースコード上は、FE_DFL_ENV マクロ定義に応じて、呼び出しの有無が異なりますが、Mac OS Xfsenv.h ヘッダファイル (少なくとも、x86 環境で読み込まれるアーキテクチャ依存ヘッダファイル) では、FE_DFL_ENV が定義されているので、まず間違いなく fegetenv および fesetenv が呼び出されているでしょう。

浮動小数点演算環境の退避・復旧」ということは、FPU レジスタ周りのあれやこれやを操作する筈(特権モード移行が必要になるかも?)なので、gettimeofdaytime の間に、umask (後述) で計測された「システムコール呼び出しコスト」相当の差があるのも、これなら納得できます。

なお、私の使用してる Mac OS X 10.7.5/Xcode 4.6.3 環境では、 fegetenv および fesetenv のオンラインマニュアルが参照できませんでしたが、少なくともウェブ上では参照できるようですので、ひょっとしたら最新版の Xcode では参照できるようになっているのかもしれません。

ちなみに、数千回程度の連続実行では、timegettimeofdayシステムコール実施が検出されないのですが、今回の計測での数千万回単位での連続呼び出しのような、ある程度の負荷が掛かっている状態では、以下の様な呼び出しスタックの延長で、システムコールとしての 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 上での umaskgetpid の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 バイナリを使って実行性能を計測してみると、getpidumask がほぼ同等の実行性能であることが確認できました。

なお、time の場合は、32bit/64bit バイナリで有意な性能差を確認できなかったのですが:

  • 32bit/64bit バイナリで、実行されるアセンブラ命令列のバイト数に大きな差異が無い (事実)
  • 計測時は引数 NULL で呼び出し (事実) ⇒ メモリ格納コストが無い (予想)
  • スタック経由/レジスタ経由の引数渡しコスト差の影響が少ないケース (予想)

上記の様な理由によるものではないかと推測しています。

また、time 同様に gettimeofday も、32bit/64bit バイナリで実行性能差が確認できなかったのですが、gettimeofday 実現で使用している「割り込み」では、割り込みの発生〜結果の受け渡し方法が 32bit/64bit バイナリで差異が無いことが、有意な実行性能差が生じない原因ではないかと思われます。