[インデックス 16349] ファイルの概要
このコミットは、Go言語のテストスイートにおける特定のテスト (test/fixedbugs/issue5493.go
) が、32ビットアーキテクチャ上で正確なガベージコレクション (GC) の挙動に依存しているために失敗する問題を修正するものです。具体的には、32ビット環境でのGCの「部分的に保守的 (partially conservative)」な性質が原因で、ファイナライザが期待通りに呼び出されないためにテストが失敗していました。このコミットは、32ビット環境ではこのテストを実行しないようにすることで、ビルドの失敗を防ぎます。
コミット
- コミットハッシュ:
910bd157c94ce893ec4f092c065954c8842ac6f4
- 作者: Dmitriy Vyukov dvyukov@google.com
- コミット日時: 2013年5月20日 月曜日 21:53:16 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/910bd157c94ce893ec4f092c065954c8842ac6f4
元コミット内容
test: do not run the test that relies on precise GC on 32-bits
Currently most of the 32-bit builder are broken.
Fixes #5516.
R=golang-dev, dave, iant
CC=golang-dev
https://golang.org/cl/9573043
変更の背景
この変更の背景には、Go言語のテストインフラストラクチャにおける安定性の問題がありました。具体的には、Goの公式ビルドシステム(ビルダー)のうち、32ビットアーキテクチャで動作するものが、test/fixedbugs/issue5493.go
というテストの失敗によって頻繁に壊れていました。
issue5493.go
テストは、Goのガベージコレクション (GC) とファイナライザの正確な動作に依存しています。ファイナライザは、オブジェクトがGCによって回収される直前に実行される関数です。このテストは、特定の条件下で全てのファイナライザが呼び出されることを期待していましたが、当時の32ビットGo環境では、GCが「部分的に保守的 (partially conservative)」であったため、この期待が満たされませんでした。
「部分的に保守的なGC」とは、GCがスタックやレジスタ上のポインタを正確に識別できない場合があることを意味します。これにより、実際には到達不能なオブジェクトであっても、スタック上のビットパターンがたまたまポインタのように見えるために、GCがそのオブジェクトを「生きている」と誤認識し、回収しないことがあります。結果として、そのオブジェクトに登録されたファイナライザも実行されません。
64ビット環境では、ポインタのサイズが大きく、誤認識の可能性が低いため、この問題は顕在化しにくい傾向にありました。しかし、32ビット環境ではポインタのサイズが小さく、誤認識の可能性が高まるため、このテストが頻繁に失敗する原因となっていました。
このコミットは、根本的なGCの改善(「完全に正確なGC (fully precise GC)」の実装)を待つ間の一時的な対策として、32ビット環境でのテストの実行をスキップすることで、ビルドの安定性を確保することを目的としています。
前提知識の解説
ガベージコレクション (GC)
ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはや使用されていない(到達不能な)ものを自動的に解放する仕組みです。これにより、プログラマは手動でのメモリ管理の煩雑さから解放され、メモリリークなどのバグを減らすことができます。
Go言語のGCは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの停止時間(ストップ・ザ・ワールド)を最小限に抑えることを目指しています。
正確なGC (Precise GC) と保守的なGC (Conservative GC)
GCには大きく分けて「正確なGC」と「保守的なGC」の2種類があります。
- 正確なGC (Precise GC): プログラムのメモリ上の値がポインタであるか否かを常に正確に識別できるGCです。これにより、GCは到達可能なオブジェクトのみを正確にマークし、到達不能なオブジェクトを確実に回収できます。Go言語は最終的に完全に正確なGCを目指しています。
- 保守的なGC (Conservative GC): メモリ上の値がポインタであるか否かを常に正確に識別できないGCです。特にスタックやレジスタ上の値について、それがポインタなのか、たまたまポインタのように見える整数値なのかを区別できない場合があります。このような場合、GCは安全のために、ポインタのように見える値を全てポインタであると仮定し、その指す先のオブジェクトを「生きている」と判断します。これにより、実際には到達不能なオブジェクトが回収されずに残ってしまう(メモリリークではないが、メモリ使用量が増える)可能性があります。これを「メモリの保持 (memory retention)」と呼びます。
当時のGoの32ビット環境では、スタック上の値に対して部分的に保守的なGCが適用されており、これがファイナライザの呼び出しに影響を与えていました。
ファイナライザ (Finalizer)
Go言語のファイナライザは、runtime.SetFinalizer
関数を使ってオブジェクトに登録される関数です。GCがそのオブジェクトを回収する直前に、登録されたファイナライザが実行されます。ファイナライザは、ファイルディスクリプタのクローズ、ネットワーク接続の切断、C言語ライブラリとの連携で確保したメモリの解放など、リソースのクリーンアップ処理によく利用されます。
ファイナライザはGCの動作に強く依存するため、GCがオブジェクトを回収しない限り、ファイナライザは実行されません。保守的なGCの性質上、オブジェクトが回収されない可能性があるため、ファイナライザの実行が保証されない場合があります。
32ビットと64ビットアーキテクチャ
- 32ビットアーキテクチャ: メモリのアドレス空間が2^32バイト(約4GB)に制限されます。ポインタのサイズも32ビット(4バイト)です。
- 64ビットアーキテクチャ: メモリのアドレス空間が2^64バイトに拡張されます。ポインタのサイズも64ビット(8バイト)です。
32ビット環境ではポインタのサイズが小さいため、ランダムな整数値がたまたま有効なポインタのように見える確率が高まります。これが、32ビット環境で保守的なGCがより問題になりやすい理由の一つです。
技術的詳細
test/fixedbugs/issue5493.go
テストは、多数のオブジェクトを作成し、それぞれにファイナライザを登録した後、明示的に runtime.GC()
を呼び出して、全てのファイナライザが呼び出されることを確認するものです。テストの成功条件は、count
変数が0になること、つまり全てのファイナライザが実行されたことを意味します。
当時のGoの32ビット実装では、GCがスタック上のポインタを完全に正確に識別できない「部分的に保守的なGC」でした。このため、main
関数内のローカル変数(スタック上に存在する)が、実際にはもう参照されていないオブジェクトへのポインタを保持しているとGCが誤認識する可能性がありました。もしGCがオブジェクトへの参照がまだ存在すると判断した場合、そのオブジェクトは回収されず、結果として登録されたファイナライザも実行されません。
特に、main
関数内で wg.Add(N)
や wg.Wait()
のような操作が行われる際、スタックフレームが変化し、以前のスタック上の値がGCによって誤ってポインタと解釈されることがありました。32ビット環境ではポインタのビットパターンが短いため、このような誤認識の確率が高まります。
この問題は、GoのGCが「完全に正確なGC」に進化するまでの過渡期における制約でした。完全に正確なGCが実装されれば、GCはスタック上の値がポインタであるか否かを正確に判断できるようになり、このような誤認識はなくなります。
このコミットは、この根本的なGCの改善を待つ間、テストが32ビット環境で失敗し続けることを避けるための実用的な回避策として導入されました。テスト自体がGoのGCの特定の挙動(ファイナライザの正確な呼び出し)を検証するものであるため、その前提条件(完全に正確なGC)が満たされない環境ではテストをスキップするのが最も適切な判断でした。
コアとなるコードの変更箇所
変更は test/fixedbugs/issue5493.go
ファイルの main
関数内で行われました。
--- a/test/fixedbugs/issue5493.go
+++ b/test/fixedbugs/issue5493.go
@@ -31,6 +31,11 @@ func run() error {
}\n \n func main() {\n+\t// Does not work on 32-bits due to partially conservative GC.\n+\t// Try to enable when we have fully precise GC.\n+\tif runtime.GOARCH != \"amd64\" {\n+\t\treturn\n+\t}\n count = N
var wg sync.WaitGroup
wg.Add(N)
@@ -46,6 +51,7 @@ func main() {\
\truntime.GC()\n }\n if count != 0 {\n+\t\tprintln(count, \"out of\", N, \"finalizer are called\")\n \tpanic(\"not all finalizers are called\")\n }\n }\
コアとなるコードの解説
追加された主要なコードは以下の部分です。
// Does not work on 32-bits due to partially conservative GC.
// Try to enable when we have fully precise GC.
if runtime.GOARCH != "amd64" {
return
}
runtime.GOARCH
: これはGoの標準ライブラリruntime
パッケージが提供する定数で、現在のプログラムが実行されているシステムのアーキテクチャ(例: "amd64", "386", "arm" など)を表す文字列です。if runtime.GOARCH != "amd64"
: この条件文は、現在の実行環境のアーキテクチャが "amd64" (64ビットIntel/AMDアーキテクチャ) ではない場合に真となります。つまり、32ビットアーキテクチャ(例: "386")やARMアーキテクチャなどで実行されている場合にこのブロックに入ります。return
: 条件が真の場合、main
関数はここで終了します。これにより、issue5493.go
テストの残りのロジック(ファイナライザの登録とGCの実行、そして結果の検証)は実行されなくなります。
この変更により、32ビット環境でこのテストが実行されることがなくなり、ビルドシステムでのテスト失敗が回避されるようになりました。コメントにもあるように、これは「部分的に保守的なGC」が原因であり、「完全に正確なGC」が実装された際には、このテストを再度有効にすることを意図した一時的な措置です。
また、デバッグのために以下の println
ステートメントが追加されましたが、これはテストがパニックする直前にファイナライザの呼び出し状況を出力するためのものです。
println(count, "out of", N, "finalizer are called")
これは、テストが失敗した際に、何個のファイナライザが呼び出されなかったのかを明確にするためのもので、問題の診断に役立ちます。
関連リンク
- GitHub Issue: https://github.com/golang/go/issues/5516
- Go Code Review (CL): https://golang.org/cl/9573043
参考にした情報源リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (当時のGoのGCの挙動に関する情報)
- Go言語の
runtime
パッケージのドキュメント - 32ビットと64ビットアーキテクチャにおけるポインタとメモリ管理の一般的な概念