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

[インデックス 14353] runtime: use vDSO for gettimeofday on linux/amd64

コミット

コミットハッシュ: 024a92c1da0b5e147df01ed184fe8d655af62900 Author: Anthony Martin ality@pbrane.org Date: Wed Nov 7 18:29:31 2012 -0800

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

https://github.com/golang/go/commit/024a92c1da0b5e147df01ed184fe8d655af62900

元コミット内容

runtime: use vDSO for gettimeofday on linux/amd64

Intel Core 2 Duo (2.16 GHz) running 3.6.5-1-ARCH

benchmark       old ns/op    new ns/op    delta
BenchmarkNow         1856         1034  -44.29%

R=rsc
CC=golang-dev
https://golang.org/cl/6826072

変更の背景

このコミットは、Goランタイムにおけるgettimeofdayシステムコールのパフォーマンスを向上させることを目的としています。gettimeofdayは、現在の時刻(秒とマイクロ秒)を取得するために頻繁に呼び出される関数であり、その性能はアプリケーション全体の応答性に大きな影響を与えます。

従来のシステムコールは、ユーザー空間からカーネル空間へのコンテキストスイッチを伴うため、オーバーヘッドが発生します。特に、時刻取得のような頻繁に実行される操作では、このオーバーヘッドが累積してパフォーマンスのボトルネックとなる可能性があります。

コミットメッセージに示されているベンチマーク結果(BenchmarkNowが1856 ns/opから1034 ns/opへ、44.29%の改善)は、この変更がGoプログラムの時刻取得処理において顕著な速度向上をもたらすことを明確に示しています。この改善は、特に高頻度で時刻情報を必要とするアプリケーションや、多数のゴルーチンが同時に動作するような環境で、より高いスループットと低いレイテンシを実現するのに貢献します。

前提知識の解説

システムコールとコンテキストスイッチ

オペレーティングシステム(OS)は、ハードウェアリソースへのアクセスや、ファイル操作、ネットワーク通信、時刻取得といった特権的な操作を管理します。ユーザープログラムがこれらの操作を行うためには、OSが提供する「システムコール」を呼び出す必要があります。

システムコールが呼び出されると、CPUはユーザーモードからカーネルモードへと実行コンテキストを切り替えます。この切り替えは「コンテキストスイッチ」と呼ばれ、CPUのレジスタの状態保存や復元、メモリ管理ユニット(MMU)の切り替えなど、一定のオーバーヘッドを伴います。頻繁なシステムコールは、このコンテキストスイッチのオーバーヘッドが積み重なり、パフォーマンスの低下を招く可能性があります。

gettimeofday

gettimeofdayは、Unix系OSで広く使われているシステムコールの一つで、現在の時刻を秒とマイクロ秒の精度で取得します。多くのプログラミング言語やライブラリが内部的にこのシステムコールを利用して、時刻関連の機能を提供しています。

vDSO (virtual Dynamic Shared Object)

vDSOは、Linuxカーネルが提供する最適化メカニズムの一つです。これは、特定の頻繁に呼び出されるシステムコール(例: gettimeofday, clock_gettime, getcpuなど)を、カーネル空間へのコンテキストスイッチなしにユーザー空間で直接実行できるようにするものです。

vDSOは、カーネルが起動時に生成し、各プロセスの仮想アドレス空間にマッピングする共有ライブラリのようなものです。このライブラリには、時刻情報など、カーネルがユーザー空間に公開しても安全なデータや、簡単な計算で結果が得られる関数の実装が含まれています。これにより、ユーザープログラムはシステムコールを呼び出す代わりに、vDSOが提供する関数を通常の関数呼び出しのように実行でき、コンテキストスイッチのオーバーヘッドを回避してパフォーマンスを向上させることができます。

vDSOが利用できない場合や、何らかの理由でvDSO経由での実行が失敗した場合は、自動的に従来のシステムコールにフォールバックする仕組みが備わっています。

技術的詳細

このコミットの核心は、GoランタイムがLinux/amd64環境でgettimeofdayを呼び出す際に、従来のシステムコールではなくvDSOを利用するように変更した点にあります。

Goのtime.now関数は、内部的にOSの時刻取得メカニズムを利用しています。Linux/amd64の場合、これは通常gettimeofdayシステムコールにマッピングされます。

変更前は、time.now関数内でgettimeofdayを呼び出す際に、特定の固定アドレス0xffffffffff600000を直接使用していました。このアドレスは、LinuxカーネルがvDSOをマッピングする一般的なベースアドレスの一つですが、これはハードコードされた値であり、vDSOのシンボル解決を適切に行っているわけではありませんでした。

変更後は、runtime·__vdso_gettimeofday_sym(SB)というシンボルを参照するように修正されています。これは、GoランタイムがvDSOのgettimeofday関数のエントリポイントを動的に解決し、そのアドレスをAXレジスタにロードしてからCALL命令で呼び出すことを意味します。

vDSOによるgettimeofdayの動作は以下のようになります。

  1. ユーザー空間での実行: gettimeofdayの呼び出しは、カーネルへのトラップ(システムコール)を発生させず、ユーザー空間にマッピングされたvDSOページ内のコードを実行します。
  2. 時刻データの取得: vDSO内のコードは、カーネルがユーザー空間に公開している共有メモリ領域から、現在の時刻に関する情報(例: タイムスタンプカウンタのオフセット、周波数など)を読み取ります。
  3. rdtscの利用: 多くの場合、vDSOの時刻取得関数は、CPUのrdtsc(Read Time-Stamp Counter)命令を利用します。rdtscは、CPUが起動してからのサイクル数を非常に高速に取得できる命令です。vDSOは、このrdtscの値と、カーネルから提供されるオフセットや周波数情報を用いて、現在の壁時計時刻を計算します。
  4. コンテキストスイッチの回避: このプロセス全体がユーザー空間で完結するため、カーネルモードへのコンテキストスイッチが不要となり、大幅なパフォーマンス向上が実現されます。

この変更により、GoプログラムはLinux/amd64環境において、より効率的に時刻情報を取得できるようになり、特に高負荷な状況下でのパフォーマンスが改善されます。

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

変更はsrc/pkg/runtime/sys_linux_amd64.sファイル内のtime·now関数の実装にあります。

--- a/src/pkg/runtime/sys_linux_amd64.s
+++ b/src/pkg/runtime/sys_linux_amd64.s
@@ -104,7 +104,7 @@ TEXT runtime·mincore(SB),7,$0-24
 TEXT time·now(SB), 7, $32
  	LEAQ	8(SP), DI
  	MOVQ	$0, SI
-	MOVQ	$0xffffffffff600000, AX
+	MOVQ	runtime·__vdso_gettimeofday_sym(SB), AX
  	CALL	AX
  	MOVQ	8(SP), AX	// sec
  	MOVL	16(SP), DX	// usec

具体的には、107行目のMOVQ命令が変更されています。

  • 変更前: MOVQ $0xffffffffff600000, AX
  • 変更後: MOVQ runtime·__vdso_gettimeofday_sym(SB), AX

コアとなるコードの解説

このアセンブリコードスニペットは、Goランタイムのtime.now関数の一部であり、Linux/amd64アーキテクチャにおける時刻取得ロジックを実装しています。

  1. LEAQ 8(SP), DI:

    • SPはスタックポインタレジスタです。8(SP)は、スタックポインタから8バイトオフセットしたアドレスを指します。
    • LEAQ (Load Effective Address Quadword) 命令は、オペランドのアドレスを計算し、その結果をDIレジスタにロードします。
    • これは、gettimeofdayシステムコール(またはvDSO関数)の最初の引数(tv構造体へのポインタ)を準備していると考えられます。tv構造体は、秒とマイクロ秒を格納するために使用されます。
  2. MOVQ $0, SI:

    • MOVQ (Move Quadword) 命令は、即値0SIレジスタにロードします。
    • これは、gettimeofdayシステムコールの2番目の引数(tz構造体へのポインタ、タイムゾーン情報)をNULLに設定していると考えられます。
  3. 変更された行:

    • 変更前 (MOVQ $0xffffffffff600000, AX):
      • 0xffffffffff600000は、LinuxカーネルがvDSOをマッピングする一般的な仮想アドレスの一つです。
      • この命令は、gettimeofday関数のアドレスがこの固定値であると仮定し、そのアドレスをAXレジスタに直接ロードしていました。これは、vDSOのシンボル解決を動的に行わず、ハードコードされたアドレスに依存するアプローチでした。この方法は、カーネルのバージョンや設定によってはvDSOのアドレスが異なる場合に問題を引き起こす可能性がありました。
    • 変更後 (MOVQ runtime·__vdso_gettimeofday_sym(SB), AX):
      • runtime·__vdso_gettimeofday_sym(SB)は、Goリンカが解決するシンボルです。Goランタイムは、プログラムの起動時にvDSOのシンボルテーブルを解析し、__vdso_gettimeofday関数の実際のアドレスを特定します。
      • この命令は、特定された__vdso_gettimeofday関数のアドレスをAXレジスタにロードします。これにより、ハードコードされたアドレスに依存することなく、動的に解決された正確なvDSO関数のエントリポイントを呼び出すことが可能になります。
  4. CALL AX:

    • AXレジスタにロードされたアドレス(vDSOのgettimeofday関数のエントリポイント)に対して関数呼び出しを行います。これにより、ユーザー空間内で高速に時刻情報が取得されます。
  5. MOVQ 8(SP), AX // secMOVL 16(SP), DX // usec:

    • CALL AXの実行後、gettimeofday関数は結果をスタック上のtv構造体に書き込みます。
    • これらの命令は、スタックから秒(sec)とマイクロ秒(usec)の値を取得し、それぞれAXDXレジスタにロードしています。これらの値は、Goのtime.now関数の戻り値として使用されます。

この変更により、GoランタイムはLinux/amd64システムにおいて、より堅牢かつパフォーマンスの高い方法でvDSOを利用し、時刻取得の効率を大幅に向上させています。

関連リンク

参考にした情報源リンク