彷徨えるフジワラ

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

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

本来であれば、その3までに実施した計測に先立って確認しておくべきなのですが、各OS毎の「システムコール実施自体のコスト」を、ここで改めて比較しようと思います。

実のところ、「比較しようと思います」などと言いつつ、比較そのものは出来ていないのですが、その理由は順次説明します。

getpid の性能比較

本来の処理に時間を要する (or 実装によって所要時間が大きく変動し得る) システムコールでは、「システムコール実施コスト」を比較することはできません。

とりあえず思い浮かんだものとしては、自プロセスのプロセスIDを取得する getpid あたりが妥当そうな気がします。

早速、getpid 実施を繰り返すプログラムを作成して、システムコール実施状況を確認してみました……繰り返し回数とシステムコール実施数が全然一致しません…… orz

Linux 上では straceMac OS X 上では dtruss と DTrace の両方を使って、システムコール実施状況を確認した限りでは、一度だけ getpid システムコールが実施されるものの、どんなに繰り返し回数を増やしても、それ以後は getpid が実施されません。

ソースコードでの確認はしていませんが、おそらく:

という実装ではないかと思われます。
自プロセスのプロセスIDは、fork/vfork 実行契機以外では不変な訳ですから、実行効率の観点からは、至極妥当な実装と言えます。

その一方で、Solaris 上での getpid は、都度システムコールが実施されています。

素直な実装ではあるのですが、頻繁に自プロセスのプロセス ID を取得するようなプログラム (例: ログ記録フレームワークのようなもの) の場合、実装側で情報をキャッシュする仕組みを用意しなければ、実行効率の点で残念な事になってしまいますね。

あらかたエントリを書き終えてから知ったのですが、byte-unixbench における System Call Overhead 計測実装の一部として、システムコール getpid が使用されていました。。

Solaris 的な愚直な実装とそれ以外を切り分けるのには役に立つかもしれませんが、上記の様な getpid 実装の現状を考えると、「性能計測」用途としては立ち位置が微妙なところと言えます。

gettimeofday の性能比較

LinuxMac OS Xgetpid において、2回目以降のシステムコール実施が抑止されるのは、先述したように何度実施しても返却値が変わらないためでしょう。正確には fork/vfork 契機でキャッシュされた情報を破棄する必要はあるでしょうが、実質的には「不変」とみなしても良い筈です。

システムコール実施自体のコスト」計測のために、確実にシステムコールを実施しようとするなら、「実施の都度返却値が異なる (or 副作用が生じる)」システムコールが必要ということになります。「システムコール実施コスト」以外の要因を極力排除するためには、更に「実施毎の処理が極少量」であることも必要でしょう。

とりあえず思い付いたのが gettimeofday/time でした。

実のところ、Solaristime システムコールの実装が、「高精度時刻情報を取得した上での秒部分取り出し」だったりするので、当初思い付いた 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 確定のため)

といったことが判明しました。

時刻情報のようなシステムワイドな情報を、システムコールを経由せずに取得する上記のような手法は、MachXNU 固有の流儀なのかもしれません。この辺は、いずれ折を見て、詳細を調査したいところです。

結果的に、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 実行時には「システムコール実施コスト」以上の処理要因が絡んでいることがわかります。つまり、元々計測対象としては不適切だったということですね。

以下、次回に続く。

備考

Linuxgettimeofday は、オンラインマニュアルのセクション表記通りに普通のシステムコールなのですが、Solarisgettimeofday は少々曲者です。

Solaris のオンラインマニュアルにおける gettimeofday のセクション表記が、ライブラリ関数を表す "3" (厳密には "3C") である一方、time のセクション表記は、システムコールを表す "2" となっています。

となれば、gettimeofday の実装が「time システムコール、あるいはその実現に使用されている内部システムコール」を使用していることが予想されるのですが、実際に確認してみると……これまたシステムコール実施が検出されません!

もしかして SolarisMac 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 呼び出しの延長で「システムコール実施」が検出されないのも納得ですね。