[インデックス 14367] ファイルの概要
このコミットは、GoランタイムがLinux/amd64システム上で時刻を取得する方法を改善するものです。具体的には、time.now
関数とruntime.nanotime
関数において、既存のgettimeofday
システムコールに代わり、vDSO (virtual Dynamic Shared Object) を介したclock_gettime
システムコールを利用するように変更しています。これにより、パフォーマンスの向上と、より高精度なナノ秒単位の時刻分解能の実現を目指しています。
コミット
commit 4022fc4e21d6c5feecb01248c25f8bc54e9762c2
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Fri Nov 9 14:19:07 2012 +0800
runtime: use vDSO clock_gettime for time.now & runtime.nanotime on Linux/amd64
Performance improvement aside, time.Now() now gets real nanosecond resolution
on supported systems.
Benchmark done on Core i7-2600 @ 3.40GHz with kernel 3.5.2-gentoo.
original vDSO gettimeofday:
BenchmarkNow 100000000 27.4 ns/op
new vDSO gettimeofday fallback:
BenchmarkNow 100000000 27.6 ns/op
new vDSO clock_gettime:
BenchmarkNow 100000000 24.4 ns/op
R=golang-dev, bradfitz, iant, iant
CC=golang-dev
https://golang.org/cl/6814103
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4022fc4e21d6c5feecb01248c25f8bc54e9762c2
元コミット内容
このコミットの目的は、Linux/amd64環境におけるGoの時刻取得メカニズムを最適化することです。具体的には、time.Now()
およびruntime.nanotime()
が内部的に使用するシステムコールを、gettimeofday
からclock_gettime
へ切り替えることで、パフォーマンスの向上と真のナノ秒分解能の実現を目指しています。
コミットメッセージには、Core i7-2600 (3.40GHz) とLinuxカーネル 3.5.2-gentoo上でのベンチマーク結果が示されています。
- 元のvDSO
gettimeofday
: 27.4 ns/op - 新しいvDSO
gettimeofday
フォールバック: 27.6 ns/op (これはclock_gettime
が利用できない場合のフォールバックパスの性能) - 新しいvDSO
clock_gettime
: 24.4 ns/op
この結果から、clock_gettime
を使用することで約3ナノ秒の性能改善が見られることがわかります。
変更の背景
時刻の取得は、多くのアプリケーション、特に高パフォーマンスが要求されるシステムや、正確な時間計測が必要な場面で頻繁に行われる操作です。従来のgettimeofday
システムコールはマイクロ秒単位の精度しか提供せず、ナノ秒単位の精度が必要な場合には不十分でした。また、システムコールはユーザー空間からカーネル空間へのコンテキストスイッチを伴うため、オーバーヘッドが発生します。
このコミットの背景には、以下の課題と目的があります。
- 精度向上:
time.Now()
が真のナノ秒分解能を提供できるようにすること。これは、より正確な時間計測や、ナノ秒単位でのイベントの順序付けが必要なアプリケーションにとって重要です。 - パフォーマンス改善: 時刻取得のオーバーヘッドを削減し、全体的なアプリケーションのパフォーマンスを向上させること。特に、頻繁に時刻を取得するようなシナリオでは、この改善が大きな影響を与えます。
- vDSOの活用: Linuxカーネルが提供するvDSOメカニズムを最大限に活用し、システムコールを介さずにユーザー空間から直接時刻情報を取得することで、コンテキストスイッチのコストを回避すること。
clock_gettime
は、gettimeofday
よりも新しいシステムコールであり、より高精度なタイムソース(例: TSC - Time Stamp Counter)を利用できるため、ナノ秒単位の精度を提供できます。この変更は、GoのランタイムがLinuxシステム上でより効率的かつ高精度に動作するための重要なステップと言えます。
前提知識の解説
1. システムコール (System Call)
システムコールは、ユーザー空間で動作するプログラムが、カーネル空間で提供されるサービス(ファイルI/O、メモリ管理、プロセス制御、時刻取得など)を利用するためのインターフェースです。プログラムがシステムコールを発行すると、CPUはユーザーモードからカーネルモードに切り替わり、カーネルが要求された処理を実行します。このモード切り替えにはオーバーヘッドが伴います。
2. gettimeofday
gettimeofday
は、Unix系システムで広く使われている時刻取得のためのシステムコールです。通常、エポック(1970年1月1日00:00:00 UTC)からの経過時間を秒とマイクロ秒(10^-6秒)で返します。この関数の精度はマイクロ秒までであり、ナノ秒単位の精度は提供しません。
3. clock_gettime
clock_gettime
は、POSIX標準で定義された時刻取得のためのシステムコールで、gettimeofday
よりも新しいものです。この関数は、様々なクロックID(例: CLOCK_REALTIME
、CLOCK_MONOTONIC
など)を指定でき、ナノ秒(10^-9秒)単位の精度で時刻を返します。高精度なタイムソースを利用できるため、より正確な時間計測が可能です。
4. vDSO (virtual Dynamic Shared Object)
vDSOは、Linuxカーネルが提供する最適化メカニズムの一つです。一部の頻繁に呼び出されるシステムコール(例: 時刻取得関連の関数)について、カーネルは対応するコードをユーザー空間にマッピングされた共有ライブラリとして提供します。これにより、プログラムはシステムコールを発行することなく、直接ユーザー空間からこれらの関数を呼び出すことができます。結果として、カーネルモードへのコンテキストスイッチのオーバーヘッドが回避され、パフォーマンスが大幅に向上します。
vDSOは、通常/proc/self/maps
などで確認できる[vdso]
というメモリ領域にマッピングされます。プログラムは、このvDSO領域内のシンボルを解決して関数を呼び出します。
5. Goのtime.Now()
とruntime.nanotime()
time.Now()
: Goの標準ライブラリtime
パッケージで提供される関数で、現在のローカル時刻をtime.Time
型で返します。内部的にはOSの時刻取得メカニズムを利用しています。runtime.nanotime()
: Goのランタイム内部で使用される関数で、システム起動時からの経過時間をナノ秒単位で返します。これは、プロファイリングや内部的な時間計測など、絶対時刻ではなく相対的な時間が必要な場合に使用されます。
これらの関数は、Goプログラムのパフォーマンスに直接影響を与えるため、その基盤となる時刻取得メカニズムの最適化は非常に重要です。
技術的詳細
このコミットの技術的詳細を理解するためには、Goのランタイムがどのようにシステムコールを呼び出し、vDSOシンボルを解決しているかを把握する必要があります。
Goのランタイムは、OS固有のアセンブリコード(sys_linux_amd64.s
など)とCコード(vdso_linux_amd64.c
など)を組み合わせて、低レベルの操作を実行します。
vDSOシンボルの解決
vdso_linux_amd64.c
ファイルは、vDSOシンボルを動的に解決する役割を担っています。Linuxカーネルは、vDSO領域に特定のシンボル(例: __vdso_gettimeofday
、__vdso_clock_gettime
)をエクスポートします。Goランタイムは、これらのシンボルを検索し、そのアドレスを内部変数(例: runtime·__vdso_gettimeofday_sym
、runtime·__vdso_clock_gettime_sym
)に格納します。これにより、Goのコードはこれらのアドレスを介してvDSO関数を直接呼び出すことができます。
このコミットでは、__vdso_clock_gettime
シンボルを新たに解決対象に追加しています。
時刻取得ロジックの変更
sys_linux_amd64.s
ファイルは、time.now
とruntime.nanotime
の実装を含むアセンブリコードです。
time.now
の変更点:
変更前は、time.now
は直接__vdso_gettimeofday
を呼び出していました。
変更後は、まずruntime·__vdso_clock_gettime_sym
がゼロでないか(つまり、__vdso_clock_gettime
シンボルが解決できたか)をチェックします。
__vdso_clock_gettime
が利用可能な場合:CLOCK_REALTIME
(0
) を第一引数 (DI
) に設定します。- 時刻情報を格納する構造体のアドレスを第二引数 (
SI
) に設定します。 runtime·__vdso_clock_gettime_sym
に格納されたアドレスを介して__vdso_clock_gettime
を呼び出します。- 返された秒とナノ秒をGoの
time.now
の戻り値にマッピングします。
__vdso_clock_gettime
が利用できない場合 (fallback_gtod
へジャンプ):- 従来通り
__vdso_gettimeofday
を呼び出します。 gettimeofday
はマイクロ秒を返すため、これをナノ秒に変換(IMULQ $1000, DX
)してからGoの戻り値にマッピングします。
- 従来通り
このフォールバックメカニズムにより、clock_gettime
が利用できない古いカーネルなどでもGoプログラムが正常に動作することが保証されます。
runtime.nanotime
の変更点:
変更前は、runtime.nanotime
もgettimeofday
に似た方法で時刻を取得し、それをナノ秒に変換していました。
変更後は、runtime.nanotime
がtime.now
を呼び出すように変更されています。time.now
が既にclock_gettime
またはgettimeofday
を介して高精度な時刻を取得するようになったため、nanotime
もその恩恵を受けることができます。time.now
が返す秒とナノ秒を組み合わせて、最終的なナノ秒単位の経過時間を計算します。
性能と精度の向上
- 性能: vDSOを介した
clock_gettime
は、システムコールを介さないため、コンテキストスイッチのオーバーヘッドがありません。これにより、ベンチマーク結果が示すように、時刻取得のレイテンシが削減されます。 - 精度:
clock_gettime
はナノ秒単位の精度を提供するため、time.Now()
が真のナノ秒分解能を持つようになります。これは、特に高頻度で時刻を計測するようなアプリケーションにおいて、より正確な結果をもたらします。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/pkg/runtime/sys_linux_amd64.s
src/pkg/runtime/vdso_linux_amd64.c
src/pkg/runtime/sys_linux_amd64.s
このファイルは、Linux/amd64アーキテクチャにおけるGoランタイムの低レベルなアセンブリコードを含んでいます。
--- a/src/pkg/runtime/sys_linux_amd64.s
+++ b/src/pkg/runtime/sys_linux_amd64.s
@@ -102,31 +102,37 @@ TEXT runtime·mincore(SB),7,$0-24
// func now() (sec int64, nsec int32)
TEXT time·now(SB), 7, $32
+\tMOVQ\truntime·__vdso_clock_gettime_sym(SB), AX
+\tCMPQ\tAX, $0
+\tJEQ\tfallback_gtod
+\tMOVL\t$0, DI // CLOCK_REALTIME
+\tLEAQ\t8(SP), SI
+\tCALL\tAX
+\tMOVQ\t8(SP), AX\t// sec
+\tMOVQ\t16(SP), DX\t// nsec
+\tMOVQ\tAX, sec+0(FP)\n+\tMOVL\tDX, nsec+8(FP)\n+\tRET
+fallback_gtod:
\tLEAQ\t8(SP), DI
\tMOVQ\t$0, SI
\tMOVQ\truntime·__vdso_gettimeofday_sym(SB), AX
\tCALL\tAX
\tMOVQ\t8(SP), AX\t// sec
\tMOVL\t16(SP), DX\t// usec
-\n-\t// sec is in AX, usec in DX
-\tMOVQ\tAX, sec+0(FP)\n \tIMULQ\t$1000, DX
+\tMOVQ\tAX, sec+0(FP)\n \tMOVL\tDX, nsec+8(FP)\n \tRET
TEXT runtime·nanotime(SB), 7, $32
-\tLEAQ\t8(SP), DI
-\tMOVQ\t$0, SI
-\tMOVQ\t$0xffffffffff600000, AX
-\tCALL\tAX
-\tMOVQ\t8(SP), AX\t// sec
-\tMOVL\t16(SP), DX\t// usec
+\tCALL\ttime·now(SB)
+\tMOVQ\t0(SP), AX\t// sec
+\tMOVL\t8(SP), DX\t// nsec
\t// sec is in AX, usec in DX
\t// return nsec in AX
\tIMULQ\t$1000000000, AX
-\tIMULQ\t$1000, DX
\tADDQ\tDX, AX
\tRET
src/pkg/runtime/vdso_linux_amd64.c
このファイルは、vDSOシンボルの初期化と解決を担当するCコードを含んでいます。
--- a/src/pkg/runtime/vdso_linux_amd64.c
+++ b/src/pkg/runtime/vdso_linux_amd64.c
@@ -161,11 +161,13 @@ static version_key linux26 = { (byte*)\"LINUX_2.6\", 0x3ae75f6 };
// initialize with vsyscall fallbacks
void* runtime·__vdso_time_sym = (void*)0xffffffffff600400ULL;
void* runtime·__vdso_gettimeofday_sym = (void*)0xffffffffff600000ULL;
+void* runtime·__vdso_clock_gettime_sym = (void*)0;
-#define SYM_KEYS_COUNT 2
+#define SYM_KEYS_COUNT 3
static symbol_key sym_keys[] = {\n \t{ (byte*)\"__vdso_time\", &runtime·__vdso_time_sym },\n \t{ (byte*)\"__vdso_gettimeofday\", &runtime·__vdso_gettimeofday_sym },\n+\t{ (byte*)\"__vdso_clock_gettime\", &runtime·__vdso_clock_gettime_sym },\n };
static void vdso_init_from_sysinfo_ehdr(struct vdso_info *vdso_info, Elf64_Ehdr* hdr) {
コアとなるコードの解説
src/pkg/runtime/sys_linux_amd64.s
の変更点
time·now
関数:runtime·__vdso_clock_gettime_sym
(vDSOclock_gettime
関数のアドレスを格納する変数) の値をAX
レジスタにロードします。AX
が0
(つまり、clock_gettime
が利用できない場合)であれば、fallback_gtod
ラベルにジャンプし、従来のgettimeofday
を使用するパスに進みます。clock_gettime
が利用可能な場合:DI
レジスタに0
を設定します。これはCLOCK_REALTIME
クロックIDに対応します。- スタック上の
8(SP)
(秒とナノ秒を格納する構造体のアドレス)をSI
レジスタにロードします。 AX
に格納されたclock_gettime
のアドレスを介して関数を呼び出します (CALL AX
)。- 呼び出し後、スタックから秒 (
8(SP)
) とナノ秒 (16(SP)
) を取得し、それぞれsec+0(FP)
とnsec+8(FP)
(Goの戻り値)に格納してRET
(リターン)します。
fallback_gtod
パスでは、gettimeofday
を呼び出し、返されたマイクロ秒をナノ秒に変換するためにIMULQ $1000, DX
を実行します。
runtime·nanotime
関数:- 以前は直接時刻取得ロジックを持っていましたが、変更後は
time·now(SB)
を呼び出すように簡略化されました。 time·now
が返す秒とナノ秒をスタックから取得し、秒をナノ秒に変換(IMULQ $1000000000, AX
)して、ナノ秒部分と加算 (ADDQ DX, AX
) することで、最終的なナノ秒単位の経過時間を計算して返します。
- 以前は直接時刻取得ロジックを持っていましたが、変更後は
src/pkg/runtime/vdso_linux_amd64.c
の変更点
runtime·__vdso_clock_gettime_sym
という新しいグローバル変数が追加され、初期値として0
が設定されています。この変数は、vDSOの__vdso_clock_gettime
関数のアドレスを格納するために使用されます。SYM_KEYS_COUNT
マクロが2
から3
に更新され、シンボルキーの配列sym_keys
に__vdso_clock_gettime
が追加されました。これにより、Goランタイムの初期化時に__vdso_clock_gettime
シンボルもvDSOから解決されるようになります。
これらの変更により、GoランタイムはLinux/amd64システム上で、より高精度で高速なclock_gettime
を優先的に使用し、互換性のためにgettimeofday
へのフォールバックパスも維持するようになりました。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
- Goのコミット: https://golang.org/cl/6814103
参考にした情報源リンク
- vDSO (virtual Dynamic Shared Object) について:
gettimeofday
manページ: https://man7.org/linux/man-pages/man2/gettimeofday.2.htmlclock_gettime
manページ: https://man7.org/linux/man-pages/man3/clock_gettime.3.html- Goのアセンブリについて: https://go.dev/doc/asm
- Goのランタイムについて: https://go.dev/doc/articles/go_mem (メモリ管理に関する記事だが、ランタイムの低レベルな側面を理解するのに役立つ)