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

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

このコミットは、Go言語のテストスイート内の test/mapnan.go ファイルに対する変更です。具体的には、マップにNaN(Not a Number)を挿入する際のパフォーマンスをテストする部分において、時間の計測方法を syscall.Getrusage から time.Now に変更しています。

コミット

commit 08a561459763181649c35eb7191c945b5a10c933
Author: Russ Cox <rsc@golang.org>
Date:   Tue Oct 22 18:33:37 2013 -0400

    test/mapnan: use time.Now instead of syscall.Getrusage
    
    Avoids a dependency on a somewhat nonstandard part of package syscall.
    
    R=golang-dev, dave, r
    CC=golang-dev
    https://golang.org/cl/15570046

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

https://github.com/golang/go/commit/08a561459763181649c35eb7191c945b5a10c933

元コミット内容

test/mapnan: use time.Now instead of syscall.Getrusagetest/mapnan: syscall.Getrusage の代わりに time.Now を使用)

Avoids a dependency on a somewhat nonstandard part of package syscall.syscall パッケージのやや非標準的な部分への依存を避ける)

変更の背景

このコミットの背景には、Go言語のテストのポータビリティと信頼性の向上が挙げられます。syscall.Getrusage は、プロセスやその子プロセスのリソース使用量(CPU時間、メモリ使用量など)を取得するためのシステムコールをGoの syscall パッケージ経由で呼び出すものです。しかし、このシステムコールはオペレーティングシステム(OS)によってその挙動やサポート状況が異なる場合があります。特に、Utime (ユーザーCPU時間) や Stime (システムCPU時間) の精度や利用可能性はOSに依存します。

test/mapnan.go は、Goのマップが NaN (Not a Number) をキーとして扱う際のパフォーマンス特性をテストするためのものです。IEEE 754浮動小数点標準では、NaN はそれ自身を含め、他の NaN とも等しくないと定義されています。Goのマップはキーの等価性に基づいて動作するため、NaN をキーとして使用すると、通常の数値とは異なる挙動を示す可能性があります。このテストは、NaN を多数挿入した際に、マップの操作が二次関数的に(quadratic)遅くならないことを確認することを目的としていました。

このようなパフォーマンス測定を行うテストにおいて、syscall.Getrusage を用いた時間計測は、OS間の差異や、システムコール自体のオーバーヘッド、あるいはユーザー時間とシステム時間の区別といった要因により、テスト結果の安定性や正確性に影響を与える可能性がありました。コミットメッセージにある「somewhat nonstandard part of package syscall」(syscall パッケージのやや非標準的な部分)という記述は、Getrusage がすべてのプラットフォームで一貫した、あるいは期待通りの振る舞いをしない可能性を指していると考えられます。

これに対し、time.Now() はGoの標準ライブラリ time パッケージが提供する関数であり、現在のローカル時刻を返します。time.Since()time.Now() と組み合わせて、ある時点からの経過時間を計測するために用いられます。これらはOSのシステムコールに直接依存する Getrusage よりも、Goランタイムが提供する抽象化された、よりポータブルで安定した時間計測手段です。

したがって、この変更は、特定のOS依存のシステムコールへの依存を排除し、よりポータブルで信頼性の高い方法でテストのパフォーマンス測定を行うことを目的としています。これにより、異なるプラットフォーム上でのテストの安定性が向上し、Go言語全体の品質保証に貢献します。

前提知識の解説

  1. NaN (Not a Number): IEEE 754浮動小数点標準で定義される特殊な浮動小数点値です。0による除算、無限大同士の減算、負の数の平方根など、数学的に未定義または表現不可能な演算の結果として生成されます。NaN の最も重要な特性は、それ自身を含め、他のいかなる値とも等しくないという点です。つまり、NaN == NaNfalse と評価されます。Go言語の math パッケージには math.NaN() 関数があり、NaN 値を生成できます。

  2. Go言語のマップ (map): Go言語のマップは、キーと値のペアを格納するハッシュテーブルの実装です。キーは一意であり、値にアクセスするために使用されます。マップのキーは比較可能でなければなりません。浮動小数点数(float32, float64)は比較可能ですが、NaN の特殊な等価性ルールがマップの動作に影響を与えます。NaN をキーとしてマップに挿入すると、NaN はそれ自身と等しくないため、同じ NaN 値を複数回挿入しても、マップはそれらを異なるキーとして扱う可能性があります。この挙動は、マップのサイズが予期せず増大したり、パフォーマンスが低下したりする原因となることがあります。test/mapnan.go は、この NaN の特性がマップのパフォーマンスに二次関数的な影響を与えないことを確認するためのテストです。

  3. syscall パッケージと syscall.Getrusage: syscall パッケージは、Goプログラムから基盤となるオペレーティングシステムが提供するシステムコールに直接アクセスするための低レベルなインターフェースを提供します。これにより、ファイルシステム操作、ネットワーク通信、プロセス管理など、OSレベルの機能を利用できます。 syscall.Getrusage は、現在のプロセスまたはその子プロセスのリソース使用量に関する情報を取得するための関数です。引数として who (どのプロセスのリソースを取得するか、例: 0 は現在のプロセス) と *Rusage 構造体へのポインタを取ります。Rusage 構造体には、ユーザーCPU時間 (Utime)、システムCPU時間 (Stime)、最大常駐セットサイズ (Maxrss) などの情報が含まれます。 UtimeStimesyscall.Timeval 型で表され、秒とマイクロ秒のペアで構成されます。この関数はOSに依存するため、異なるOSでは利用できない、あるいは異なる情報を提供する可能性があります。

  4. time パッケージと time.Now(), time.Since(): time パッケージは、Go言語で時間に関連する操作(時刻の取得、期間の計算、タイマー、日付のフォーマットなど)を行うための標準ライブラリです。

    • time.Now(): 現在のローカル時刻を表す time.Time オブジェクトを返します。これは、システムクロックに基づいており、OSに依存しないGoランタイムの抽象化された時間表現です。
    • time.Since(t time.Time): 引数 t から現在までの経過時間を time.Duration 型で返します。これは、パフォーマンス測定やタイムアウト処理などで非常に便利です。

技術的詳細

このコミットの技術的な変更点は、test/mapnan.go 内の t := func(n int) time.Duration という匿名関数における時間計測ロジックの置き換えです。

変更前は、syscall.Getrusage を使用して、マップ操作の開始前と終了後のユーザーCPU時間を取得し、その差分を計算していました。

// 変更前
t := func(n int) time.Duration {
    var u0 syscall.Rusage
    if err := syscall.Getrusage(0,  &u0); err != nil {
        panic(err)
    }
    m := map[float64]int{}
    nan := math.NaN()
    for i := 0; i < n; i++ {
        m[nan] = i
    }
    if len(m) != n {
        panic("wrong size map after nan insertion")
    }
    var u1 syscall.Rusage
    if err := syscall.Getrusage(0,  &u1); err != nil {
        panic(err)
    }
    return time.Duration(u1.Utime.Nano() - u0.Utime.Nano())
}

ここで、u0u1syscall.Rusage 構造体であり、Utime フィールドは syscall.Timeval 型です。Utime.Nano() は、syscall.Timeval をナノ秒単位の int64 に変換するメソッド(または同等の操作)を想定しています。この方法は、プロセスが実際にCPUを使用した時間を測定するため、システム全体の負荷や他のプロセスの影響を受けにくいという利点があります。しかし、前述の通り、OS依存性や精度、オーバーヘッドの問題がありました。

変更後は、time.Now() を使用してマップ操作の開始時刻を記録し、操作終了後に time.Since() を使って経過時間を直接計算しています。

// 変更後
t := func(n int) time.Duration {
    t1 := time.Now() // 開始時刻を記録
    m := map[float64]int{}
    nan := math.NaN()
    for i := 0; i < n; i++ {
        m[nan] = i
    }
    if len(m) != n {
        panic("wrong size map after nan insertion")
    }
    return time.Since(t1) // 経過時間を計算
}

この変更により、以下の点が改善されます。

  1. ポータビリティの向上: time.Now()time.Since() はGoの標準ライブラリ関数であり、Goがサポートするすべてのプラットフォームで一貫して動作します。これにより、テストが異なるOS環境でも同じように実行され、信頼性の高い結果が得られるようになります。
  2. コードの簡潔化: syscall.Getrusage の呼び出しとエラーハンドリングが不要になり、コードがより簡潔になりました。
  3. 依存関係の削減: syscall パッケージへの直接的な依存がテストコードから削除されました。これにより、テストのビルドや実行時の複雑さが軽減されます。
  4. 測定対象の変更: syscall.Getrusage がCPU使用時間(ユーザー時間)を測定するのに対し、time.Since() は壁時計時間(wall-clock time)を測定します。このテストの目的が「NaNをマップに挿入する際の操作が二次関数的に遅くならないこと」を確認することであるため、CPU時間だけでなく、実際の経過時間(壁時計時間)でパフォーマンスを評価する方が、ユーザーが体感するパフォーマンスに近い指標となります。ただし、壁時計時間はシステム全体の負荷やスケジューリングの影響を受けるため、純粋なCPUパフォーマンスを測定する場合には不向きな場合もあります。このテストの文脈では、マップ操作の全体的な応答時間を評価することが重要であるため、time.Since が適切と判断されたと考えられます。

この変更は、Goのテストインフラストラクチャをより堅牢で、プラットフォームに依存しないものにするための継続的な取り組みの一環です。

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

test/mapnan.go ファイルの以下の部分が変更されました。

--- a/test/mapnan.go
+++ b/test/mapnan.go
@@ -13,17 +13,13 @@ import (
  	"fmt"
  	"math"
  	"time"
-	"syscall" // 削除
 )
 
 func main() {
 
  	// Test that NaNs in maps don't go quadratic.
  	t := func(n int) time.Duration {
-	// 削除されたコード
-	// 	var u0 syscall.Rusage
-	// 	if err := syscall.Getrusage(0,  &u0); err != nil {
-	// 		panic(err)
-	// 	}
+	t1 := time.Now() // 追加されたコード
  	m := map[float64]int{}
  	nan := math.NaN()
  	for i := 0; i < n; i++ {
@@ -32,11 +28,7 @@ func main() {
  	if len(m) != n {
  		panic("wrong size map after nan insertion")
  	}
-	// 削除されたコード
-	// 	var u1 syscall.Rusage
-	// 	if err := syscall.Getrusage(0,  &u1); err != nil {
-	// 		panic(err)
-	// 	}
-	// 	return time.Duration(u1.Utime.Nano() - u0.Utime.Nano())
+	return time.Since(t1) // 変更された戻り値
  	}
  
  	// Depending on the machine and OS, this test might be too fast

コアとなるコードの解説

変更されたコードは、test/mapnan.go 内の匿名関数 t の実装です。この関数は、引数 n で指定された回数だけ NaN をマップに挿入し、その操作にかかる時間を time.Duration 型で返します。

変更前:

  1. syscall パッケージがインポートされていました。
  2. t 関数の冒頭で syscall.Getrusage(0, &u0) を呼び出し、マップ操作開始前のリソース使用量(特にユーザーCPU時間)を u0 に記録していました。エラーが発生した場合は panic していました。
  3. マップ操作(n 回の NaN 挿入)を実行していました。
  4. マップ操作後、再度 syscall.Getrusage(0, &u1) を呼び出し、終了時のリソース使用量を u1 に記録していました。
  5. u1.Utime.Nano() - u0.Utime.Nano() を計算し、ユーザーCPU時間の差分をナノ秒単位で取得し、それを time.Duration に変換して返していました。

変更後:

  1. syscall パッケージのインポートが削除されました。
  2. t 関数の冒頭で t1 := time.Now() を呼び出し、マップ操作開始前の現在の時刻を t1 に記録していました。
  3. マップ操作(n 回の NaN 挿入)は変更なく実行されます。
  4. マップ操作後、time.Since(t1) を呼び出し、t1 から現在までの経過時間を直接 time.Duration 型で取得し、それを返しています。

この変更により、時間計測のメカニズムが、OS依存のシステムコールから、Goの標準ライブラリが提供するより抽象化された、ポータブルな時間計測機能へと移行しました。これにより、テストの実行環境に起因する潜在的な問題が解消され、テストの信頼性と一貫性が向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (time, syscall, map)
  • IEEE 754 浮動小数点標準に関する一般的な情報源
  • Go言語のコミット履歴と関連するコードレビュー (CL 15570046)
  • syscall.Getrusage の挙動に関する一般的なOSのドキュメント (例: Linux man pages for getrusage)
  • time.Now()time.Since() の使用例に関するGoのチュートリアルやブログ記事
  • Go言語のテストのベストプラクティスに関する情報