[インデックス 17664] ファイルの概要
このコミットは、Go言語のランタイムテストスイートの一部である test/chan/select2.go
ファイルに対する変更です。このファイルは、select
ステートメントとチャネル操作がメモリ使用量に与える影響をテストするために設計されています。具体的には、多数の select
操作を行った際のメモリ割り当てを測定し、それが許容範囲内であるかを確認します。
コミット
commit 1c45f98fa38d9600ee1c60c2bfba3c0dced86087
Author: Carl Shapiro <cshapiro@google.com>
Date: Fri Sep 20 17:27:56 2013 -0700
test/chan: avoid wrap-around in memstats comparison
The select2.go test assumed that the memory allocated between
its two samplings of runtime.ReadMemStats is strictly
increasing. To avoid failing the tests when this is not true,
a greater-than check is introduced before computing the
difference in allocated memory.
R=golang-dev, r, cshapiro
CC=golang-dev
https://golang.org/cl/13701046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1c45f98fa38d9600ee1c60c2bfba3c0dced86087
元コミット内容
test/chan: avoid wrap-around in memstats comparison
The select2.go test assumed that the memory allocated between
its two samplings of runtime.ReadMemStats is strictly
increasing. To avoid failing the tests when this is not true,
a greater-than check is introduced before computing the
difference in allocated memory.
R=golang-dev, r, cshapiro
CC=golang-dev
https://golang.org/cl/13701046
変更の背景
test/chan/select2.go
テストは、Goプログラムのメモリ使用量を測定するために runtime.ReadMemStats
関数を使用しています。このテストでは、特定の操作(この場合は100,000回の select
操作)の前後でメモリ統計を2回サンプリングし、その差分を計算することで、操作によって割り当てられたメモリ量を推定していました。
しかし、このテストには潜在的な問題がありました。それは、2回目の runtime.ReadMemStats
の呼び出しで取得される memstats.Alloc
の値が、1回目の呼び出しで取得された alloc
の値よりも常に大きいと仮定していた点です。Goのガベージコレクタ (GC) は非同期に動作し、テストの実行中にメモリを解放する可能性があります。また、テスト環境やランタイムの最適化によっては、メモリの再利用や解放が行われ、memstats.Alloc
の値が一時的に減少する可能性がありました。
もし memstats.Alloc
が alloc
よりも小さい場合、memstats.Alloc - alloc
の計算結果は負の値になります。Goの uint64
型のような符号なし整数型で負の値を表現しようとすると、「ラップアラウンド (wrap-around)」が発生し、非常に大きな正の値として解釈されてしまいます。この結果、テストは「BUG: too much memory for 100,000 selects: [非常に大きな値]」という誤ったエラーを出力し、実際にはメモリリークがないにもかかわらずテストが失敗する、という問題が発生していました。
このコミットは、このような誤ったテスト失敗を防ぐために導入されました。
前提知識の解説
Goのメモリ統計 (runtime.MemStats
)
Go言語の runtime
パッケージは、プログラムの実行時情報にアクセスするための機能を提供します。その中でも runtime.MemStats
構造体は、Goランタイムのメモリ割り当てに関する詳細な統計情報を含んでいます。
runtime.ReadMemStats(m *MemStats)
: この関数は、現在のGoランタイムのメモリ統計をm
が指すMemStats
構造体に読み込みます。この関数は、ガベージコレクション (GC) の状態、ヒープの使用状況、オブジェクトの割り当て数など、多岐にわたる情報を提供します。MemStats.Alloc
:MemStats
構造体に含まれるフィールドの一つで、現在割り当てられているオブジェクトの合計バイト数を示します。これは、Goプログラムが現在使用しているメモリの量を示す主要な指標の一つです。
ガベージコレクション (GC)
Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラムが不要になったメモリを自動的に解放し、再利用可能にします。GCはバックグラウンドで非同期に動作することが多く、特定のタイミングでメモリが解放されるため、runtime.ReadMemStats
を呼び出すたびに Alloc
の値が単調増加するとは限りません。特に、大量のオブジェクトが生成され、その後すぐに不要になるようなシナリオでは、GCによって Alloc
の値が減少する可能性があります。
符号なし整数型のラップアラウンド
Goの uint64
のような符号なし整数型は、負の値を表現できません。もし uint64
型の変数 a
から b
を減算する際に a < b
であった場合、結果は負の値になるはずですが、符号なし整数型では最下位ビットから最上位ビットへの「桁借り」が連続的に発生し、結果として非常に大きな正の値になります。これを「ラップアラウンド」と呼びます。
例えば、8ビットの符号なし整数で 5 - 10
を計算すると、通常は -5
ですが、ラップアラウンドにより 251
(2^8 - 5
) となります。このコミットのケースでは、memstats.Alloc - alloc
が負になるべき場合に、非常に大きな正の値として評価され、テストの閾値 1.1e5
(110,000) を超えてしまうことが問題でした。
技術的詳細
test/chan/select2.go
の元のコードは以下のようになっていました。
// ...
runtime.GC()
runtime.ReadMemStats(memstats)
if memstats.Alloc-alloc > 1.1e5 {
println("BUG: too much memory for 100,000 selects:", memstats.Alloc-alloc)
}
// ...
ここで、alloc
はテスト開始前に runtime.ReadMemStats
で取得された memstats.Alloc
の値です。テストの意図は、100,000回の select
操作によって割り当てられるメモリが 1.1e5
バイト(110KB)を超えないことを確認することでした。
問題は、memstats.Alloc
が alloc
よりも小さくなる可能性があったことです。これは、テスト実行中にGCが走り、以前に割り当てられたメモリを解放した結果、現在の memstats.Alloc
が初期の alloc
よりも少なくなるという状況です。
例えば、alloc
が 100
で、GCによってメモリが解放され memstats.Alloc
が 50
になったとします。この場合、memstats.Alloc - alloc
は 50 - 100 = -50
となります。しかし、memstats.Alloc
と alloc
は uint64
型であるため、この減算は符号なし整数演算として実行されます。結果として、-50
は uint64
の最大値に近い非常に大きな正の値(例えば 2^64 - 50
)にラップアラウンドします。
このラップアラウンドした非常に大きな値は、常に 1.1e5
を超えるため、テストは誤って「BUG: too much memory...」というエラーを出力し、失敗していました。これは、実際のメモリリークや過剰なメモリ使用がないにもかかわらず、テストが不安定になる原因となっていました。
このコミットでは、この問題を解決するために、減算を行う前に memstats.Alloc
が alloc
よりも大きいことを確認する条件を追加しました。
コアとなるコードの変更箇所
--- a/test/chan/select2.go
+++ b/test/chan/select2.go
@@ -47,7 +47,8 @@ func main() {
runtime.GC()
runtime.ReadMemStats(memstats)
- if memstats.Alloc-alloc > 1.1e5 {
+ // Be careful to avoid wraparound.
+ if memstats.Alloc > alloc && memstats.Alloc-alloc > 1.1e5 {
println("BUG: too much memory for 100,000 selects:", memstats.Alloc-alloc)
}
}
コアとなるコードの解説
変更は test/chan/select2.go
ファイルの main
関数内、メモリ使用量をチェックする if
文にあります。
元のコード:
if memstats.Alloc-alloc > 1.1e5 {
変更後のコード:
if memstats.Alloc > alloc && memstats.Alloc-alloc > 1.1e5 {
追加された memstats.Alloc > alloc
という条件がこのコミットの核心です。
memstats.Alloc > alloc
: この条件は、現在のメモリ割り当て量 (memstats.Alloc
) が、テスト開始時のメモリ割り当て量 (alloc
) よりも厳密に大きい場合にのみ、その後の差分計算と閾値チェックを実行するようにします。- もし
memstats.Alloc
がalloc
以下(つまり、GCによってメモリが解放されたか、メモリ使用量が減少した)であれば、この条件はfalse
となり、&&
演算子の短絡評価により、memstats.Alloc-alloc
の計算は行われません。これにより、符号なし整数型のラップアラウンドによる誤った大きな値の生成が回避されます。 - この変更により、テストはメモリ使用量が実際に増加した場合にのみ、その増加量が閾値を超えているかをチェックするようになります。メモリ使用量が減少した場合や、ほとんど変化がなかった場合には、テストは何も報告せず、誤った失敗を防ぎます。
- もし
この修正は、テストの堅牢性を高め、GoランタイムのGC動作やメモリ管理の特性を考慮に入れた、より正確なメモリ使用量テストを可能にしました。
関連リンク
- Go CL 13701046: https://golang.org/cl/13701046
参考にした情報源リンク
- Go言語の公式ドキュメント (
runtime
パッケージ): https://pkg.go.dev/runtime - Go言語の公式ドキュメント (
runtime.MemStats
): https://pkg.go.dev/runtime#MemStats - Go言語の公式ドキュメント (
runtime.ReadMemStats
): https://pkg.go.dev/runtime#ReadMemStats - 符号なし整数型のラップアラウンドに関する一般的な情報 (Go言語に特化したものではないが、概念理解に役立つ): https://en.wikipedia.org/wiki/Integer_overflow#Unsigned_integers (Wikipedia)
- Go言語のガベージコレクションに関する情報: https://go.dev/doc/gc-guide (Go公式ブログなど)
select
ステートメントに関する情報: https://go.dev/ref/spec#Select_statements (Go言語仕様)println
関数に関する情報: https://pkg.go.dev/builtin#println (Go言語組み込み関数)1.1e5
のような浮動小数点リテラルに関する情報: https://go.dev/ref/spec#Floating-point_literals (Go言語仕様)&&
(論理AND) 演算子の短絡評価に関する情報: https://go.dev/ref/spec#Logical_operators (Go言語仕様)