OS毎のシステムコール実行性能 〜 その4
本来であれば、その3までに実施した計測に先立って確認しておくべきなのですが、各OS毎の「システムコール実施自体のコスト」を、ここで改めて比較しようと思います。
実のところ、「比較しようと思います」などと言いつつ、比較そのものは出来ていないのですが、その理由は順次説明します。
- その1: fork, execve の性能計測
- その2: vfork の性能計測
- その3: fstat, lstat の性能計測
- その4: getpid, gettimeofday の性能計測
- その5: time, umask の性能計測
- その6: まとめ
getpid の性能比較
本来の処理に時間を要する (or 実装によって所要時間が大きく変動し得る) システムコールでは、「システムコール実施コスト」を比較することはできません。
とりあえず思い浮かんだものとしては、自プロセスのプロセスIDを取得する getpid
あたりが妥当そうな気がします。
早速、getpid
実施を繰り返すプログラムを作成して、システムコール実施状況を確認してみました……繰り返し回数とシステムコール実施数が全然一致しません…… orz
Linux 上では strace
、Mac OS X 上では dtruss
と DTrace の両方を使って、システムコール実施状況を確認した限りでは、一度だけ getpid
システムコールが実施されるものの、どんなに繰り返し回数を増やしても、それ以後は getpid
が実施されません。
ソースコードでの確認はしていませんが、おそらく:
という実装ではないかと思われます。
自プロセスのプロセスIDは、fork
/vfork
実行契機以外では不変な訳ですから、実行効率の観点からは、至極妥当な実装と言えます。
その一方で、Solaris 上での getpid
は、都度システムコールが実施されています。
素直な実装ではあるのですが、頻繁に自プロセスのプロセス ID を取得するようなプログラム (例: ログ記録フレームワークのようなもの) の場合、実装側で情報をキャッシュする仕組みを用意しなければ、実行効率の点で残念な事になってしまいますね。
あらかたエントリを書き終えてから知ったのですが、byte-unixbench における System Call Overhead 計測実装の一部として、システムコール getpid
が使用されていました。。
Solaris 的な愚直な実装とそれ以外を切り分けるのには役に立つかもしれませんが、上記の様な getpid
実装の現状を考えると、「性能計測」用途としては立ち位置が微妙なところと言えます。
gettimeofday の性能比較
Linux や Mac OS X の getpid
において、2回目以降のシステムコール実施が抑止されるのは、先述したように何度実施しても返却値が変わらないためでしょう。正確には fork
/vfork
契機でキャッシュされた情報を破棄する必要はあるでしょうが、実質的には「不変」とみなしても良い筈です。
「システムコール実施自体のコスト」計測のために、確実にシステムコールを実施しようとするなら、「実施の都度返却値が異なる (or 副作用が生じる)」システムコールが必要ということになります。「システムコール実施コスト」以外の要因を極力排除するためには、更に「実施毎の処理が極少量」であることも必要でしょう。
とりあえず思い付いたのが gettimeofday
/time
でした。
実のところ、Solaris の time
システムコールの実装が、「高精度時刻情報を取得した上での秒部分取り出し」だったりするので、当初思い付いた gettimeofday
/time
ではなく、管理構造体参照のみで済むであろう処理の軽さを見込んで getpid
を採用したのですが、当てが外れて当初の思い付きに戻っただけだったりします。更に残念な事に、この選択も妥当ではなかったのですが…… (詳細は後述)
閑話休題。
オンラインマニュアルによると、Mac OS X 上では gettimeofday
のみがシステムコール (セクション表記が "2") なので、gettimeofday
を計測対象にしましょう。
早速計測してみると……今度は1回もシステムコールが実施されません…… orz > gettimeofday
アセンブラソースレベルで確認した限りでも、インライン展開のような、関数呼出し指定を無視する類の後処理が実施されている痕跡は、特に見受けられませんでした。
ソースコードレベルの確認では埒が明かないので、以下のような DTrace スクリプトを使って、対象プロセスのシステムコール実施+呼出しスタックを採取してみました。
syscall::*:return /pid == $target/ { ustack(); }
採取した呼出しスタックを見てみると:
libsystem_kernel.dylib`shm_open+0xa libsystem_c.dylib`notify_register_tz+0xd9 libsystem_c.dylib`tzsetwall_basic+0x130 libsystem_c.dylib`localtime+0x117 libsystem_c.dylib`gettimeofday+0x63 perf-gettimeofday`main+0x44 perf-gettimeofday`start+0x34 perf-gettimeofday`0x2 libsystem_kernel.dylib`__mmap+0xa libsystem_c.dylib`notify_register_tz+0xd9 libsystem_c.dylib`tzsetwall_basic+0x130 libsystem_c.dylib`localtime+0x117 libsystem_c.dylib`gettimeofday+0x63 perf-gettimeofday`main+0x44 perf-gettimeofday`start+0x34 perf-gettimeofday`0x2 : :
gettimeofday
がシステムコールでないだけでなく、localtime
呼出し結果を使っている?
「経過時間」ベースの gettimeofday
が、「カレンダー情報」ベースの localtime
を利用するのは、実行効率的にちょっと無理がある気がしたので、公開されているソースコードを調べてみたところ:
__commpage_gettimeofday
が失敗した時だけ、システムコールとしてのgettimeofday
(=__gettimeofday
) を実行- システムコールとしての
gettimeofday
が存在している・実施される可能性があるので、gettimeofday
のセクション表記が 2 であるのは (一応は) 妥当 __commpage_gettimeofday
は アセンブラ実装- 名前からすると、commpage 機能はメモリ共有等を使った情報伝搬とか? (
/proc
配下のファイルを READONLY でmmap
するようなイメージ) - メモリ共有での情報伝播なら、システムコール実施が検出されないのも納得
localtime
呼出しは、第2引数 (vtzp
) が非 NULL な初回呼出しでのみ (timezone 確定のため)
といったことが判明しました。
時刻情報のようなシステムワイドな情報を、システムコールを経由せずに取得する上記のような手法は、Mach や XNU 固有の流儀なのかもしれません。この辺は、いずれ折を見て、詳細を調査したいところです。
結果的に、gettimeofday
は、Mac OS X 上でのシステムコール実施コストの計測には使えないことになります…… orz
ちなみに、gettimeofday
の第2引数に関して、Linux では obsolete (「無視」と同義?) である旨が:
The use of the timezone structure is obsolete; the tz argument should normally be specified as NULL.
Solaris でも ignored である旨が明記されていますが:
The tzp argument to gettimeofday() and settimeofday() is ignored.
Mac OS X では上記のような表記はありませんし、先のトレース結果からもわかるように、非 NULL 指定によって内部での localtime
呼出し契機になります。
つまり、非 NULL な gettimeofday
の第2引数による情報の取得は、Linux/Solaris ではサポートされておらず、格納結果を当てにする実装はポータブルな実装ではなくなる点に注意が必要ですね(初回呼出しの際に、余計な localtime
実行も発生しますし……)。
システムコール実施コストの比較にはなりませんが、OS による性能差が顕著でしたので、折角ですから性能計測結果を記録しておきます。以下の値は、0x4000000 =約6千7百万回の繰り返しで計測したものです。
---- | Mac OS X | Linux | Solaris |
---|---|---|---|
gettimeofday | 2.86 | 250.78 | 32.94 |
Linux での gettimeofday
が際立って時間を要していることがわかります。Mac OS X との比較では、fstat
x 10 回での性能差が、gettimeofday
x 1 回でチャラ (前回の計測結果参照) になる勘定 ですね。
とは言え、繰り返し回数が 0x10000 = 約6万4千と、今回の計測の 1/1000 であるにも関わらず、fork
+ execve
での所要時間差 (こちらは前々回の計測結果参照) が 160 秒もあることと比較すると、性能差の決定的な要因とはなり辛いかな? (処理全体での呼び出し頻度次第ではありますが……) > gettimeofday
fstat
呼び出しよりも遅い時点で、gettimeofday
実行時には「システムコール実施コスト」以上の処理要因が絡んでいることがわかります。つまり、元々計測対象としては不適切だったということですね。
以下、次回に続く。
備考
Linux の gettimeofday
は、オンラインマニュアルのセクション表記通りに普通のシステムコールなのですが、Solaris の gettimeofday
は少々曲者です。
Solaris のオンラインマニュアルにおける gettimeofday
のセクション表記が、ライブラリ関数を表す "3" (厳密には "3C") である一方、time
のセクション表記は、システムコールを表す "2" となっています。
となれば、gettimeofday
の実装が「time
システムコール、あるいはその実現に使用されている内部システムコール」を使用していることが予想されるのですが、実際に確認してみると……これまたシステムコール実施が検出されません!
もしかして Solaris も Mac OS X と同じ方式なのか?とソースコードを確認してみたところ、gettimeofday
実装では 0xd2 割り込みを発生させていました。
_gettimeofday() _gettimeofday: b8 05 00 00 00 movl $0x5,%eax _gettimeofday+0x5: cd d2 int $0xd2
コンパイル時に特定の処理をインラインアセンブラ化させるための設定ファイル usr/src/lib/libc/i386/threads/i386.il
において、高精度の時刻情報取得 API gethrtime
(GET High Resolution TIME) の呼び出しが、以下のように 0xd2 割り込みを発生させていることからも、0xd2 割り込みは時刻情報の取得に用いられているものと思われます。
.inline gethrtime, 0 movl $3, %eax int $0xd2 .end
「割り込み」は「システムコール実施」とは異なる仕組みですから、gettimeofday
呼び出しの延長で「システムコール実施」が検出されないのも納得ですね。