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

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

このコミットは、GoランタイムがPlan 9オペレーティングシステム上で時刻を取得する方法を変更するものです。具体的には、従来の/dev/bintimeデバイスファイルからの読み取りに代わり、nsecシステムコールを使用するように修正されています。これにより、時刻取得の効率性と正確性が向上します。

コミット

  • コミットハッシュ: a84e3ad198387019aaef6e979e46e498600ea12f
  • 作者: Aram Hăvărneanu aram@mgk.ro
  • コミット日時: 2014年7月9日 水曜日 12:33:42 +0200

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

https://github.com/golang/go/commit/a84e3ad198387019aaef6e979e46e498600ea12f

元コミット内容

runtime: use the nsec system call instead of /dev/bintime on Plan 9

LGTM=0intro
R=0intro
CC=ality, dave, golang-codereviews, jas, mischief, rsc
https://golang.org/cl/104570043

変更の背景

この変更の背景には、Plan 9オペレーティングシステムにおける時刻取得メカニズムの進化と、Goランタイムのパフォーマンスおよび堅牢性の向上が挙げられます。

従来のPlan 9では、/dev/bintimeというデバイスファイルを通じてバイナリ形式の時刻情報を取得していました。Goランタイムのruntime·nanotime関数も、このファイルを開いて読み取ることでナノ秒単位の時刻を取得していました。しかし、このファイルベースのアプローチにはいくつかの課題がありました。特に、プロセスがフォーク(複製)される際にファイルディスクリプタの管理が複雑になり、エラーが発生しやすくなるという問題がありました。元のコードのコメントにも「As long as all goroutines share the same file descriptor table we can get away with using just a static fd. Without a lock the file can be opened twice but that's okay.」とあり、静的なファイルディスクリプタを使用する上での考慮事項が示されています。また、パフォーマンス面でも、/dev/bintimeからの読み取りは、特に頻繁に時刻を取得するようなシナリオにおいて、オーバーヘッドが大きくなる可能性がありました。元のコメントでは「Using /dev/bintime gives us a latency on the order of ten microseconds between two calls.」とあり、さらに「The naïve implementation (without the cached file descriptor) is roughly four times slower in 9vx on a 2.16 GHz Intel Core 2 Duo.」と、パフォーマンスの懸念が具体的に示されています。

これに対し、nsecシステムコールは、より直接的かつ効率的にナノ秒単位の時刻情報を提供する現代的な方法です。システムコールとして実装されているため、ファイルI/Oのオーバーヘッドを回避でき、ファイルディスクリプタ管理の複雑さも解消されます。この変更は、GoランタイムがPlan 9上でより高速かつ安定して動作するために不可欠でした。

また、os_plan9.c内のruntime·nanotime関数には// TODO(aram): remove hack after I fix _nsec in the pc64 kernel.というコメントがあり、これはnsecシステムコール自体が当時のpc64カーネルでまだ完全に最適化されていなかった可能性を示唆しています。このコミットは、その時点での最善の解決策としてnsecシステムコールへの移行を選択しつつ、将来的なカーネル側の改善を見越していたと考えられます。

前提知識の解説

Plan 9オペレーティングシステム

Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。その設計哲学は「すべてがファイルである」という原則に基づいています。これは、システム内のあらゆるリソース(プロセス、ネットワーク接続、デバイスなど)がファイルシステム上のファイルとして表現され、標準的なファイルI/O操作(読み書き、オープン、クローズなど)を通じてアクセスできることを意味します。これにより、システム管理とプログラミングが簡素化されることを目指しています。

/dev/bintime

Plan 9における/dev/bintimeは、システム時刻をバイナリ形式で提供する特殊なデバイスファイルです。プログラムはこのファイルを開き、その内容を読み取ることで、現在の時刻情報を取得していました。これは「すべてがファイルである」というPlan 9の哲学を体現する一例ですが、前述の通り、ファイルディスクリプタの管理やパフォーマンスの面で課題がありました。

nsecシステムコール

nsecは、Plan 9オペレーティングシステムが提供するシステムコールの一つで、Unixエポック(1970年1月1日00:00:00 UTC)からの経過時間をナノ秒単位で返します。システムコールは、ユーザー空間のプログラムがカーネル空間の機能に直接アクセスするためのメカニズムであり、ファイルI/Oを介するよりも低オーバーヘッドで高速な処理が可能です。nsecシステムコールは、より高精度かつ効率的な時刻取得のために導入されました。

Goランタイム

Goランタイムは、Go言語で書かれたプログラムの実行を管理するソフトウェアコンポーネントです。これには、ガベージコレクション、スケジューリング、メモリ管理、そしてシステムコールへのインターフェースなどが含まれます。runtimeパッケージは、Goプログラムがオペレーティングシステムと対話するための低レベルな機能を提供し、runtime·nanotimeのような関数は、OS固有の時刻取得メカニズムを抽象化してGoプログラムに提供します。

アセンブリ言語 (386/amd64)

アセンブリ言語は、コンピュータのプロセッサが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。386(Intel 80386)およびamd64(x86-64)は、それぞれ32ビットおよび64ビットのx86アーキテクチャを指します。オペレーティングシステムのカーネルやランタイムのような低レベルのソフトウェアでは、特定のシステムコールを呼び出すためや、パフォーマンスが非常に重要な部分で、アセンブリ言語が直接使用されることがあります。このコミットでは、nsecシステムコールを呼び出すためのアセンブリコードが追加されています。

技術的詳細

このコミットの主要な技術的変更は、Goランタイムのruntime·nanotime関数が、Plan 9上で時刻を取得するメカニズムを/dev/bintimeからのファイル読み取りからnsecシステムコールへの直接呼び出しに切り替えた点です。

変更前 (/dev/bintime方式): 変更前のruntime·nanotime関数は、静的なファイルディスクリプタfdを使用して/dev/bintimeを開き、そこから8バイトのバイナリデータを読み取っていました。この8バイトは、上位4バイトが時刻の「上位部分」、下位4バイトが「下位部分」を表し、これらを組み合わせて64ビットのナノ秒単位の時刻を構築していました。 この方式の課題は、ファイルI/Oのオーバーヘッドと、ファイルディスクリプタの管理(特にマルチスレッド環境やフォークを伴うプロセスにおいて)の複雑さにありました。コメントにもあるように、2回の呼び出し間で約10マイクロ秒のレイテンシがあり、キャッシュされていないファイルディスクリプタを使用するとパフォーマンスが著しく低下する可能性がありました。

変更後 (nsecシステムコール方式): 変更後のruntime·nanotime関数は、新たに導入されたruntime·nsec関数を呼び出すようになりました。runtime·nsecは、Plan 9のnsecシステムコールを直接呼び出すためのGoランタイムのラッパー関数です。このシステムコールは、ナノ秒単位の時刻を直接返します。 この変更により、ファイルI/Oのオーバーヘッドが排除され、時刻取得のレイテンシが大幅に削減されます。また、ファイルディスクリプタの管理に関する複雑さも解消されます。

runtime·nsecの実装: runtime·nsec関数は、src/pkg/runtime/os_plan9.hで宣言され、src/pkg/runtime/sys_plan9_386.ssrc/pkg/runtime/sys_plan9_amd64.sにそれぞれ32ビット(x86)および64ビット(x86-64)アーキテクチャ向けのアセンブリ実装が追加されました。

  • sys_plan9_386.s (32ビット): TEXT runtime·nsec(SB),NOSPLIT,$0で関数が定義され、MOVL $53, AXでシステムコール番号53(nsecシステムコールに対応)をAXレジスタにロードし、INT $64でシステムコールを呼び出しています。これはPlan 9におけるシステムコール呼び出しの一般的なパターンです。返された値はAXレジスタに格納され、それが関数の戻り値となります。また、CMPL AX, $-1でエラーチェックを行い、エラーの場合はscratch引数にエラーコードを格納する処理も含まれています。

  • sys_plan9_amd64.s (64ビット): TEXT runtime·nsec(SB),NOSPLIT,$0で関数が定義され、MOVQ $53, BPでシステムコール番号53をBPレジスタにロードし、SYSCALL命令でシステムコールを呼び出しています。64ビットアーキテクチャでは、システムコール呼び出しにSYSCALL命令が使用されるのが一般的です。

一時的なハック (TODOコメント): os_plan9.cruntime·nanotime関数には、// TODO(aram): remove hack after I fix _nsec in the pc64 kernel.というコメントがあります。これは、nsecシステムコールがpc64カーネルでまだ完全に機能していない、または特定の条件下で0を返す可能性があるため、一時的にscratch引数に格納された値を使用するという回避策が取られていることを示しています。これは、このコミットがnsecシステムコールへの移行を促進しつつも、当時のPlan 9カーネルの特定の制約に対応していたことを示唆しています。

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

このコミットで変更された主要なファイルとコードブロックは以下の通りです。

  1. src/pkg/runtime/os_plan9.c:

    • runtime·nanotime関数の実装が完全に書き換えられました。
    • /dev/bintimeを開くためのfd変数、ファイル読み取り、バイト操作のロジックが削除されました。
    • 代わりに、runtime·nsec(&scratch)を呼び出し、その戻り値を使用するようになりました。
    • 一時的なハックとして、ns == 0の場合にscratchの値を返すロジックが追加されました。
  2. src/pkg/runtime/os_plan9.h:

    • runtime·nsec関数のプロトタイプ宣言が追加されました: int64 runtime·nsec(int64*);
  3. src/pkg/runtime/sys_plan9_386.s:

    • runtime·nsecという新しいアセンブリ関数が追加されました。
    • この関数は、システムコール番号53(nsec)をAXレジスタに設定し、INT $64命令でシステムコールを呼び出します。
    • エラーチェックとscratch引数への値の格納ロジックが含まれています。
  4. src/pkg/runtime/sys_plan9_amd64.s:

    • runtime·nsecという新しいアセンブリ関数が追加されました。
    • この関数は、システムコール番号53をBPレジスタに設定し、SYSCALL命令でシステムコールを呼び出します。

コアとなるコードの解説

src/pkg/runtime/os_plan9.c の変更

--- a/src/pkg/runtime/os_plan9.c
+++ b/src/pkg/runtime/os_plan9.c
@@ -150,29 +150,13 @@ runtime·usleep(uint32 µs)
 int64
 runtime·nanotime(void)
 {
-	static int32 fd = -1;
-	byte b[8];
-	uint32 hi, lo;
-
-	// As long as all goroutines share the same file
-	// descriptor table we can get away with using
-	// just a static fd.  Without a lock the file can
-	// be opened twice but that's okay.
-	//
-	// Using /dev/bintime gives us a latency on the
-	// order of ten microseconds between two calls.
-	//
-	// The naïve implementation (without the cached
-	// file descriptor) is roughly four times slower
-	// in 9vx on a 2.16 GHz Intel Core 2 Duo.
-
-	if(fd < 0 && (fd = runtime·open("/dev/bintime", OREAD|OCEXEC, 0)) < 0)
-		return 0;
-	if(runtime·pread(fd, b, sizeof b, 0) != sizeof b)
-		return 0;
-	hi = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];
-	lo = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];
-	return (int64)hi<<32 | (int64)lo;
+	int64 ns, scratch;
+
+	ns = runtime·nsec(&scratch);
+	// TODO(aram): remove hack after I fix _nsec in the pc64 kernel.
+	if(ns == 0)
+		return scratch;
+	return ns;
 }

この変更は、runtime·nanotime関数が時刻を取得するメカニズムを根本的に変えています。

  • 削除されたコードは、/dev/bintimeファイルを開き、そこから8バイトのバイナリデータを読み取り、それを64ビットのナノ秒値に変換するロジックでした。これには、ファイルディスクリプタのキャッシュや、パフォーマンスに関するコメントが含まれていました。
  • 新しいコードでは、runtime·nsec(&scratch)という新しい関数を呼び出しています。この関数は、nsecシステムコールを直接呼び出すためのGoランタイムのラッパーです。
  • if(ns == 0) return scratch;という行は、nsecシステムコールが何らかの理由で0を返した場合(おそらくエラーを示す)に、scratch変数に格納された代替値(おそらくエラーコードや、別の方法で取得された時刻情報)を返すという一時的な回避策です。これは、TODOコメントが示すように、当時のPlan 9 pc64カーネルにおけるnsecシステムコールの挙動に関する既知の問題に対応するためのものです。

src/pkg/runtime/os_plan9.h の変更

--- a/src/pkg/runtime/os_plan9.h
+++ b/src/pkg/runtime/os_plan9.h
@@ -15,6 +15,7 @@ int32	runtime·plan9_tsemacquire(uint32 *addr, int32 ms);
 int32 	runtime·plan9_semrelease(uint32 *addr, int32 count);
 int32	runtime·notify(void (*fn)(void*, int8*));
 int32	runtime·noted(int32);
+int64	runtime·nsec(int64*);
 void	runtime·sigtramp(void*, int8*);
 void	runtime·sigpanic(void);
 void	runtime·goexitsall(int8*);

この変更は、runtime·nsec関数のプロトタイプ宣言を追加しています。これにより、他のCファイルやGoのランタイムコードからこの関数を呼び出すことができるようになります。int64 runtime·nsec(int64*);は、int64型の戻り値を持ち、int64型へのポインタを引数として受け取ることを示しています。このポインタは、前述のscratch変数として使用されます。

src/pkg/runtime/sys_plan9_386.s の変更

--- a/src/pkg/runtime/sys_plan9_386.s
+++ b/src/pkg/runtime/sys_plan9_386.s
@@ -64,6 +64,16 @@ TEXT runtime·plan9_tsemacquire(SB),NOSPLIT,$0
 	INT	$64
 	RET
 
+TEXT runtime·nsec(SB),NOSPLIT,$0
+\tMOVL\t$53, AX
+\tINT\t$64
+\tCMPL\tAX, $-1
+\tJNE\t4(PC)
+\tMOVL\ta+0(FP), CX
+\tMOVL\tAX, 0(CX)
+\tMOVL\tAX, 4(CX)
+\tRET
+\
 TEXT runtime·notify(SB),NOSPLIT,$0
 	MOVL	$28, AX
 	INT	$64

このアセンブリコードは、32ビットx86アーキテクチャ(386)向けのruntime·nsec関数の実装です。

  • TEXT runtime·nsec(SB),NOSPLIT,$0: runtime·nsecという名前の関数を定義しています。NOSPLITはスタックフレームを分割しないことを示し、$0はローカル変数に0バイトを割り当てることを示します。
  • MOVL $53, AX: システムコール番号53(Plan 9のnsecシステムコールに対応)をAXレジスタに移動します。システムコール番号は通常AXレジスタに渡されます。
  • INT $64: 割り込み番号64を発生させ、Plan 9カーネルにシステムコールを要求します。
  • CMPL AX, $-1: システムコールからの戻り値(AXレジスタに格納されている)が-1(エラーを示す一般的な値)と等しいかどうかを比較します。
  • JNE 4(PC): AX-1でない場合(エラーでない場合)、現在のプログラムカウンタから4バイト先にジャンプします。これは、エラー処理をスキップして関数を終了することを意味します。
  • MOVL a+0(FP), CX: 関数の最初の引数(scratchポインタ)をCXレジスタにロードします。a+0(FP)は、フレームポインタFPからのオフセットで引数にアクセスするGoアセンブリの記法です。
  • MOVL AX, 0(CX): AXレジスタの値(システムコールの戻り値、エラーコード)をCXが指すメモリ位置(scratch変数)に格納します。
  • MOVL AX, 4(CX): 32ビットシステムでは、64ビット値を格納するために2つの32ビットレジスタを使用することがあります。ここでは、AXの値をscratch変数の上位4バイトにも格納している可能性があります。これは、nsecが64ビット値を返すため、そのエラーコードを64ビットのscratch変数全体にコピーしていると考えられます。
  • RET: 関数から戻ります。

src/pkg/runtime/sys_plan9_amd64.s の変更

--- a/src/pkg/runtime/sys_plan9_amd64.s
+++ b/src/pkg/runtime/sys_plan9_amd64.s
@@ -77,6 +77,11 @@ TEXT runtime·plan9_tsemacquire(SB),NOSPLIT,$0
 	SYSCALL
 	RET
 
+TEXT runtime·nsec(SB),NOSPLIT,$0
+\tMOVQ\t$53, BP
+\tSYSCALL
+\tRET
+\
 TEXT runtime·notify(SB),NOSPLIT,$0
 	MOVQ	$28, BP
 	SYSCALL

このアセンブリコードは、64ビットx86アーキテクチャ(amd64)向けのruntime·nsec関数の実装です。

  • TEXT runtime·nsec(SB),NOSPLIT,$0: runtime·nsecという名前の関数を定義しています。
  • MOVQ $53, BP: システムコール番号53をBPレジスタに移動します。64ビットシステムコールでは、システムコール番号を特定のレジスタ(この場合はBP)に渡すのが一般的です。
  • SYSCALL: SYSCALL命令を実行し、カーネルにシステムコールを要求します。
  • RET: 関数から戻ります。64ビットシステムコールでは、戻り値は通常AXレジスタに格納されますが、このアセンブリコードでは明示的なMOVQ命令は記述されていません。これは、Goのアセンブリリンカが自動的に戻り値を処理するか、またはnsecシステムコールが直接AXレジスタに結果を書き込むためと考えられます。

関連リンク

参考にした情報源リンク