[インデックス 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_REALTIME
はgettimeofday
と同様にリアルタイムクロックに基づきますが、ナノ秒(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
)で行われています。
- システムコール番号の変更:
gettimeofday
に対応するシステムコール番号(例: Linux/386では78
)から、clock_gettime
に対応するシステムコール番号(例: Linux/386では265
)に変更されています。- FreeBSDでは
116
から232
へ、NetBSDでは418
から427
へ変更されています。
- 引数の変更:
gettimeofday
は通常、timeval
構造体(秒とマイクロ秒)とtimezone
構造体(通常はNULL)を引数に取ります。clock_gettime
は、クロックID(例:CLOCK_REALTIME
を示す0
)とtimespec
構造体(秒とナノ秒)を引数に取ります。アセンブリコードでは、これらの引数を適切にレジスタに設定しています。
- 戻り値の処理の変更:
gettimeofday
はマイクロ秒を返しますが、clock_gettime
はナノ秒を返します。- 以前のコードでは、マイクロ秒をナノ秒に変換するために
IMULL $1000, BX
(386/ARM) やIMULQ $1000, DX
(amd64) のような乗算命令を使用していました。このコミットでは、clock_gettime
が直接ナノ秒を返すため、これらの変換処理が不要となり、削除されています。これにより、余分な計算が省かれ、わずかながらパフォーマンスの改善にも寄与する可能性があります。
doc/go1.1.html
の更新:- Go 1.1のリリースノートに、
time
パッケージの精度が向上したこと、および既存のコードがマイクロ秒精度を前提としている場合に互換性の問題が発生する可能性があることが追記されています。これは、外部ストレージに時刻を保存する際に、ナノ秒情報が失われる可能性があるためです。
- Go 1.1のリリースノートに、
ベンチマーク結果の分析
コミットメッセージには、様々な環境での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.now
とruntime.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つの変更が行われています。
- システムコール番号の変更:
gettimeofday
に対応するシステムコール番号から、clock_gettime
に対応するシステムコール番号へ変更。 - マイクロ秒からナノ秒への変換処理の削除:
gettimeofday
が返すマイクロ秒をナノ秒に変換するために行っていたIMULL $1000, BX
やIMULQ $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()
の内部実装の一部)をどのように実装しているかを示しています。
変更前:
MOVL $78, AX
:AX
レジスタにシステムコール番号78
をロードします。Linux/386では、78
はgettimeofday
システムコールに対応します。LEAL 8(SP), BX
: スタックポインタSP
から8バイトオフセットしたアドレスをBX
レジスタにロードします。これはtimeval
構造体のアドレスをシステムコールに渡すための準備です。MOVL $0, CX
/MOVL $0, DX
:timezone
構造体のアドレスとして0
(NULL)を渡します。CALL *runtime·_vdso(SB)
: VDSO経由でgettimeofday
システムコールを呼び出します。MOVL 8(SP), AX
/MOVL 12(SP), BX
: システムコールから返されたtimeval
構造体の内容を読み取ります。8(SP)
には秒(sec
)が、12(SP)
にはマイクロ秒(usec
)が格納されます。IMULL $1000, BX
:BX
レレジスタに格納されているマイクロ秒の値に1000
を乗算し、ナノ秒に変換します。1マイクロ秒 = 1000ナノ秒
であるためです。MOVL BX, nsec+8(FP)
: 変換されたナノ秒の値を、関数の戻り値であるnsec
(ナノ秒)の領域に格納します。
変更後:
MOVL $265, AX
:AX
レジスタにシステムコール番号265
をロードします。Linux/386では、265
はclock_gettime
システムコールに対応します。MOVL $0, BX
:BX
レジスタに0
をロードします。これはclock_gettime
の第一引数であるclockid_t
で、CLOCK_REALTIME
(リアルタイムクロック)を指定します。LEAL 8(SP), CX
: スタックポインタSP
から8バイトオフセットしたアドレスをCX
レジスタにロードします。これはtimespec
構造体のアドレスをシステムコールに渡すための準備です。MOVL $0, DX
:DX
レジスタに0
をロードします(未使用の引数)。CALL *runtime·_vdso(SB)
: VDSO経由でclock_gettime
システムコールを呼び出します。MOVL 8(SP), AX
/MOVL 12(SP), BX
: システムコールから返されたtimespec
構造体の内容を読み取ります。8(SP)
には秒(sec
)が、12(SP)
にはナノ秒(nsec
)が格納されます。IMULL $1000, BX
の行が削除されています。これは、clock_gettime
が既にナノ秒単位で時刻を返しているため、変換が不要になったためです。MOVL BX, nsec+8(FP)
:BX
レジスタに格納されているナノ秒の値を、関数の戻り値であるnsec
の領域に直接格納します。
この変更により、GoランタイムはOSから直接ナノ秒精度の時刻を取得できるようになり、余分な変換処理が不要になりました。これにより、より正確な時刻情報がGoアプリケーションに提供されることになります。
関連リンク
- Go CL 6820120: https://golang.org/cl/6820120
参考にした情報源リンク
gettimeofday
man page: https://man7.org/linux/man-pages/man2/gettimeofday.2.htmlclock_gettime
man page: https://man7.org/linux/man-pages/man2/clock_gettime.2.html- VDSO (Virtual Dynamic Shared Object) on Linux: https://lwn.net/Articles/446528/
- Go Assembly Language (Go Asm): https://go.dev/doc/asm
- Go 1.1 Release Notes (time package section): https://go.dev/doc/go1.1#time (このコミットで更新されたドキュメントの最終版)