[インデックス 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)の頻度を増加させ、プログラムの実行を一時停止させる「ストップ・ザ・ワールド」時間を引き起こし、全体的なパフォーマンスを低下させる可能性があります。 - 動作原理:
- ウォームアップ実行: まず、関数
f
を1回ウォームアップとして実行します。これは、JITコンパイルやキャッシュのウォームアップなど、初回実行時のオーバーヘッドを排除し、より安定した測定結果を得るためです。 - メモリ統計の取得:
runtime.ReadMemStats
関数を使用して、ウォームアップ実行前後のメモリ統計(特にMallocs
、つまりメモリ割り当ての総数)を取得します。 - 複数回実行: その後、指定された
runs
回数だけ関数f
を繰り返し実行します。 - 平均の計算: 実行前後の
Mallocs
の差分を計算し、それをruns
で割ることで、1回あたりの平均アロケーション数を算出します。 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)
この計算では、mallocs
がruns
で割り切れない場合、結果は小数点以下の値を持つ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))
ここで重要なのは、mallocs
がuint64
型であり、runs
がint
型であるため、uint64(runs)
とすることで両方をuint64
型にしてから整数除算mallocs / uint64(runs)
を行う点です。Go言語における整数除算は、小数点以下を切り捨てます。例えば、5 / 2
は2
となります。この整数除算の結果を最後に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
関数に集中しています。
-
コメントの追加:
// Although the return value has type float64, it will always be an integral value.
この新しいコメントは、関数のシグネチャが
float64
を返すにもかかわらず、その値が常に整数値であることを明確に示しています。これは、このコミットの意図を直接的に説明するものです。 -
計算ロジックの変更:
- return float64(mallocs) / float64(runs) + return float64(mallocs / uint64(runs))
これが最も重要な変更点です。
- 変更前:
float64(mallocs) / float64(runs)
mallocs
とruns
をそれぞれfloat64
に型変換してから除算を行っていました。これにより、結果は浮動小数点数となり、割り切れない場合は小数点以下の値を持つ可能性がありました。
- 変更後:
float64(mallocs / uint64(runs))
- まず、
mallocs
(uint64
型)をuint64(runs)
(runs
をuint64
に型変換したもの)で整数除算します。Goの整数除算は小数点以下を切り捨てます。 - この整数除算の結果を、最後に
float64
に型変換して返します。
- まず、
- 変更前:
この変更により、mallocs
がruns
で割り切れない場合でも、除算は整数として行われるため、結果は常に整数値に切り捨てられます。例えば、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
を返す必要があること)と、その制約の中でいかにしてユーザーが期待する整数比較を可能にするかという、このコミットの設計思想を明確に示しています。
関連リンク
- Go CL 9837049: https://golang.org/cl/9837049
参考にした情報源リンク
- Go
testing.AllocsPerRun
function purpose: https://azoff.dev/posts/go-testing-allocsperrun/ - Go
runtime.ReadMemStats
documentation: https://pkg.go.dev/runtime#ReadMemStats - Understanding Go Memory Statistics: https://reintech.io/blog/understanding-go-memory-statistics
- Go Memory Profiling: https://go.dev/blog/pprof
- Go
runtime/metrics
package: https://pkg.go.dev/runtime/metrics