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

[インデックス 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()という二重のクローズ処理が行われていました。

  1. defer res.Body.Close()の問題: deferは、そのforループのイテレーションが終了するたびに実行されるわけではありません。deferは、BenchmarkClientServer関数自体がリターンする直前に実行されるようにスケジュールされます。したがって、b.Nが例えば100万回実行されると、100万個のres.Body.Close()呼び出しがスタックに蓄積され、関数終了時にまとめて実行されることになります。これは、ベンチマークの実行中にメモリを大量に消費し、ベンチマーク結果を歪める原因となります。
  2. 二重クローズ: 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インターフェースに関する公式ドキュメント。