Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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で現在時刻(エポックからの秒数とマイクロ秒数)を取得するための標準的なシステムコールです。通常、ユーザー空間のアプリケーションがこの関数を呼び出すと、カーネル空間に処理が移行し、カーネルが時刻情報を取得してユーザー空間に返します。このカーネルとユーザー空間間のコンテキストスイッチは、ある程度のオーバーヘッドを伴います。

vsyscallvDSO (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関数は、これらの値を使用して、以下の手順で現在時刻を計算します。

  1. 世代カウンタのチェック: gtod_generationnt_generationを読み取り、データの一貫性を確認するためのループに入ります。これは、Comm Pageのデータが非同期に更新されるため、読み取り中にデータが変更される可能性があるためです。
  2. TSCの読み取り: RDTSC (Read Time-Stamp Counter) 命令を使用して、CPUのタイムスタンプカウンタの現在値を読み取ります。TSCは、CPUが起動してからのサイクル数をカウントするレジスタです。
  3. データの一貫性再確認: 再度世代カウンタをチェックし、読み取り中にデータが変更されていないことを確認します。変更されていた場合は、ループの最初に戻り、再試行します。
  4. 時刻の計算: 以下の式に基づいて、ナノ秒単位の現在時刻を計算します。 ((tsc - nt_tsc_base) * nt_scale) >> 32 + nt_ns_base - gtod_ns_base + gtod_sec_base * 1e9 この計算は、TSCの差分をスケール値で乗算し、シフトすることでナノ秒に変換し、さらにベース値を加算・減算して絶対的なナノ秒時刻を導き出します。>> 32は、96ビット積の上位64ビットを抽出するための操作です。
  5. システムコールへのフォールバック: もし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の変更点

  1. Comm Pageベースアドレスのロード: MOVQ $0x7fffffe00000, BP これは、OS XのComm PageのベースアドレスをBPレジスタにロードしています。このアドレスは、Comm Pageがメモリにマッピングされている場所を示します。

  2. 一貫性スナップショットループ (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_generationnt_generationは世代カウンタであり、Comm Pageのデータが更新されるたびにインクリメントされます。
    • ループの開始時と終了時にこれらのカウンタを比較することで、読み取り中にデータが変更されていないことを確認します。もし変更されていた場合(JNE timeloop)、ループを再開して再試行します。
    • RDTSC命令は、CPUのタイムスタンプカウンタ (TSC) の現在値をEDX:EAXレジスタペア(64ビット値)に読み込みます。
    • その後、nt_tsc_basent_scalent_ns_basegtod_ns_basegtod_sec_baseといった時刻計算に必要なオフセット値をComm Pageから読み込み、それぞれR10R11R12R13R14レジスタに格納します。
  3. 時刻計算: 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, DXADDQ 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: 関数から戻ります。
  4. システムコールへのフォールバック (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·nowruntime·nanotimeが返すナノ秒単位の時刻を、秒とナノ秒に分解して返します。この分解処理は、Goコンパイラによって生成される最適化されたコードに似たアセンブリコードで行われます。

関連リンク

参考にした情報源リンク