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

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

このコミットは、Go言語のtimeパッケージにおけるNanoseconds関数のパフォーマンス改善を目的としています。具体的には、Goランタイムがメモリ割り当てなしで現在の時刻(ナノ秒単位)を取得できるように、基盤となるシステムコール呼び出しとアセンブリコードが最適化されています。これにより、高頻度で時刻を取得する際のオーバーヘッドが削減され、全体的なパフォーマンスが向上します。

コミット

  • コミットハッシュ: f437331f80b05944e8f15b2f81429729101a9455
  • 作者: Russ Cox rsc@golang.org
  • 日付: 2011年11月3日 木曜日 17:35:28 -0400

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

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

元コミット内容

time: faster Nanoseconds call

runtime knows how to get the time of day
without allocating memory.

R=golang-dev, dsymonds, dave, hectorchu, r, cw
CC=golang-dev
https://golang.org/cl/5297078

変更の背景

Go言語のtimeパッケージは、プログラムが現在時刻を取得するための機能を提供します。特にNanoseconds()関数は、Unixエポック(1970年1月1日00:00:00 UTC)からの経過時間をナノ秒単位で返します。この関数は、パフォーマンス測定、ログ記録、タイムスタンプの生成など、多くの場面で頻繁に呼び出される可能性があります。

このコミット以前のNanoseconds()の実装は、内部的にruntime·gettimeという関数を呼び出していました。このgettime関数は、秒とマイクロ秒を別々の引数として受け取り、それらをナノ秒に変換する際にメモリ割り当てが発生する可能性がありました。高頻度で時刻を取得するようなアプリケーションでは、この小さなメモリ割り当てがGC(ガベージコレクション)の頻度を増やし、全体的なパフォーマンスに悪影響を与えることが懸念されていました。

このコミットの目的は、Nanoseconds()の呼び出しを高速化し、特にメモリ割り当てを排除することにありました。Goランタイムが直接、メモリ割り当てなしでナノ秒単位の時刻を取得できるようにすることで、より効率的な時刻取得メカニズムを提供し、Goプログラムのパフォーマンスを向上させることを目指しています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。

1. システムコール (System Call)

システムコールは、ユーザー空間で動作するプログラムが、カーネル空間で提供されるOSのサービス(ファイルI/O、メモリ管理、プロセス管理、時刻取得など)を利用するためのインターフェースです。時刻取得もOSの機能であるため、通常はシステムコールを介して行われます。システムコールは、ユーザーモードからカーネルモードへのコンテキストスイッチを伴うため、ある程度のオーバーヘッドがあります。

2. 時刻取得のメカニズム

オペレーティングシステムは、様々な方法で時刻情報を提供します。

  • gettimeofday (Unix系): 秒とマイクロ秒の精度で現在の時刻を返します。通常、システムコールを介してアクセスされます。
  • QueryPerformanceCounter / QueryPerformanceFrequency (Windows): 高分解能のパフォーマンスカウンタを提供し、CPUサイクルに基づいた非常に正確な時間測定を可能にします。これは、壁時計時間ではなく、経過時間を測定するのに適しています。
  • GetSystemTimeAsFileTime (Windows): 1601年1月1日からの100ナノ秒単位の時間を返します。

3. アセンブリ言語 (Assembly Language)

Goランタイムの低レベルな部分は、C言語とアセンブリ言語で記述されています。特に、OSのシステムコールを直接呼び出す部分や、パフォーマンスがクリティカルな部分はアセンブリ言語で実装されることが多いです。このコミットでは、各OSおよびアーキテクチャ(386, amd64, armなど)固有のアセンブリコードが変更されています。アセンブリコードは、CPUのレジスタ操作やメモリへの直接アクセスを伴い、非常に低レベルな最適化を可能にします。

4. Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するコンポーネントです。ガベージコレクション、スケジューリング、メモリ管理、システムコールインターフェースなど、Go言語の並行性モデルと効率的な実行を支える多くの機能を提供します。runtimeパッケージは、これらのランタイム機能への低レベルなアクセスを提供します。

5. メモリ割り当てとガベージコレクション (Memory Allocation and Garbage Collection)

Goはガベージコレクタを持つ言語であり、不要になったメモリを自動的に解放します。しかし、頻繁なメモリ割り当てはガベージコレクタの作業量を増やし、プログラムの実行を一時停止させる(ストップ・ザ・ワールド)可能性があります。そのため、パフォーマンスが重要な部分では、可能な限りメモリ割り当てを避けることが望ましいとされます。

6. time.goc ファイル

Goのソースコードには、.goファイルの他に.gocファイルが存在することがあります。これらはGoのC言語との連携(cgo)や、ランタイムの低レベルな実装のために使われる特殊なファイルです。Goのツールチェーンによってコンパイル時に処理されます。

技術的詳細

このコミットの主要な技術的変更点は、timeパッケージのNanoseconds()関数が、Goランタイム内の新しいruntime·nanotime()関数を直接呼び出すように変更されたことです。そして、このruntime·nanotime()関数が、各OSおよびアーキテクチャに特化したアセンブリコードで実装され、メモリ割り当てなしで高精度な時刻を取得するように最適化されています。

runtime·gettime から runtime·nanotime への移行

以前は、runtime·gettime(int64 *sec, int32 *usec)という関数が秒とマイクロ秒をポインタ経由で返し、呼び出し側でナノ秒に変換していました。このポインタ渡しがメモリ割り当てを誘発する可能性がありました。

新しいruntime·nanotime(void)関数は、直接int64型のナノ秒値を返すように設計されています。これにより、関数呼び出しのインターフェースが簡素化され、メモリ割り当てが不要になります。

各OS/アーキテクチャでの実装

runtime·nanotimeの実装は、OSとCPUアーキテクチャによって異なりますが、共通の目的は「メモリ割り当てなしでナノ秒を取得する」ことです。

  • Unix系 (Darwin, FreeBSD, Linux, OpenBSD) の 386/amd64 アーキテクチャ:

    • これらのシステムでは、主にgettimeofdayシステムコールが使用されます。
    • アセンブリコード内でgettimeofdayを呼び出し、返された秒(AXレジスタ)とマイクロ秒(DXレジスタまたは別のレジスタ)を直接ナノ秒に変換します。
    • 変換ロジックは以下のようになります(例: 386の場合、DX:AXは64ビット値を表す):
      // sec is in AX, usec in BX (or DX)
      // convert to DX:AX nsec
      MOVL    $1000000000, CX  // CX = 1,000,000,000 (seconds to nanoseconds)
      MULL    CX               // AX = AX * CX (seconds * 1e9), DX:AX holds 64-bit result
      IMULL   $1000, BX        // BX = BX * 1000 (microseconds to nanoseconds)
      ADDL    BX, AX           // AX = AX + BX (add usec part to low 32-bits of nsec)
      ADCL    $0, DX           // DX = DX + carry (add carry to high 32-bits of nsec)
      
    • このアセンブリレベルでの直接計算により、中間的なメモリ割り当てが回避されます。
  • Linux ARM:

    • このコミット時点では、ARM版はダミーの実装(常に0を返す)になっています。これは、特定のハードウェアやカーネルバージョンでの正確な時刻取得メカニズムが複雑であるか、まだ実装が完了していなかったためと考えられます。
  • Windows:

    • Windowsでは、QueryPerformanceCounterQueryPerformanceFrequencyを使用していた以前の実装から、GetSystemTimeAsFileTimeを使用するように変更されました。
    • GetSystemTimeAsFileTimeは、1601年1月1日からの100ナノ秒単位の時間をFILETIME構造体(実質的にはint64)で返します。
    • この値をUnixエポック(1970年1月1日)からのナノ秒に変換するために、固定のオフセット値(1601年から1970年までの100ナノ秒単位の差)を減算し、さらに100を乗算してナノ秒単位に変換します。
      // Filetime is 100s of nanoseconds since January 1, 1601.
      // Convert to nanoseconds since January 1, 1970.
      return (filetime - 116444736000000000LL) * 100LL;
      
    • この変更により、Windows環境でもメモリ割り当てなしで高精度な時刻取得が可能になりました。

time.goc の導入

src/pkg/runtime/time.gocという新しいファイルが導入されました。このファイルは、Goのtimeパッケージとランタイムの間のブリッジとして機能します。

// src/pkg/runtime/time.goc
package time

#include "runtime.h"

func Nanoseconds() (ret int64) {
	ret = runtime·nanotime();
}

このコードは、GoのtimeパッケージのNanoseconds()関数が、ランタイムで定義されたruntime·nanotime()関数を呼び出すことを示しています。これにより、GoのユーザーレベルAPIから、最適化されたランタイムの時刻取得メカニズムへの直接的なパスが提供されます。

runtime.cruntime.h の変更

runtime.cからは、以前のruntime·nanotimeの実装(runtime·gettimeを呼び出して秒とマイクロ秒を変換していたC言語の実装)が削除されました。これは、各OS/アーキテクチャのアセンブリコードにそのロジックが直接組み込まれたためです。 runtime.hからは、runtime·gettimeの宣言が削除され、runtime·nanotimeの宣言が残されました。

src/pkg/time/sys.go の変更

Goのtimeパッケージの公開APIであるNanoseconds()関数とSeconds()関数も変更されました。 Nanoseconds()は、新しく導入されたruntime/time.gocを介してruntime·nanotime()を呼び出すようになりました。 Seconds()は、Nanoseconds()の結果を10億で割ることで計算されるようになりました。これにより、os.Time()(以前はメモリ割り当ての可能性があった)への依存がなくなりました。

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

このコミットで変更された主要なファイルと、その変更の概要は以下の通りです。

  • src/pkg/runtime/Makefile:
    • time.$Oがオブジェクトファイルリストに追加され、time.gocがビルドプロセスに含まれるようになりました。
  • src/pkg/runtime/darwin/386/sys.s, src/pkg/runtime/darwin/amd64/sys.s:
    • runtime·gettimeruntime·nanotimeにリネームされ、gettimeofdayシステムコールから返される秒とマイクロ秒を直接ナノ秒に変換するアセンブリロジックが追加されました。
  • src/pkg/runtime/freebsd/386/sys.s, src/pkg/runtime/freebsd/amd64/sys.s:
    • 上記Darwinと同様に、runtime·gettimeruntime·nanotimeにリネームされ、gettimeofdayシステムコールからの変換ロジックが追加されました。
  • src/pkg/runtime/linux/386/sys.s, src/pkg/runtime/linux/amd64/sys.s:
    • 上記と同様に、runtime·gettimeruntime·nanotimeにリネームされ、gettimeofdayシステムコールからの変換ロジックが追加されました。
  • src/pkg/runtime/linux/arm/sys.s:
    • runtime·gettimeruntime·nanotimeにリネームされましたが、この時点ではダミーの実装(0を返す)です。
  • src/pkg/runtime/openbsd/386/sys.s, src/pkg/runtime/openbsd/amd64/sys.s:
    • 上記と同様に、runtime·gettimeruntime·nanotimeにリネームされ、gettimeofdayシステムコールからの変換ロジックが追加されました。
  • src/pkg/runtime/plan9/386/signal.c:
    • runtime·gettimeの宣言が削除され、runtime·nanotimeの宣言が追加されました。Plan 9では、この関数はコンパイルエラーになるように意図的に残されています(おそらく未実装のため)。
  • src/pkg/runtime/runtime.c:
    • 以前のC言語で実装されていたruntime·nanotimeruntime·gettimeを呼び出していたもの)が削除されました。
  • src/pkg/runtime/runtime.h:
    • runtime·gettimeの関数プロトタイプが削除され、runtime·nanotimeのプロトタイプが維持されました。
  • src/pkg/runtime/time.goc:
    • 新規ファイル。GoのtimeパッケージのNanoseconds()関数が、ランタイムのruntime·nanotime()を呼び出すためのブリッジを提供します。
  • src/pkg/runtime/windows/thread.c:
    • Windows固有の時刻取得メカニズムがQueryPerformanceCounterからGetSystemTimeAsFileTimeに変更され、関連するAPIのインポートと初期化ロジックが更新されました。runtime·nanotimeの実装がGetSystemTimeAsFileTimeに基づくものに置き換えられました。
  • src/pkg/time/sys.go:
    • time.Nanoseconds()runtime·nanotime()を直接呼び出すように変更され、time.Seconds()time.Nanoseconds()に基づいて計算されるようになりました。os.Time()への依存が削除されました。

コアとなるコードの解説

このコミットの核心は、各OS/アーキテクチャのアセンブリコードにおけるruntime·nanotimeの実装と、time.gocによるGo言語側からの呼び出しです。

アセンブリコードの例 (src/pkg/runtime/darwin/386/sys.s)

// int64 nanotime(void) so really
// void nanotime(int64 *nsec)
TEXT runtime·nanotime(SB), 7, $32
	LEAL	12(SP), AX	// must be non-nil, unused
	MOVL	AX, 4(SP)
	MOVL	$0, 8(SP)	// time zone pointer
	MOVL	$116, AX
	INT	$0x80
	MOVL	DX, BX

	// sec is in AX, usec in BX
	// convert to DX:AX nsec
	MOVL	$1000000000, CX
	MULL	CX
	IMULL	$1000, BX
	ADDL	BX, AX
	ADCL	$0, DX
	
	MOVL	ret+0(FP), DI
	MOVL	AX, 0(DI)
	MOVL	DX, 4(DI)
	RET
  1. TEXT runtime·nanotime(SB), 7, $32: runtime·nanotime関数の定義。SBはStatic Baseレジスタ、7はフラグ、$32はスタックフレームサイズ。
  2. LEAL 12(SP), AX ... INT $0x80: これはgettimeofdayシステムコールを呼び出す部分です。
    • $116はDarwin/386におけるgettimeofdayのシステムコール番号です。
    • INT $0x80はシステムコールを実行します。
    • システムコールから戻ると、秒がAXレジスタに、マイクロ秒がDXレジスタに格納されます。
  3. MOVL DX, BX: マイクロ秒をDXからBXレジスタに移動します。
  4. // sec is in AX, usec in BX: コメントで現在のレジスタの状態を説明しています。
  5. // convert to DX:AX nsec: 64ビットのナノ秒値への変換を開始します。
  6. MOVL $1000000000, CX: CXレジスタに10億(秒をナノ秒に変換するための係数)をロードします。
  7. MULL CX: AX(秒)とCX(10億)を乗算します。結果は64ビット値としてDX:AXに格納されます(DXが上位32ビット、AXが下位32ビット)。
  8. IMULL $1000, BX: BX(マイクロ秒)に1000を乗算し、ナノ秒に変換します。
  9. ADDL BX, AX: BX(変換されたマイクロ秒)をAX(ナノ秒の下位32ビット)に加算します。
  10. ADCL $0, DX: ADDL命令で発生したキャリー(繰り上がり)をDX(ナノ秒の上位32ビット)に加算します。これにより、64ビットの加算が正しく行われます。
  11. MOVL ret+0(FP), DI ... MOVL DX, 4(DI): 最終的な64ビットのナノ秒値(DX:AX)を、呼び出し元が指定した戻り値のポインタ(ret+0(FP))に格納します。AXが下位32ビット、DXが上位32ビットです。
  12. RET: 関数から戻ります。

このアセンブリコードは、システムコールから直接取得した秒とマイクロ秒の値を、レジスタ内で直接ナノ秒に変換し、メモリ割り当てを一切行わないように設計されています。

src/pkg/runtime/time.goc

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Runtime implementations to help package time.

package time

#include "runtime.h"

func Nanoseconds() (ret int64) {
	ret = runtime·nanotime();
}

このファイルは、GoのtimeパッケージのNanoseconds()関数が、ランタイムのruntime·nanotime()関数を呼び出すためのGo言語側の定義です。#include "runtime.h"は、Cgoのようなメカニズムを通じて、GoのコードからランタイムのC/アセンブリ関数を呼び出すことを可能にします。これにより、Goのユーザーコードは、低レベルで最適化された時刻取得メカニズムを透過的に利用できます。

関連リンク

参考にした情報源リンク