[インデックス 18606] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージ内のベンチマークテストファイルsrc/pkg/net/http/serve_test.go
に対する変更です。具体的には、BenchmarkClientServer
関数におけるdefer
文の不要な使用を修正しています。
コミット
commit b1c5bafda37a21216ee8e6f6d5bcfc6e1db08084
Author: Robert Daniel Kortschak <dan.kortschak@adelaide.edu.au>
Date: Mon Feb 24 18:17:07 2014 +0400
net/http: don't pile up defers in b.N loop
One defer was not removed in CL61150043.
LGTM=dvyukov
R=bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/64600044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b1c5bafda37a21216ee8e6f6d5bcfc6e1db08084
元コミット内容
net/http: don't pile up defers in b.N loop
One defer was not removed in CL61150043.
LGTM=dvyukov
R=bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/64600044
変更の背景
このコミットは、以前の変更セット(Change List: CL)であるCL61150043で修正されなかったdefer
文の残存を修正することを目的としています。net/http
パッケージのベンチマークテストBenchmarkClientServer
関数において、b.N
ループ内でdefer res.Body.Close()
が使用されていました。
defer
文は、その関数がリターンする直前に実行されるようにスケジュールされます。ベンチマークループ(b.N
ループ)内でdefer
が使用されると、ループの各イテレーションでdefer
される関数呼び出しがスタックに蓄積されていきます。これは、ベンチマークの実行中に不要なオーバーヘッド(メモリ消費や処理時間の増加)を引き起こし、正確なパフォーマンス測定を妨げる可能性があります。
CL61150043は「runtime: fix stack growth for large frames」という内容で、Goランタイムのスタック管理、特に大きなスタックフレームの成長に関する問題を解決するものでした。このコミットは、そのCLで修正されるべきだった、あるいはその変更によって顕在化したdefer
の残存を修正する、いわば後続のクリーンアップ作業と考えられます。ベンチマークの正確性を保つため、ループ内でリソースを即座に解放し、defer
による蓄積を避ける必要がありました。
前提知識の解説
Go言語のdefer
文
defer
文は、Go言語の重要な機能の一つで、関数がリターンする直前に指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルやネットワーク接続のクローズ、ロックの解除など)を確実に行うために非常によく使われます。defer
された関数はLIFO(後入れ先出し)の順序で実行されます。
例:
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 関数が終了する時にf.Close()が呼ばれることを保証
return ioutil.ReadAll(f)
}
Go言語のベンチマーク (testing
パッケージのb.N
)
Go言語には、標準ライブラリのtesting
パッケージにベンチマーク機能が組み込まれています。ベンチマーク関数はBenchmarkXxx(*testing.B)
というシグネチャを持ちます。
b.N
: ベンチマーク関数は、b.N
回繰り返して実行されます。b.N
の値は、ベンチマーク実行時にGoのテストフレームワークによって動的に調整され、統計的に有意な結果が得られるように十分な回数が実行されます。- 正確なベンチマーク: ベンチマークの目的は、特定のコードパスのパフォーマンスを測定することです。そのため、ループ内で測定対象外のオーバーヘッド(例えば、リソースの確保と解放を繰り返すことによるメモリ割り当てやGCの負荷)が発生すると、結果が不正確になります。
net/http
パッケージとres.Body.Close()
net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。HTTPリクエストを送信し、レスポンスを受け取る際に、レスポンスボディ(res.Body
)はio.ReadCloser
インターフェースを実装しています。これは、ボディの内容を読み取ることができるだけでなく、Close()
メソッドを呼び出して関連するリソースを解放する必要があることを意味します。
res.Body.Close()
を呼び出すことは、ネットワーク接続やバッファなどのリソースリークを防ぐために非常に重要です。これを怠ると、プログラムが利用可能なソケットを使い果たしたり、メモリを消費し続けたりする可能性があります。
技術的詳細
このコミットの技術的な問題点は、BenchmarkClientServer
関数内のb.N
ループでdefer res.Body.Close()
が使用されていたことです。
元のコードは以下のようになっていました。
func BenchmarkClientServer(b *testing.B) {
// ...
for i := 0; i < b.N; i++ {
res, err := client.Get(ts.URL)
if err != nil {
b.Fatal("Get:", err)
}
defer res.Body.Close() // ★問題の箇所
all, err := ioutil.ReadAll(res.Body)
res.Body.Close() // ★ここでも呼ばれている
if err != nil {
b.Fatal("ReadAll:", err)
}
_ = all
}
// ...
}
このコードでは、defer res.Body.Close()
と、その直後のres.Body.Close()
という二重のクローズ処理が行われていました。
defer res.Body.Close()
の問題:defer
は、そのfor
ループのイテレーションが終了するたびに実行されるわけではありません。defer
は、BenchmarkClientServer
関数自体がリターンする直前に実行されるようにスケジュールされます。したがって、b.N
が例えば100万回実行されると、100万個のres.Body.Close()
呼び出しがスタックに蓄積され、関数終了時にまとめて実行されることになります。これは、ベンチマークの実行中にメモリを大量に消費し、ベンチマーク結果を歪める原因となります。- 二重クローズ:
defer
されているにもかかわらず、その直後にもres.Body.Close()
が明示的に呼び出されています。これは、ioutil.ReadAll
がボディを読み終えた直後にリソースを解放するという意図があったためと考えられます。しかし、defer
が存在するため、同じres.Body
に対してClose()
が二度呼び出される可能性がありました。io.Closer
の実装によっては、二度目のクローズがエラーになることは稀ですが、無駄な処理であり、意図しない副作用を引き起こす可能性もゼロではありません。
このコミットは、b.N
ループ内でdefer
を使用することの悪影響を認識し、不要なdefer
文を削除することで、ベンチマークの正確性と効率性を向上させています。ioutil.ReadAll
の後に直接res.Body.Close()
を呼び出すことで、リソースは即座に解放され、defer
による蓄積の問題が解消されます。
コアとなるコードの変更箇所
変更はsrc/pkg/net/http/serve_test.go
ファイルの一箇所のみです。
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2258,7 +2258,6 @@ func BenchmarkClientServer(b *testing.B) {
if err != nil {
b.Fatal("Get:", err)
}
- defer res.Body.Close()
all, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
コアとなるコードの解説
削除された行は以下の通りです。
- defer res.Body.Close()
この一行が削除されたことで、BenchmarkClientServer
関数内のb.N
ループにおいて、res.Body.Close()
の呼び出しがdefer
によってスタックに蓄積されることがなくなりました。
変更後のコードでは、ioutil.ReadAll(res.Body)
の直後にres.Body.Close()
が明示的に呼び出されています。
all, err := ioutil.ReadAll(res.Body)
res.Body.Close() // こちらの呼び出しが残る
if err != nil {
b.Fatal("ReadAll:", err)
}
これにより、HTTPレスポンスボディのリソースは、各ループイテレーション内でボディの読み取りが完了した直後に即座に解放されるようになります。これは、ベンチマークの各イテレーションが独立した状態で行われることを保証し、正確なパフォーマンス測定を可能にします。defer
によるオーバーヘッドがなくなるため、ベンチマークの実行速度も向上し、より信頼性の高い結果が得られるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/b1c5bafda37a21216ee8e6f6d5bcfc6e1db08084
- Go CL 64600044: https://golang.org/cl/64600044
参考にした情報源リンク
- CL 61150043に関するWeb検索結果: "runtime: fix stack growth for large frames" の問題解決とGoランタイムのスタック管理に関する情報。
- Go言語の
defer
文に関する公式ドキュメントやチュートリアル。 - Go言語の
testing
パッケージとベンチマークに関する公式ドキュメント。 - Go言語の
net/http
パッケージとio.ReadCloser
インターフェースに関する公式ドキュメント。