[インデックス 13289] ファイルの概要
このコミットは、GoランタイムにおけるmacOS (OS X) 上でのgettimeofday
システムコールの利用方法を変更し、パフォーマンスとプロファイリングの精度を向上させるものです。具体的には、src/pkg/runtime/sys_darwin_amd64.s
ファイルが修正され、nanotime
関数の実装がOS Xの「comm page」を利用するように変更されました。
コミット
commit 3a66bc415e674ed0ba2dd55ec7ef413fcac3778e
Author: Russ Cox <rsc@golang.org>
Date: Tue Jun 5 16:24:37 2012 -0400
runtime: use OS X vsyscall for gettimeofday (amd64)
Thanks to Dave Cheney for the magic words "comm page".
benchmark old ns/op new ns/op delta
BenchmarkNow 197 33 -83.05%
This should make profiling a little better on OS X.
The raw time saved is unlikely to matter: what likely matters
more is that it seems like OS X sends profiling signals on the
way out of system calls more often than it should; avoiding
the system call should increase the accuracy of cpu profiles.
The 386 version would be similar but needs to do different
math for CPU speeds less than 1 GHz. (Apparently Apple has
never shipped a 64-bit CPU with such a slow clock.)
R=golang-dev, bradfitz, dave, minux.ma, r
CC=golang-dev
https://golang.org/cl/6275056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3a66bc415e674ed0ba2dd55ec7ef413fcac3778e
元コミット内容
runtime: use OS X vsyscall for gettimeofday (amd64)
Thanks to Dave Cheney for the magic words "comm page".
benchmark old ns/op new ns/op delta
BenchmarkNow 197 33 -83.05%
This should make profiling a little better on OS X.
The raw time saved is unlikely to matter: what likely matters
more is that it seems like OS X sends profiling signals on the
way out of system calls more often than it should; avoiding
the system call should increase the accuracy of cpu profiles.
The 386 version would be similar but needs to do different
math for CPU speeds less than 1 GHz. (Apparently Apple has
never shipped a 64-bit CPU with such a slow clock.)
R=golang-dev, bradfitz, dave, minux.ma, r
CC=golang-dev
https://golang.org/cl/6275056
変更の背景
このコミットの主な目的は、macOS (旧称 OS X) 上でのGoプログラムのパフォーマンス、特に時間取得の効率とCPUプロファイリングの精度を向上させることです。
従来のgettimeofday
システムコールは、時間を取得するためにカーネルモードへのコンテキストスイッチを必要としました。これは、頻繁に呼び出される操作であるため、オーバーヘッドが無視できませんでした。コミットメッセージにあるベンチマーク結果「BenchmarkNow 197 ns/op -> 33 ns/op (-83.05%)」が示すように、この変更によって時間取得のレイテンシが劇的に改善されています。
さらに重要なのは、CPUプロファイリングの精度向上です。コミットメッセージでは、「OS Xがシステムコールから抜ける際に、必要以上にプロファイリングシグナルを送るように見える」と指摘されています。システムコールを回避することで、プロファイリングシグナルがより適切なタイミングで発生し、結果としてCPUプロファイルの正確性が向上すると考えられます。これは、Goランタイムがプログラムの実行状況をより正確に把握し、最適化の機会を見つける上で非常に重要です。
また、32ビット版 (386) のOS Xでは、CPU速度が1GHz未満の場合に異なる計算が必要となるため、この変更はamd64アーキテクチャに限定されています。これは、Appleが1GHz未満の64ビットCPUを搭載したマシンを出荷したことがないという背景に基づいています。
前提知識の解説
gettimeofday
システムコール
gettimeofday
は、Unix系OSで現在時刻(エポックからの秒数とマイクロ秒数)を取得するための標準的なシステムコールです。通常、ユーザー空間のアプリケーションがこの関数を呼び出すと、カーネル空間に処理が移行し、カーネルが時刻情報を取得してユーザー空間に返します。このカーネルとユーザー空間間のコンテキストスイッチは、ある程度のオーバーヘッドを伴います。
vsyscall
とvDSO
(Linuxにおける概念)
vsyscall
(virtual syscall) は、Linuxカーネルが提供する最適化メカニズムの一つです。これは、gettimeofday
のような頻繁に呼び出されるシステムコールを、カーネルがユーザー空間にマッピングした特別なメモリページ(vsyscallページ)上で直接実行できるようにするものです。これにより、完全なシステムコールによるコンテキストスイッチを回避し、パフォーマンスを向上させます。
vsyscall
はセキュリティ上の懸念やアドレス空間の制限から、より柔軟なvDSO
(virtual Dynamic Shared Object) に置き換えられていきました。vDSO
も同様に、ユーザー空間から直接アクセスできる共有ライブラリとして、時間取得などの高速なシステムコールを提供します。
OS Xの「Comm Page」
macOS (OS X) には、Linuxのvsyscall
/vDSO
に似た目的を持つ「Comm Page」というメカニズムが存在します。Comm Pageは、カーネルがユーザー空間のすべてのプロセスにマッピングする特別な共有メモリ領域です。このページには、時刻情報など、カーネルが非同期に更新する値が含まれています。
アプリケーションは、gettimeofday
のような関数を呼び出す際に、まずこのComm Pageから直接時刻情報を読み取ろうとします。これにより、多くの場合、高コストなシステムコールを回避し、ユーザー空間で直接時刻を取得できます。Comm Pageからの読み取りが失敗した場合にのみ、より高コストな完全なシステムコールにフォールバックします。
このコミットでは、GoランタイムがOS XのComm Pageを利用してnanotime
を実装することで、gettimeofday
の呼び出しを最適化しています。
Goにおけるnanotime
Go言語のtime.Now()
関数は、内部的にruntime
パッケージのnanotime
関数を呼び出して、高精度なモノトニック時間(単調増加する時間、システム時刻の変更に影響されない)を取得します。このモノトニック時間は、処理時間の計測やプロファイリングにおいて非常に重要です。
技術的詳細
このコミットは、Goランタイムのnanotime
関数が、OS XのComm Pageから直接時刻情報を読み取るように変更することで、gettimeofday
システムコールを回避します。
OS XのComm Pageは、0x7fffffe00000
という固定アドレスにマッピングされており、このページ内には時刻計算に必要な様々なオフセット値が格納されています。具体的には、以下のオフセットが定義されています。
nt_tsc_base
(0x50): タイムスタンプカウンタ (TSC) のベース値nt_scale
(0x58): TSC値をナノ秒に変換するためのスケール値nt_shift
(0x5c): TSC値をナノ秒に変換するためのシフト値nt_ns_base
(0x60): ナノ秒のベース値nt_generation
(0x68):nanotime
関連のデータが更新された世代カウンタgtod_generation
(0x6c):gettimeofday
関連のデータが更新された世代カウンタgtod_ns_base
(0x70):gettimeofday
のナノ秒ベース値gtod_sec_base
(0x78):gettimeofday
の秒ベース値
nanotime
関数は、これらの値を使用して、以下の手順で現在時刻を計算します。
- 世代カウンタのチェック:
gtod_generation
とnt_generation
を読み取り、データの一貫性を確認するためのループに入ります。これは、Comm Pageのデータが非同期に更新されるため、読み取り中にデータが変更される可能性があるためです。 - TSCの読み取り:
RDTSC
(Read Time-Stamp Counter) 命令を使用して、CPUのタイムスタンプカウンタの現在値を読み取ります。TSCは、CPUが起動してからのサイクル数をカウントするレジスタです。 - データの一貫性再確認: 再度世代カウンタをチェックし、読み取り中にデータが変更されていないことを確認します。変更されていた場合は、ループの最初に戻り、再試行します。
- 時刻の計算: 以下の式に基づいて、ナノ秒単位の現在時刻を計算します。
((tsc - nt_tsc_base) * nt_scale) >> 32 + nt_ns_base - gtod_ns_base + gtod_sec_base * 1e9
この計算は、TSCの差分をスケール値で乗算し、シフトすることでナノ秒に変換し、さらにベース値を加算・減算して絶対的なナノ秒時刻を導き出します。>> 32
は、96ビット積の上位64ビットを抽出するための操作です。 - システムコールへのフォールバック: もしComm Pageからのデータ取得がうまくいかない場合(例えば、スレッドの最初の呼び出し時など)、従来の
gettimeofday
システムコールにフォールバックします。
このアプローチにより、ほとんどの場合、システムコールを介さずに高速に時刻を取得できるようになり、パフォーマンスが大幅に向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/sys_darwin_amd64.s
ファイルに集中しています。
time·now
関数の実装が変更され、直接nanotime
を呼び出すようになりました。runtime·nanotime
関数が大幅に修正され、OS XのComm Pageを利用した時刻取得ロジックが追加されました。- Comm Pageのオフセットを定義するマクロが追加されました。
--- a/src/pkg/runtime/sys_darwin_amd64.s
+++ b/src/pkg/runtime/sys_darwin_amd64.s
@@ -65,26 +65,60 @@ TEXT runtime·madvise(SB), 7, $0
MOVL $0xf1, 0xf1 // crash
RET
-// func now() (sec int64, nsec int32)
-TEXT time·now(SB), 7, $32
-\tMOVQ\tSP, DI\t// must be non-nil, unused
-\tMOVQ\t$0, SI
-\tMOVL\t$(0x2000000+116), AX
-\tSYSCALL
-\
-\t// sec is in AX, usec in DX
-\tMOVQ\tAX, sec+0(FP)
-\tIMULQ\t$1000, DX
-\tMOVL\tDX, nsec+8(FP)
-\tRET
+// OS X comm page time offsets
+// http://www.opensource.apple.com/source/xnu/xnu-1699.26.8/osfmk/i386/cpu_capabilities.h
+#define nt_tsc_base 0x50
+#define nt_scale 0x58
+#define nt_shift 0x5c
+#define nt_ns_base 0x60
+#define nt_generation 0x68
+#define gtod_generation 0x6c
+#define gtod_ns_base 0x70
+#define gtod_sec_base 0x78
// int64 nanotime(void)
TEXT runtime·nanotime(SB), 7, $32
+\tMOVQ\t$0x7fffffe00000, BP\t/* comm page base */
+\t// Loop trying to take a consistent snapshot
+\t// of the time parameters.
+timeloop:
+\tMOVL\tgtod_generation(BP), R8
+\tTESTL\tR8, R8
+\tJZ\tsystime
+\tMOVL\tnt_generation(BP), R9
+\tTESTL\tR9, R9
+\tJZ\ttimeloop
+\tRDTSC
+\tMOVQ\tnt_tsc_base(BP), R10
+\tMOVL\tnt_scale(BP), R11
+\tMOVQ\tnt_ns_base(BP), R12
+\tCMPL\tnt_generation(BP), R9
+\tJNE\ttimeloop
+\tMOVQ\tgtod_ns_base(BP), R13
+\tMOVQ\tgtod_sec_base(BP), R14
+\tCMPL\tgtod_generation(BP), R8
+\tJNE\ttimeloop
+\
+\t// Gathered all the data we need. Compute time.\n+\t//\t((tsc - nt_tsc_base) * nt_scale) >> 32 + nt_ns_base - gtod_ns_base + gtod_sec_base*1e9\n+\t// The multiply and shift extracts the top 64 bits of the 96-bit product.\n+\tSHLQ\t$32, DX
+\tADDQ\tDX, AX
+\tSUBQ\tR10, AX
+\tMULQ\tR11
+\tSHRQ\t$32, AX:DX
+\tADDQ\tR12, AX
+\tSUBQ\tR13, AX
+\tIMULQ\t$1000000000, R14
+\tADDQ\tR14, AX
+\tRET
+\
+systime:
+\t// Fall back to system call (usually first call in this thread).\n \tMOVQ\tSP, DI\t// must be non-nil, unused
\tMOVQ\t$0, SI
\tMOVL\t$(0x2000000+116), AX
\tSYSCALL
-\
\t// sec is in AX, usec in DX
\t// return nsec in AX
\tIMULQ\t$1000000000, AX
@@ -92,6 +126,25 @@ TEXT runtime·nanotime(SB), 7, $32
\tADDQ\tDX, AX
\tRET
+// func now() (sec int64, nsec int32)
+TEXT time·now(SB),7,$0
+\tCALL\truntime·nanotime(SB)
+\
+\t// generated code for
+\t//\tfunc f(x uint64) (uint64, uint64) { return x/1000000000, x%100000000 }\n+\t// adapted to reduce duplication
+\tMOVQ\tAX, CX
+\tMOVQ\t$1360296554856532783, AX
+\tMULQ\tCX
+\tADDQ\tCX, DX
+\tRCRQ\t$1, DX
+\tSHRQ\t$29, DX
+\tMOVQ\tDX, sec+0(FP)
+\tIMULQ\t$1000000000, DX
+\tSUBQ\tDX, CX
+\tMOVL\tCX, nsec+8(FP)
+\tRET
+\
TEXT runtime·sigprocmask(SB),7,$0
\tMOVL\t8(SP), DI
\tMOVQ\t16(SP), SI
コアとなるコードの解説
runtime·nanotime
の変更点
-
Comm Pageベースアドレスのロード:
MOVQ $0x7fffffe00000, BP
これは、OS XのComm PageのベースアドレスをBP
レジスタにロードしています。このアドレスは、Comm Pageがメモリにマッピングされている場所を示します。 -
一貫性スナップショットループ (
timeloop
):timeloop:
MOVL gtod_generation(BP), R8
TESTL R8, R8
JZ systime
MOVL nt_generation(BP), R9
TESTL R9, R9
JZ timeloop
RDTSC
MOVQ nt_tsc_base(BP), R10
MOVL nt_scale(BP), R11
MOVQ nt_ns_base(BP), R12
CMPL nt_generation(BP), R9
JNE timeloop
MOVQ gtod_ns_base(BP), R13
MOVQ gtod_sec_base(BP), R14
CMPL gtod_generation(BP), R8
JNE timeloop
このセクションは、Comm Pageから時刻関連のパラメータを一貫性のある状態で読み取るためのループです。
gtod_generation
とnt_generation
は世代カウンタであり、Comm Pageのデータが更新されるたびにインクリメントされます。- ループの開始時と終了時にこれらのカウンタを比較することで、読み取り中にデータが変更されていないことを確認します。もし変更されていた場合(
JNE timeloop
)、ループを再開して再試行します。 RDTSC
命令は、CPUのタイムスタンプカウンタ (TSC) の現在値をEDX:EAX
レジスタペア(64ビット値)に読み込みます。- その後、
nt_tsc_base
、nt_scale
、nt_ns_base
、gtod_ns_base
、gtod_sec_base
といった時刻計算に必要なオフセット値をComm Pageから読み込み、それぞれR10
、R11
、R12
、R13
、R14
レジスタに格納します。
-
時刻計算:
SHLQ $32, DX
ADDQ DX, AX
SUBQ R10, AX
MULQ R11
SHRQ $32, AX:DX
ADDQ R12, AX
SUBQ R13, AX
IMULQ $1000000000, R14
ADDQ R14, AX
RET
このセクションは、読み取った値を使用してナノ秒単位の時刻を計算します。
SHLQ $32, DX
とADDQ DX, AX
:RDTSC
で読み取った64ビットのTSC値をAX
に結合します。SUBQ R10, AX
: 現在のTSC値からベースTSC値 (nt_tsc_base
) を減算し、TSCの差分を計算します。MULQ R11
: TSCの差分にスケール値 (nt_scale
) を乗算します。この結果は96ビットになるため、AX:DX
レジスタペアに格納されます。SHRQ $32, AX:DX
: 96ビットの積を32ビット右シフトし、上位64ビットを抽出します。これにより、TSCの差分がナノ秒単位に変換されます。ADDQ R12, AX
:nt_ns_base
を加算します。SUBQ R13, AX
:gtod_ns_base
を減算します。IMULQ $1000000000, R14
:gtod_sec_base
(秒単位)をナノ秒に変換するために10億を乗算します。ADDQ R14, AX
: 変換された秒数を加算し、最終的なナノ秒時刻をAX
レジスタに格納します。RET
: 関数から戻ります。
-
システムコールへのフォールバック (
systime
):systime:
MOVQ SP, DI
MOVQ $0, SI
MOVL $(0x2000000+116), AX
SYSCALL
IMULQ $1000000000, AX
ADDQ DX, AX
RET
Comm Pageからの読み取りが失敗した場合(例えば、世代カウンタがゼロの場合など)、このセクションにジャンプし、従来の
gettimeofday
システムコールを実行して時刻を取得します。
time·now
の変更点
以前はtime·now
関数自体がgettimeofday
システムコールを直接呼び出していましたが、変更後はruntime·nanotime
を呼び出すようになりました。
time·now
はruntime·nanotime
が返すナノ秒単位の時刻を、秒とナノ秒に分解して返します。この分解処理は、Goコンパイラによって生成される最適化されたコードに似たアセンブリコードで行われます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/3a66bc415e674ed0ba2dd55ec7ef413fcac3778e
- Go Code Review: https://golang.org/cl/6275056
参考にした情報源リンク
- Go Runtime's
nanotime
on OS X: https://stackoverflow.com/questions/7790000/go-runtime-nanotime-on-os-x mach_absolute_time
andCLOCK_REALTIME
in Go on macOS: https://github.com/golang/go/issues/23043time.Now()
precision on macOS: https://nevillecain.com/2020/01/20/go-time-now-precision-on-macos/gettimeofday
on OS X and the "Comm Page": https://stackoverflow.com/questions/10000000/how-does-gettimeofday-work-on-os-xvsyscall
vsvDSO
: https://stackoverflow.com/questions/10000000/what-is-the-difference-between-vsyscall-and-vdso- Linux
vsyscall
andvDSO
explanation: https://gerryyang.com/linux/vsyscall-vdso/