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

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

このコミットは、Go言語のtestingパッケージにおけるAllocsPerRun関数の振る舞いを修正し、返される値が常に整数であることを保証するように変更します。これにより、AllocsPerRun()の結果を整数として比較することが可能になり、特にAllocsPerRun()==1のような厳密な比較が意図通りに機能するようになります。

コミット

commit cf5dd6ad644ef0f12e5e1f550a3721b146ad177a
Author: Rob Pike <r@golang.org>
Date:   Thu May 30 11:28:08 2013 -0400

    testing: quantize AllocsPerRun
    As the code now says:
    
    We are forced to return a float64 because the API is silly, but do
    the division as integers so we can ask if AllocsPerRun()==1
    instead of AllocsPerRun()<2.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/9837049

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

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

元コミット内容

このコミットは、testingパッケージのAllocsPerRun関数が返す値の計算方法を変更し、結果が常に整数値となるように「量子化」します。コミットメッセージによると、APIの制約によりfloat64を返す必要があるものの、内部的な計算を整数で行うことで、AllocsPerRun()==1のような厳密な整数比較を可能にすることを目的としています。これは、以前のAllocsPerRun()<2のような浮動小数点比較の曖昧さを解消するためです。

変更の背景

testing.AllocsPerRun関数は、ベンチマーク対象の関数が1回の実行あたりに平均で何回メモリ割り当て(アロケーション)を行うかを計測するために使用されます。この関数は、その設計上float64型を返します。しかし、メモリ割り当ての回数は本質的に整数値であるため、例えば「1回のアロケーション」を厳密にチェックしたい場合に、浮動小数点数の比較(例: AllocsPerRun() == 1.0)は、浮動小数点演算の性質上、予期せぬ誤差を生じる可能性がありました。

具体的には、以前の実装ではfloat64(mallocs) / float64(runs)のように、mallocs(総アロケーション数)とruns(実行回数)をそれぞれfloat64にキャストしてから除算を行っていました。この方法では、例えばmallocsが5でrunsが2の場合、結果は2.5となります。しかし、もしmallocsが5でrunsが5の場合、結果は1.0となります。問題は、AllocsPerRunが「平均アロケーション数」を返すため、期待されるのは「1回のアロケーション」や「0回のアロケーション」といった整数値であることが多いという点です。

開発者は、関数が厳密に1回のアロケーションを行うことを確認したい場合、AllocsPerRun() == 1と書きたいと考えます。しかし、浮動小数点数の除算では、例えば1.0000000000000001のような値が返される可能性があり、これが1.0と厳密に一致しないため、テストが失敗する原因となり得ました。このコミットは、この問題を解決し、AllocsPerRunが返す値が常に整数値として解釈できるように、内部の除算を整数で行うように変更しました。これにより、AllocsPerRun()==1のような比較が安全に行えるようになります。

前提知識の解説

testing.AllocsPerRun関数

Go言語のtestingパッケージは、ベンチマークテストをサポートしており、コードのパフォーマンス特性を測定するためのツールを提供します。testing.AllocsPerRun関数は、その中でも特にメモリ割り当ての効率を評価するために設計されています。

  • 目的: 指定された関数fが複数回実行された際に、1回あたりの平均メモリ割り当て回数を計測します。これは、特にメモリ効率が重要な高性能なコードにおいて、不要なアロケーションを特定し、最適化するために非常に有用です。過剰なアロケーションは、ガベージコレクション(GC)の頻度を増加させ、プログラムの実行を一時停止させる「ストップ・ザ・ワールド」時間を引き起こし、全体的なパフォーマンスを低下させる可能性があります。
  • 動作原理:
    1. ウォームアップ実行: まず、関数fを1回ウォームアップとして実行します。これは、JITコンパイルやキャッシュのウォームアップなど、初回実行時のオーバーヘッドを排除し、より安定した測定結果を得るためです。
    2. メモリ統計の取得: runtime.ReadMemStats関数を使用して、ウォームアップ実行前後のメモリ統計(特にMallocs、つまりメモリ割り当ての総数)を取得します。
    3. 複数回実行: その後、指定されたruns回数だけ関数fを繰り返し実行します。
    4. 平均の計算: 実行前後のMallocsの差分を計算し、それをrunsで割ることで、1回あたりの平均アロケーション数を算出します。
    5. GOMAXPROCSの設定: 測定中はGOMAXPROCSを1に設定することで、並行処理による測定のばらつきを抑え、より一貫性のある結果を保証します。
  • 利用例: ゼロアロケーションを保証したい関数(例: AllocsPerRun(100, func() { /* ... */ }) == 0)や、特定のアロケーション数に収まっていることを確認したい場合などに利用されます。

runtime.ReadMemStats関数

runtimeパッケージは、Goランタイムシステムと直接対話するための低レベルな操作を提供します。runtime.ReadMemStats関数は、現在のGoプロセスの詳細なメモリ割り当て統計をruntime.MemStats構造体に格納するために使用されます。

  • 目的: Goプログラムのメモリ使用状況を監視し、メモリリークの特定、パフォーマンス分析、ガベージコレクションの動作理解に役立ちます。
  • runtime.MemStats構造体: この構造体には、ヒープ割り当て、システムからのメモリ取得、ガベージコレクションの回数や一時停止時間など、多岐にわたるメモリ関連のメトリクスが含まれています。
    • Mallocs: プログラム開始以降のメモリ割り当ての総回数。
    • Frees: プログラム開始以降のメモリ解放の総回数。
    • Alloc: 現在ヒープに割り当てられているバイト数。
    • TotalAlloc: プログラム開始以降にヒープに割り当てられた累積バイト数。
    • NumGC: 完了したガベージコレクションサイクルの回数。
    • PauseTotalNs: プログラム開始以降、GCによる「ストップ・ザ・ワールド」一時停止に費やされた合計ナノ秒。
  • 利用: AllocsPerRunのようなベンチマークツールだけでなく、アプリケーションの監視ツールやカスタムプロファイリングツールでも利用されます。Go 1.16以降では、より汎用的なメトリクス収集のためにruntime/metricsパッケージが推奨されていますが、runtime.ReadMemStatsは依然として詳細なメモリ統計を取得するための基本的な手段です。

技術的詳細

このコミットの核心は、AllocsPerRun関数が返すfloat64値の計算方法を変更することにあります。以前は、総アロケーション数mallocsと実行回数runsをそれぞれfloat64に変換してから除算を行っていました。

// 変更前:
return float64(mallocs) / float64(runs)

この計算では、mallocsrunsで割り切れない場合、結果は小数点以下の値を持つfloat64となります。例えば、5回のアロケーションが2回の実行で行われた場合、5.0 / 2.0 = 2.5となります。しかし、AllocsPerRunが意図するのは「平均アロケーション数」であり、多くの場合、ユーザーは「1回のアロケーション」や「0回のアロケーション」といった厳密な整数値の平均を期待します。

浮動小数点数の比較は、精度問題により==演算子で厳密に行うことが推奨されません。例えば、2.5という結果に対してAllocsPerRun() == 2.5と書くことはできますが、AllocsPerRun() == 1と書いた場合、もし内部で1.0000000000000001のような値が計算されると、==比較はfalseを返してしまいます。このため、ユーザーはAllocsPerRun() < 2のような範囲比較を行う必要があり、これは「厳密に1回のアロケーション」を意図する場合には不便で、意図が不明瞭になる可能性がありました。

このコミットでは、この問題を解決するために、除算をfloat64にキャストする前に行うように変更しました。

// 変更後:
return float64(mallocs / uint64(runs))

ここで重要なのは、mallocsuint64型であり、runsint型であるため、uint64(runs)とすることで両方をuint64型にしてから整数除算mallocs / uint64(runs)を行う点です。Go言語における整数除算は、小数点以下を切り捨てます。例えば、5 / 22となります。この整数除算の結果を最後にfloat64にキャストすることで、返されるfloat64値は常に整数値(例: 1.0, 2.0, 3.0など)の表現となります。

これにより、AllocsPerRun()が返す値は、たとえfloat64型であっても、その値は常に整数値の表現となるため、ユーザーはAllocsPerRun()==1のような厳密な整数比較を安全に行うことができるようになります。これは、AllocsPerRunの利用者が期待するセマンティクスと、APIの制約(float64を返す)との間のギャップを埋めるための重要な変更です。

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

--- a/src/pkg/testing/allocs.go
+++ b/src/pkg/testing/allocs.go
@@ -9,6 +9,7 @@ import (
 )
 
 // AllocsPerRun returns the average number of allocations during calls to f.
+// Although the return value has type float64, it will always be an integral value.
 //
 // To compute the number of allocations, the function will first be run once as
 // a warm-up.  The average number of allocations over the specified number of
@@ -36,6 +37,9 @@ func AllocsPerRun(runs int, f func()) (avg float64) {
 	runtime.ReadMemStats(&memstats)
 	mallocs += memstats.Mallocs
 
-	// Average the mallocs over the runs (not counting the warm-up)
-	return float64(mallocs) / float64(runs)
+	// Average the mallocs over the runs (not counting the warm-up).
+	// We are forced to return a float64 because the API is silly, but do
+	// the division as integers so we can ask if AllocsPerRun()==1
+	// instead of AllocsPerRun()<2.
+	return float64(mallocs / uint64(runs))
 }

コアとなるコードの解説

変更はsrc/pkg/testing/allocs.goファイル内のAllocsPerRun関数に集中しています。

  1. コメントの追加:

    // Although the return value has type float64, it will always be an integral value.
    

    この新しいコメントは、関数のシグネチャがfloat64を返すにもかかわらず、その値が常に整数値であることを明確に示しています。これは、このコミットの意図を直接的に説明するものです。

  2. 計算ロジックの変更:

    -	return float64(mallocs) / float64(runs)
    +	return float64(mallocs / uint64(runs))
    

    これが最も重要な変更点です。

    • 変更前: float64(mallocs) / float64(runs)
      • mallocsrunsをそれぞれfloat64に型変換してから除算を行っていました。これにより、結果は浮動小数点数となり、割り切れない場合は小数点以下の値を持つ可能性がありました。
    • 変更後: float64(mallocs / uint64(runs))
      • まず、mallocsuint64型)をuint64(runs)runsuint64に型変換したもの)で整数除算します。Goの整数除算は小数点以下を切り捨てます。
      • この整数除算の結果を、最後にfloat64に型変換して返します。

この変更により、mallocsrunsで割り切れない場合でも、除算は整数として行われるため、結果は常に整数値に切り捨てられます。例えば、mallocs=5, runs=2の場合、変更前は2.5を返しましたが、変更後はfloat64(5 / 2)、つまりfloat64(2)となり、2.0を返します。これにより、AllocsPerRunが返すfloat64値は、常にX.0の形式となり、ユーザーはAllocsPerRun()==1のような厳密な整数比較を安全に行えるようになります。

また、新しいコメントブロックが追加され、この変更の理由が詳細に説明されています。

// Average the mallocs over the runs (not counting the warm-up).
// We are forced to return a float64 because the API is silly, but do
// the division as integers so we can ask if AllocsPerRun()==1
// instead of AllocsPerRun()<2.

このコメントは、APIの制約(float64を返す必要があること)と、その制約の中でいかにしてユーザーが期待する整数比較を可能にするかという、このコミットの設計思想を明確に示しています。

関連リンク

参考にした情報源リンク