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

[インデックス 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.Allocalloc よりも小さい場合、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.Allocalloc よりも小さくなる可能性があったことです。これは、テスト実行中にGCが走り、以前に割り当てられたメモリを解放した結果、現在の memstats.Alloc が初期の alloc よりも少なくなるという状況です。

例えば、alloc100 で、GCによってメモリが解放され memstats.Alloc50 になったとします。この場合、memstats.Alloc - alloc50 - 100 = -50 となります。しかし、memstats.Allocallocuint64 型であるため、この減算は符号なし整数演算として実行されます。結果として、-50uint64 の最大値に近い非常に大きな正の値(例えば 2^64 - 50)にラップアラウンドします。

このラップアラウンドした非常に大きな値は、常に 1.1e5 を超えるため、テストは誤って「BUG: too much memory...」というエラーを出力し、失敗していました。これは、実際のメモリリークや過剰なメモリ使用がないにもかかわらず、テストが不安定になる原因となっていました。

このコミットでは、この問題を解決するために、減算を行う前に memstats.Allocalloc よりも大きいことを確認する条件を追加しました。

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

--- 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 という条件がこのコミットの核心です。

  1. memstats.Alloc > alloc: この条件は、現在のメモリ割り当て量 (memstats.Alloc) が、テスト開始時のメモリ割り当て量 (alloc) よりも厳密に大きい場合にのみ、その後の差分計算と閾値チェックを実行するようにします。
    • もし memstats.Allocalloc 以下(つまり、GCによってメモリが解放されたか、メモリ使用量が減少した)であれば、この条件は false となり、&& 演算子の短絡評価により、memstats.Alloc-alloc の計算は行われません。これにより、符号なし整数型のラップアラウンドによる誤った大きな値の生成が回避されます。
    • この変更により、テストはメモリ使用量が実際に増加した場合にのみ、その増加量が閾値を超えているかをチェックするようになります。メモリ使用量が減少した場合や、ほとんど変化がなかった場合には、テストは何も報告せず、誤った失敗を防ぎます。

この修正は、テストの堅牢性を高め、GoランタイムのGC動作やメモリ管理の特性を考慮に入れた、より正確なメモリ使用量テストを可能にしました。

関連リンク

参考にした情報源リンク