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

[インデックス 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システムコールはマイクロ秒単位の精度しか提供せず、ナノ秒単位の精度が必要な場合には不十分でした。また、システムコールはユーザー空間からカーネル空間へのコンテキストスイッチを伴うため、オーバーヘッドが発生します。

このコミットの背景には、以下の課題と目的があります。

  1. 精度向上: time.Now()が真のナノ秒分解能を提供できるようにすること。これは、より正確な時間計測や、ナノ秒単位でのイベントの順序付けが必要なアプリケーションにとって重要です。
  2. パフォーマンス改善: 時刻取得のオーバーヘッドを削減し、全体的なアプリケーションのパフォーマンスを向上させること。特に、頻繁に時刻を取得するようなシナリオでは、この改善が大きな影響を与えます。
  3. 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_REALTIMECLOCK_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_symruntime·__vdso_clock_gettime_sym)に格納します。これにより、Goのコードはこれらのアドレスを介してvDSO関数を直接呼び出すことができます。

このコミットでは、__vdso_clock_gettimeシンボルを新たに解決対象に追加しています。

時刻取得ロジックの変更

sys_linux_amd64.sファイルは、time.nowruntime.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.nanotimegettimeofdayに似た方法で時刻を取得し、それをナノ秒に変換していました。 変更後は、runtime.nanotimetime.nowを呼び出すように変更されています。time.nowが既にclock_gettimeまたはgettimeofdayを介して高精度な時刻を取得するようになったため、nanotimeもその恩恵を受けることができます。time.nowが返す秒とナノ秒を組み合わせて、最終的なナノ秒単位の経過時間を計算します。

性能と精度の向上

  • 性能: vDSOを介したclock_gettimeは、システムコールを介さないため、コンテキストスイッチのオーバーヘッドがありません。これにより、ベンチマーク結果が示すように、時刻取得のレイテンシが削減されます。
  • 精度: clock_gettimeはナノ秒単位の精度を提供するため、time.Now()が真のナノ秒分解能を持つようになります。これは、特に高頻度で時刻を計測するようなアプリケーションにおいて、より正確な結果をもたらします。

コアとなるコードの変更箇所

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/runtime/sys_linux_amd64.s
  2. 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 (vDSO clock_gettime関数のアドレスを格納する変数) の値をAXレジスタにロードします。
    • AX0(つまり、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へのフォールバックパスも維持するようになりました。

関連リンク

参考にした情報源リンク