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

[インデックス 14683] ファイルの概要

このコミットは、Goランタイムにおける時刻取得の精度を向上させるための重要な変更です。具体的には、time.now および runtime.nanotime 関数が、従来のマイクロ秒精度からナノ秒精度で時刻を取得するように修正されました。これは、Linux (386, ARM)、FreeBSD (386, amd64, ARM)、NetBSD (386, amd64)、OpenBSD (386, amd64) といった複数のOSおよびアーキテクチャにおいて、gettimeofday システムコールから clock_gettime システムコールへの切り替えによって実現されています。Darwin (macOS) 環境では既にナノ秒精度が提供されていたため、この変更の対象外です。

この変更により、Goアプリケーションがより高精度な時刻情報を利用できるようになり、特に時間測定が重要なアプリケーションにおいてその恩恵を受けることができます。ただし、既存のコードがマイクロ秒精度を前提として外部ストレージに時刻を保存している場合、Go 1.1で取得したナノ秒精度の時刻を保存し、後で読み出す際に元の正確な時刻が復元できない可能性がある点に注意が必要です。

コミット

  • コミットハッシュ: 7777bac6e4570ffe485f736e79f0631a460171f4
  • 作者: Shenghou Ma minux.ma@gmail.com
  • コミット日時: 2012年12月18日 火曜日 22:57:25 +0800

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/7777bac6e4570ffe485f736e79f0631a460171f4

元コミット内容

runtime: use clock_gettime to get ns resolution for time.now & runtime.nanotime

For Linux/{386,arm}, FreeBSD/{386,amd64,arm}, NetBSD/{386,amd64}, OpenBSD/{386,amd64}.
Note: our Darwin implementation already has ns resolution.

Linux/386 (Core i7-2600 @ 3.40GHz, kernel 3.5.2-gentoo)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow          110          118   +7.27%

Linux/ARM (ARM Cortex-A8 @ 800MHz, kernel 2.6.32.28 android)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow          625          542  -13.28%

Linux/ARM (ARM Cortex-A9 @ 1GHz, Pandaboard)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow          992          909   -8.37%

FreeBSD 9-REL-p1/amd64 (Dell R610 Server with Xeon X5650 @ 2.67GHz)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow          699          695   -0.57%

FreeBSD 9-REL-p1/amd64 (Atom D525 @ 1.80GHz)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow         1553         1658   +6.76%

OpenBSD/amd64 (Dell E6410 with i5 CPU M 540 @ 2.53GHz)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow         1262         1236   -2.06%

OpenBSD/i386 (Asus eeePC 701 with Intel Celeron M 900MHz - locked to 631MHz)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow         5089         5043   -0.90%

NetBSD/i386 (VMware VM with Core i5 CPU @ 2.7GHz)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow          277          278   +0.36%

NetBSD/amd64 (VMware VM with Core i5 CPU @ 2.7Ghz)
benchmark       old ns/op    new ns/op    delta
BenchmarkNow          103          105   +1.94%

Thanks Maxim Khitrov, Joel Sing, and Dave Cheney for providing benchmark data.

R=jsing, dave, rsc
CC=golang-dev
https://golang.org/cl/6820120

変更の背景

Go言語のtimeパッケージやランタイム内部で時刻を取得する際、これまでは多くのUnix系システムでgettimeofdayシステムコールが使用されていました。このシステムコールは、通常マイクロ秒(10^-6秒)の精度で時刻を返します。しかし、現代のシステムではより高精度な時刻情報が求められることが多く、特にパフォーマンス測定やイベントの厳密な順序付けが必要な場面では、ナノ秒(10^-9秒)レベルの精度が望ましいとされていました。

このコミットの背景には、Goランタイムが提供する時刻情報の精度を向上させ、より現代的なシステム要件に対応するという目的があります。gettimeofdayはマイクロ秒精度が限界であるため、ナノ秒精度を提供できるclock_gettimeシステムコールへの移行が検討されました。これにより、Goアプリケーションがより正確な時間測定を行えるようになり、特に低レイテンシが求められるシステムや、高頻度で時刻を取得するようなシナリオでの精度が向上します。

コミットメッセージに含まれるベンチマーク結果は、この変更が様々な環境でどのようにパフォーマンスに影響するかを示しています。一部の環境ではオーバーヘッドが増加する可能性も示唆されていますが、全体としては精度向上というメリットが重視されています。

前提知識の解説

1. 時刻取得の精度とシステムコール

  • gettimeofday:
    • Unix系システムで広く使われているシステムコールで、現在の時刻を秒とマイクロ秒の単位で取得します。
    • 通常、システムクロック(リアルタイムクロック)に基づいています。
    • 精度はマイクロ秒(10^-6秒)が一般的です。
    • システム時刻の変更(NTP同期など)によって、時刻が前後する可能性があります。
  • clock_gettime:
    • POSIX標準で定義されているシステムコールで、より高精度な時刻情報を提供します。
    • 様々な種類のクロック(CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_IDなど)を指定して時刻を取得できます。
    • CLOCK_REALTIMEgettimeofdayと同様にリアルタイムクロックに基づきますが、ナノ秒(10^-9秒)の精度で時刻を返します。
    • CLOCK_MONOTONICはシステム起動時からの経過時間をナノ秒精度で提供し、システム時刻の変更に影響されません。これは、時間間隔の測定に適しています。
    • このコミットでは、CLOCK_REALTIMEを使用してナノ秒精度を実現しています。

2. ナノ秒とマイクロ秒

  • マイクロ秒 (microsecond, µs): 1秒の100万分の1 (10^-6秒)。
  • ナノ秒 (nanosecond, ns): 1秒の10億分の1 (10^-9秒)。
    • 1マイクロ秒は1000ナノ秒に相当します。
    • ナノ秒精度は、マイクロ秒精度よりも1000倍細かい時間分解能を提供します。

3. Go言語の時刻関連関数

  • time.Now(): Go言語の標準ライブラリtimeパッケージで提供される関数で、現在のローカル時刻をtime.Time型で返します。この関数は内部的にOSの時刻取得メカニズムを利用しています。
  • runtime.nanotime(): Goランタイム内部で使用される関数で、システム起動時からの経過時間をナノ秒単位で返します。これは主に、Goのスケジューラやプロファイラなど、ランタイムの内部処理で高精度な時間測定が必要な場合に使用されます。

4. アセンブリ言語 (.sファイル)

Go言語のランタイムは、パフォーマンスが要求される部分やOSとのインターフェース部分でアセンブリ言語を使用することがあります。特にシステムコールを直接呼び出すような処理は、各OSおよびアーキテクチャ固有のアセンブリコードで実装されることが多いです。

  • sys_freebsd_386.s, sys_linux_arm.s などは、特定のOS(FreeBSD, Linuxなど)とCPUアーキテクチャ(386, ARMなど)に対応するシステムコール呼び出しや低レベルな処理を記述したアセンブリファイルです。
  • これらのファイルでは、レジスタの操作やシステムコール番号の指定など、ハードウェアに近いレベルでのプログラミングが行われます。

5. VDSO (Virtual Dynamic Shared Object)

Linuxカーネルの機能の一つで、ユーザー空間のアプリケーションがシステムコールを呼び出す際に、カーネルモードへの切り替えを伴わずに一部のシステムコール(例: gettimeofday, clock_gettime)を高速に実行できるようにするメカニズムです。VDSOは、カーネルが提供する共有ライブラリのようなもので、アプリケーションのアドレス空間にマッピングされます。これにより、システムコール呼び出しのオーバーヘッドが削減され、時刻取得などの頻繁に呼び出される処理のパフォーマンスが向上します。

技術的詳細

このコミットの主要な技術的変更点は、Goランタイムが時刻を取得する際に使用するシステムコールを、gettimeofdayからclock_gettimeへ切り替えたことです。これにより、Goのtime.Now()関数や内部のruntime.nanotime()関数が、対応するOS(Linux, FreeBSD, NetBSD, OpenBSD)およびアーキテクチャにおいてナノ秒精度で時刻を返すようになります。

具体的な変更は、各OS/アーキテクチャに対応するアセンブリファイル(例: src/pkg/runtime/sys_linux_386.s)で行われています。

  1. システムコール番号の変更:
    • gettimeofdayに対応するシステムコール番号(例: Linux/386では78)から、clock_gettimeに対応するシステムコール番号(例: Linux/386では265)に変更されています。
    • FreeBSDでは116から232へ、NetBSDでは418から427へ変更されています。
  2. 引数の変更:
    • gettimeofdayは通常、timeval構造体(秒とマイクロ秒)とtimezone構造体(通常はNULL)を引数に取ります。
    • clock_gettimeは、クロックID(例: CLOCK_REALTIMEを示す0)とtimespec構造体(秒とナノ秒)を引数に取ります。アセンブリコードでは、これらの引数を適切にレジスタに設定しています。
  3. 戻り値の処理の変更:
    • gettimeofdayはマイクロ秒を返しますが、clock_gettimeはナノ秒を返します。
    • 以前のコードでは、マイクロ秒をナノ秒に変換するためにIMULL $1000, BX (386/ARM) や IMULQ $1000, DX (amd64) のような乗算命令を使用していました。このコミットでは、clock_gettimeが直接ナノ秒を返すため、これらの変換処理が不要となり、削除されています。これにより、余分な計算が省かれ、わずかながらパフォーマンスの改善にも寄与する可能性があります。
  4. doc/go1.1.htmlの更新:
    • Go 1.1のリリースノートに、timeパッケージの精度が向上したこと、および既存のコードがマイクロ秒精度を前提としている場合に互換性の問題が発生する可能性があることが追記されています。これは、外部ストレージに時刻を保存する際に、ナノ秒情報が失われる可能性があるためです。

ベンチマーク結果の分析

コミットメッセージには、様々な環境でのBenchmarkNowの結果が示されています。

  • Linux/ARM (Cortex-A8, Cortex-A9): 顕著なパフォーマンス改善(-13.28%, -8.37%)が見られます。これは、これらのアーキテクチャにおいてclock_gettimeの呼び出しがgettimeofdayよりも効率的であるか、またはマイクロ秒からナノ秒への変換処理がボトルネックになっていた可能性を示唆しています。
  • Linux/386: わずかなパフォーマンス低下(+7.27%)が見られます。これは、clock_gettimeのシステムコール呼び出しのオーバーヘッドが、特定の環境ではgettimeofdayよりも大きいことを示している可能性があります。
  • FreeBSD/amd64, OpenBSD/amd64, OpenBSD/i386, NetBSD/i386, NetBSD/amd64: ほとんどの環境でパフォーマンスの変化は微小(-0.57%から+6.76%の範囲)です。これは、システムコール自体のオーバーヘッドが大きく変わらないか、または他の要因が支配的であることを示唆しています。

全体として、この変更は時刻取得の精度向上を主目的としており、パフォーマンスへの影響は環境によって異なるものの、一部のARM環境では改善が見られるという結果になっています。

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

このコミットでは、Goランタイムの各OS/アーキテクチャ固有のアセンブリファイルにおいて、time.nowruntime.nanotimeの実装が変更されています。主要な変更は以下のファイルに見られます。

  • src/pkg/runtime/sys_freebsd_386.s
  • src/pkg/runtime/sys_freebsd_amd64.s
  • src/pkg/runtime/sys_freebsd_arm.s
  • src/pkg/runtime/sys_linux_386.s
  • src/pkg/runtime/sys_linux_arm.s
  • src/pkg/runtime/sys_netbsd_386.s
  • src/pkg/runtime/sys_netbsd_amd64.s
  • src/pkg/runtime/sys_openbsd_386.s
  • src/pkg/runtime/sys_openbsd_amd64.s

これらのファイルでは、主に以下の2つの変更が行われています。

  1. システムコール番号の変更: gettimeofdayに対応するシステムコール番号から、clock_gettimeに対応するシステムコール番号へ変更。
  2. マイクロ秒からナノ秒への変換処理の削除: gettimeofdayが返すマイクロ秒をナノ秒に変換するために行っていたIMULL $1000, BXIMULQ $1000, DXといった乗算命令が削除されています。これは、clock_gettimeが直接ナノ秒を返すため不要になったためです。

例として、src/pkg/runtime/sys_linux_386.sの変更点を見てみましょう。

変更前 (time.now):

TEXT time·now(SB), 7, $32
	MOVL	$78, AX			// syscall - gettimeofday
	LEAL	8(SP), BX
	MOVL	$0, CX
	MOVL	$0, DX
	CALL	*runtime·_vdso(SB)
	MOVL	8(SP), AX	// sec
	MOVL	12(SP), BX	// usec

	// sec is in AX, usec in BX
	MOVL	AX, sec+0(FP)
	MOVL	$0, sec+4(FP)
	IMULL	$1000, BX // <-- ここでマイクロ秒をナノ秒に変換
	MOVL	BX, nsec+8(FP)
	RET

変更後 (time.now):

TEXT time·now(SB), 7, $32
	MOVL	$265, AX		// syscall - clock_gettime
	MOVL	$0, BX
	LEAL	8(SP), CX
	MOVL	$0, DX
	CALL	*runtime·_vdso(SB)
	MOVL	8(SP), AX	// sec
	MOVL	12(SP), BX	// nsec // <-- 既にナノ秒

	// sec is in AX, nsec in BX
	MOVL	AX, sec+0(FP)
	MOVL	$0, sec+4(FP)
	// IMULL	$1000, BX // <-- 変換処理が削除
	MOVL	BX, nsec+8(FP)
	RET

同様の変更がruntime.nanotime関数にも適用されています。

コアとなるコードの解説

上記のコードスニペットは、GoランタイムがLinux/386アーキテクチャ上でtime.now関数(Goのtime.Now()の内部実装の一部)をどのように実装しているかを示しています。

変更前:

  1. MOVL $78, AX: AXレジスタにシステムコール番号78をロードします。Linux/386では、78gettimeofdayシステムコールに対応します。
  2. LEAL 8(SP), BX: スタックポインタSPから8バイトオフセットしたアドレスをBXレジスタにロードします。これはtimeval構造体のアドレスをシステムコールに渡すための準備です。
  3. MOVL $0, CX / MOVL $0, DX: timezone構造体のアドレスとして0(NULL)を渡します。
  4. CALL *runtime·_vdso(SB): VDSO経由でgettimeofdayシステムコールを呼び出します。
  5. MOVL 8(SP), AX / MOVL 12(SP), BX: システムコールから返されたtimeval構造体の内容を読み取ります。8(SP)には秒(sec)が、12(SP)にはマイクロ秒(usec)が格納されます。
  6. IMULL $1000, BX: BXレレジスタに格納されているマイクロ秒の値に1000を乗算し、ナノ秒に変換します。1マイクロ秒 = 1000ナノ秒であるためです。
  7. MOVL BX, nsec+8(FP): 変換されたナノ秒の値を、関数の戻り値であるnsec(ナノ秒)の領域に格納します。

変更後:

  1. MOVL $265, AX: AXレジスタにシステムコール番号265をロードします。Linux/386では、265clock_gettimeシステムコールに対応します。
  2. MOVL $0, BX: BXレジスタに0をロードします。これはclock_gettimeの第一引数であるclockid_tで、CLOCK_REALTIME(リアルタイムクロック)を指定します。
  3. LEAL 8(SP), CX: スタックポインタSPから8バイトオフセットしたアドレスをCXレジスタにロードします。これはtimespec構造体のアドレスをシステムコールに渡すための準備です。
  4. MOVL $0, DX: DXレジスタに0をロードします(未使用の引数)。
  5. CALL *runtime·_vdso(SB): VDSO経由でclock_gettimeシステムコールを呼び出します。
  6. MOVL 8(SP), AX / MOVL 12(SP), BX: システムコールから返されたtimespec構造体の内容を読み取ります。8(SP)には秒(sec)が、12(SP)にはナノ秒(nsec)が格納されます。
  7. IMULL $1000, BXの行が削除されています。これは、clock_gettimeが既にナノ秒単位で時刻を返しているため、変換が不要になったためです。
  8. MOVL BX, nsec+8(FP): BXレジスタに格納されているナノ秒の値を、関数の戻り値であるnsecの領域に直接格納します。

この変更により、GoランタイムはOSから直接ナノ秒精度の時刻を取得できるようになり、余分な変換処理が不要になりました。これにより、より正確な時刻情報がGoアプリケーションに提供されることになります。

関連リンク

参考にした情報源リンク