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

[インデックス 16348] ファイルの概要

このコミットは、Go言語のnet/httpパッケージにおけるHTTPボディの転送処理を簡素化し、それに伴うメモリ割り当て(allocation)の削減とパフォーマンスの向上を目的としています。具体的には、eofReaderの実装を改善し、body構造体から不要なフィールドを削除することで、HTTPリクエスト/レスポンスボディの読み込みロジックを効率化しています。

コミット

commit 27f7427995782bf60195ca65fd9b44aa34913b75
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon May 20 07:23:59 2013 -0700

    net/http: simplify transfer body; reduces allocations too
    
    benchmark                                   old ns/op    new ns/op    delta
    BenchmarkServerFakeConnNoKeepAlive              14431        14247   -1.28%
    BenchmarkServerFakeConnWithKeepAlive            11618        11357   -2.25%
    BenchmarkServerFakeConnWithKeepAliveLite         6735         6427   -4.57%
    BenchmarkServerHandlerTypeLen                    8842         8740   -1.15%
    BenchmarkServerHandlerNoLen                      8001         7828   -2.16%
    BenchmarkServerHandlerNoType                     8270         8227   -0.52%
    BenchmarkServerHandlerNoHeader                   6148         5920   -3.71%
    
    benchmark                                  old allocs   new allocs    delta
    BenchmarkServerFakeConnNoKeepAlive                 30           29   -3.33%
    BenchmarkServerFakeConnWithKeepAlive               25           24   -4.00%
    BenchmarkServerFakeConnWithKeepAliveLite           10            9  -10.00%
    BenchmarkServerHandlerTypeLen                      18           17   -5.56%
    BenchmarkServerHandlerNoLen                        15           14   -6.67%
    BenchmarkServerHandlerNoType                       16           15   -6.25%
    BenchmarkServerHandlerNoHeader                     10            9  -10.00%
    
    benchmark                                   old bytes    new bytes    delta
    BenchmarkServerFakeConnNoKeepAlive               2557         2492   -2.54%
    BenchmarkServerFakeConnWithKeepAlive             2260         2194   -2.92%
    BenchmarkServerFakeConnWithKeepAliveLite         1092         1026   -6.04%
    BenchmarkServerHandlerTypeLen                    1941         1875   -3.40%
    BenchmarkServerHandlerNoLen                      1898         1832   -3.48%
    BenchmarkServerHandlerNoType                     1906         1840   -3.46%
    BenchmarkServerHandlerNoHeader                   1092         1026   -6.04%
    
    Update #5195
    
    R=golang-dev, daniel.morsing
    CC=golang-dev
    https://golang.org/cl/9492044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/27f7427995782bf60195ca65fd9b44aa34913b75

元コミット内容

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージにおいて、HTTPボディの転送処理を簡素化し、それに伴うメモリ割り当て(allocation)を削減することを目的としています。コミットメッセージには、様々なベンチマーク結果が示されており、処理時間(ns/op)、メモリ割り当て回数(allocs)、割り当てられたバイト数(bytes)の全てにおいて改善が見られることが報告されています。特に、メモリ割り当て回数とバイト数の削減は、ガベージコレクションの負荷軽減に繋がり、全体的なパフォーマンス向上に寄与します。

変更の背景

Goのnet/httpパッケージは、ウェブサーバーやクライアントを構築するための基盤であり、そのパフォーマンスはGoアプリケーション全体の効率に直結します。特に、HTTPリクエストやレスポンスのボディを処理する部分は、データ量が多い場合にボトルネックとなりやすい箇所です。

このコミットが行われた背景には、以下の課題意識があったと考えられます。

  1. メモリ割り当ての最適化: Goのガベージコレクタは非常に効率的ですが、不要なメモリ割り当ては依然としてパフォーマンスオーバーヘッドの原因となります。特に、リクエスト/レスポンスごとに頻繁に発生する小さなオブジェクトの割り当ては、ガベージコレクションの頻度を増やし、アプリケーションのレイテンシに影響を与える可能性があります。
  2. コードの簡素化と可読性: 複雑なコードはバグの温床となりやすく、メンテナンスコストも高くなります。HTTPボディの転送処理は、チャンク転送、Content-Length、Keep-Aliveなど、様々なケースを考慮する必要があるため、コードが複雑になりがちです。これを簡素化することで、将来的な開発やデバッグが容易になります。
  3. パフォーマンスの向上: 上記のメモリ割り当ての最適化とコードの簡素化は、結果として処理時間の短縮に繋がります。ベンチマーク結果が示すように、この変更によってHTTPサーバーの処理性能が向上しています。

特に、eofReaderの改善は、ボディが存在しないHTTPリクエスト/レスポンス(例: GETリクエストのレスポンスボディがない場合や、HEADリクエストなど)において、不必要なio.Readerのラッパーオブジェクトの生成を避けることで、メモリ割り当てを削減し、処理を効率化することを意図しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびHTTPプロトコルに関する前提知識が必要です。

  1. Go言語のioパッケージ:

    • io.Readerインターフェース: データを読み込むための基本的なインターフェースで、Read([]byte) (n int, err error)メソッドを持ちます。ファイル、ネットワーク接続、メモリ上のデータなど、様々なソースからの読み込みを抽象化します。
    • io.Closerインターフェース: リソースを閉じるためのインターフェースで、Close() errorメソッドを持ちます。ファイルハンドルやネットワーク接続など、使用後に解放する必要があるリソースに実装されます。
    • io.ReadCloserインターフェース: io.Readerio.Closerの両方を満たすインターフェースです。
    • io.LimitReader: 指定されたバイト数までしか読み込まないio.Readerを返します。HTTPのContent-Lengthヘッダで指定されたボディサイズを読み込む際に利用されます。
    • ioutil.NopCloser: 任意のio.Readerio.ReadCloserに変換する関数です。Closeメソッドは何もしません。
    • strings.NewReader: 文字列からデータを読み込むio.Readerを生成します。
  2. HTTPプロトコルにおけるボディ転送:

    • Content-Length: HTTPメッセージボディの長さをバイト単位で示すヘッダです。クライアントとサーバーは、この値に基づいてボディの読み込みを終了します。
    • Transfer-Encoding: chunked: ボディの長さを事前に知ることができない場合に用いられる転送エンコーディングです。ボディは複数の「チャンク」に分割され、各チャンクの前にそのサイズが記述されます。ボディの終端は、サイズが0のチャンクで示されます。
    • Keep-Alive: 複数のHTTPリクエスト/レスポンスを同じTCP接続上で送受信することを可能にするメカニズムです。これにより、接続の確立と切断のオーバーヘッドを削減し、パフォーマンスを向上させます。
    • ボディが存在しないケース: GETリクエストのレスポンスボディがない場合や、HEADリクエスト、OPTIONSリクエストなど、HTTPメソッドによってはボディが存在しないことがあります。この場合、io.Readerは即座にEOF(End Of File)を返す必要があります。
  3. Go言語のベンチマーク:

    • Goには、コードのパフォーマンスを測定するための組み込みのベンチマークツールがあります。go test -bench=.コマンドで実行され、ns/op(操作あたりのナノ秒)、allocs(操作あたりのメモリ割り当て回数)、bytes(操作あたりのメモリ割り当てバイト数)などの指標が出力されます。これらの指標は、コードの最適化において非常に重要です。

技術的詳細

このコミットの主要な変更点は、net/httpパッケージ内のeofReader変数の定義と、transfer.goにおけるbody構造体の利用方法です。

eofReaderの変更

変更前、eofReaderは以下のように定義されていました。

var eofReader = ioutil.NopCloser(strings.NewReader(""))

これは、空文字列を読み込むstrings.NewReaderioutil.NopCloserでラップしたもので、常にEOFを返すio.ReadCloserとして機能していました。しかし、この実装では、ioutil.NopCloserが内部で新しいio.ReadCloserインターフェース型のアロケーションを発生させていました。

変更後、eofReaderは以下のように定義されました。

var eofReader = &struct {
	*strings.Reader
	io.Closer
}{
	strings.NewReader(""),
	ioutil.NopCloser(nil),
}

この変更により、eofReaderは匿名構造体へのポインタとなり、*strings.Readerio.Closerを直接埋め込む形になりました。

  • *strings.Readerを埋め込むことで、io.CopyWriteToメソッドを利用できるようになり、バッファを必要とせずに効率的なコピーが可能になります。strings.Readerio.Readerインターフェースを満たし、かつWriteToメソッドも持っています。
  • ioutil.NopCloser(nil)は、Closeメソッドが何もしないio.Closerを返します。以前のioutil.NopCloser(strings.NewReader(""))のようにstrings.NewReaderをラップするのではなく、直接nilを渡すことで、ioutil.NopCloserが内部で余分なio.Readerの参照を持たなくなり、よりシンプルになります。

この新しいeofReaderの定義は、単一の静的変数として初期化されるため、HTTPボディが存在しない場合に毎回新しいio.ReadCloserオブジェクトを割り当てる必要がなくなります。これにより、メモリ割り当てが削減され、ガベージコレクションの負荷が軽減されます。

transfer.goにおけるbody構造体の利用方法の変更

src/pkg/net/http/transfer.goreadTransfer関数では、HTTPボディの読み込み方法を決定し、適切なio.Readert.Bodyに設定していました。

変更前は、ボディが存在しない場合や、Content-Lengthが0の場合に、t.Body&body{Reader: eofReader, closing: t.Close}のようにbody構造体のインスタンスを割り当てていました。

// 変更前 (transfer.go)
case chunked(t.TransferEncoding):
    if noBodyExpected(t.RequestMethod) {
        t.Body = &body{Reader: eofReader, closing: t.Close}
    } else {
        t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
    }
case realLength == 0:
    t.Body = &body{Reader: eofReader, closing: t.Close}
// ...
else {
    // Persistent connection (i.e. HTTP/1.1)
    t.Body = &body{Reader: eofReader, closing: t.Close}
}

変更後、これらのケースでは直接t.Body = eofReaderと設定されるようになりました。

// 変更後 (transfer.go)
case chunked(t.TransferEncoding):
    if noBodyExpected(t.RequestMethod) {
        t.Body = eofReader
    } else {
        t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
    }
case realLength == 0:
    t.Body = eofReader
// ...
else {
    // Persistent connection (i.e. HTTP/1.1)
    t.Body = eofReader
}

これは、eofReader自体がio.ReadCloserインターフェースを満たすように変更されたため、body構造体でラップする必要がなくなったためです。body構造体は、チャンク転送やContent-Lengthを持つボディなど、実際のデータ読み込みが必要な場合にのみ使用されるようになりました。これにより、ボディが存在しない場合にbody構造体のインスタンスを割り当てるオーバーヘッドが削減されます。

body構造体からのフィールド削除

src/pkg/net/http/transfer.gobody構造体から、res *responseフィールドが削除されました。

// 変更前 (transfer.go)
type body struct {
	Reader  io.Reader // underlying wire-format reader for the trailer
	r       *bufio.Reader // underlying wire-format reader for the trailer
	closing bool          // is the connection to be closed after reading body?
	closed  bool
	res *response // response writer for server requests, else nil
}

// 変更後 (transfer.go)
type body struct {
	Reader  io.Reader // underlying wire-format reader for the trailer
	r       *bufio.Reader // underlying wire-format reader for the trailer
	closing bool          // is the connection to be closed after reading body?
	closed  bool
}

このresフィールドは、サーバーリクエストのレスポンスライターへの参照を保持していましたが、コミットメッセージやコードの変更内容から判断すると、このフィールドがbodyCloseメソッド内で特定のロジック(requestBodyLimitHitのチェックなど)のために使用されていたようです。しかし、このロジックが不要になったか、別の方法で処理されるようになったため、フィールドが削除されました。これにより、body構造体のサイズが小さくなり、そのインスタンスが割り当てられる際のメモリ使用量がわずかに削減されます。

また、body.Close()メソッド内の以下の条件分岐が削除されました。

// 削除されたコード (transfer.go)
case b.res != nil && b.res.requestBodyLimitHit:
    // In a server request, don't continue reading from the client
    // if we've already hit the maximum body size set by the
    // handler. If this is set, that also means the TCP connection
    // is about to be closed, so getting to the next HTTP request
    // in the stream is not necessary.
case b.Reader == eofReader:
    // Nothing to read. No need to io.Copy from it.

b.res != nil && b.res.requestBodyLimitHitの条件は、resフィールドの削除に伴い不要になりました。 b.Reader == eofReaderの条件は、eofReaderが直接t.Bodyに設定されるようになったため、body構造体のReaderフィールドがeofReaderであるケースがなくなったため削除されました。これにより、Closeメソッドのロジックが簡素化されています。

これらの変更は、HTTPボディの転送処理における不要なオブジェクトの生成を削減し、コードパスを簡素化することで、全体的なパフォーマンスと効率を向上させています。

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

diff --git a/src/pkg/net/http/server.go b/src/pkg/net/http/server.go
index fe35562447..698d3f9d46 100644
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -1814,7 +1814,15 @@ func (globalOptionsHandler) ServeHTTP(w ResponseWriter, r *Request) {
 }
 
 // eofReader is a non-nil io.ReadCloser that always returns EOF.
-var eofReader = ioutil.NopCloser(strings.NewReader(""))
+// It embeds a *strings.Reader so it still has a WriteTo method
+// and io.Copy won't need a buffer.
+var eofReader = &struct {
+	*strings.Reader
+	io.Closer
+}{
+	strings.NewReader(""),
+	ioutil.NopCloser(nil),
+}
  
 // initNPNRequest is an HTTP handler that initializes certain
 // uninitialized fields in its *Request. Such partially-initialized
diff --git a/src/pkg/net/http/transfer.go b/src/pkg/net/http/transfer.go
index 53569bcc2f..b97f7160f4 100644
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -328,12 +328,12 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 	switch {
 	case chunked(t.TransferEncoding):
 		if noBodyExpected(t.RequestMethod) {
-\t\t\tt.Body = &body{Reader: eofReader, closing: t.Close}
+\t\t\tt.Body = eofReader
 		} else {
 			t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
 		}
 	case realLength == 0:
-\t\t\tt.Body = &body{Reader: eofReader, closing: t.Close}
+\t\t\tt.Body = eofReader
 	case realLength > 0:
 		t.Body = &body{Reader: io.LimitReader(r, realLength), closing: t.Close}
 	default:
@@ -343,7 +343,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
 		\tt.Body = &body{Reader: r, closing: t.Close}
 		} else {
 			// Persistent connection (i.e. HTTP/1.1)
-\t\t\tt.Body = &body{Reader: eofReader, closing: t.Close}
+\t\t\tt.Body = eofReader
 		}
 	}\n 
@@ -518,8 +518,6 @@ type body struct {
 	r       *bufio.Reader // underlying wire-format reader for the trailer
 	closing bool          // is the connection to be closed after reading body?
 	closed  bool
-\n-\tres *response // response writer for server requests, else nil
 }
 
 // ErrBodyReadAfterClose is returned when reading a Request or Response
@@ -618,14 +616,6 @@ func (b *body) Close() error {\n 	case b.hdr == nil && b.closing:\n 	\t// no trailer and closing the connection next.\n 	\t// no point in reading to EOF.\n-\tcase b.res != nil && b.res.requestBodyLimitHit:\n-\t\t// In a server request, don\'t continue reading from the client\n-\t\t// if we\'ve already hit the maximum body size set by the\n-\t\t// handler. If this is set, that also means the TCP connection\n-\t\t// is about to be closed, so getting to the next HTTP request\n-\t\t// in the stream is not necessary.\n-\tcase b.Reader == eofReader:\n-\t\t// Nothing to read. No need to io.Copy from it.\n \tdefault:\n \t\t// Fully consume the body, which will also lead to us reading\n \t\t// the trailer headers after the body, if present.\n```

## コアとなるコードの解説

### `src/pkg/net/http/server.go`の変更

*   **`eofReader`変数の再定義**:
    *   変更前は`ioutil.NopCloser(strings.NewReader(""))`という形式で、`io.ReadCloser`インターフェースを満たすオブジェクトを生成していました。これは、空の文字列を読み込むリーダーを`NopCloser`(`Close`メソッドが何もしない)でラップしたものです。
    *   変更後は、匿名構造体へのポインタ`&struct{...}{...}`として定義されています。この匿名構造体は、`*strings.Reader`と`io.Closer`を埋め込んでいます。
        *   `strings.NewReader("")`は、空文字列を読み込む`*strings.Reader`のインスタンスを生成します。`*strings.Reader`は`io.Reader`インターフェースを満たすだけでなく、`io.WriterTo`インターフェースも満たします。これにより、`io.Copy`が`WriteTo`メソッドを利用して、バッファを介さずに直接データをコピーできるようになり、効率が向上します。
        *   `ioutil.NopCloser(nil)`は、`Close`メソッドが何もしない`io.Closer`のインスタンスを生成します。以前のように`strings.NewReader("")`をラップするのではなく、直接`nil`を渡すことで、`NopCloser`が余分な参照を持たなくなり、よりシンプルになります。
    *   この変更により、`eofReader`は単一の静的オブジェクトとして存在し、HTTPボディが存在しない場合に毎回新しい`io.ReadCloser`オブジェクトを割り当てる必要がなくなります。これにより、メモリ割り当てが削減されます。

### `src/pkg/net/http/transfer.go`の変更

*   **`readTransfer`関数内の`t.Body`への割り当ての簡素化**:
    *   `readTransfer`関数は、HTTPリクエスト/レスポンスのボディをどのように読み込むかを決定する重要な関数です。
    *   変更前は、ボディが存在しない場合(`noBodyExpected`が真の場合、`realLength == 0`の場合、または永続接続でボディがない場合)に、`t.Body`に`&body{Reader: eofReader, closing: t.Close}`という形式で`body`構造体の新しいインスタンスを割り当てていました。
    *   変更後は、これらのケースで直接`t.Body = eofReader`と設定されるようになりました。これは、`eofReader`自体が`io.ReadCloser`インターフェースを満たすように再定義されたため、`body`構造体でラップする必要がなくなったためです。これにより、ボディが存在しない場合に`body`構造体のインスタンスを割り当てるオーバーヘッドが削減されます。
*   **`body`構造体からの`res *response`フィールドの削除**:
    *   `body`構造体は、HTTPボディの読み込み状態を管理するためのものです。
    *   変更前は、`res *response`というフィールドを持っていました。これは、サーバーリクエストのレスポンスライターへの参照を保持していました。
    *   変更後は、この`res`フィールドが削除されました。これにより、`body`構造体のサイズが小さくなり、そのインスタンスが割り当てられる際のメモリ使用量がわずかに削減されます。このフィールドが不要になったのは、関連するロジックが変更されたか、別の方法で処理されるようになったためと考えられます。
*   **`body.Close()`メソッド内の条件分岐の削除**:
    *   `body.Close()`メソッドは、HTTPボディの読み込みを終了し、必要に応じて残りのデータを消費するためのものです。
    *   変更前は、`b.res != nil && b.res.requestBodyLimitHit`と`b.Reader == eofReader`という2つの条件分岐がありました。
        *   `b.res != nil && b.res.requestBodyLimitHit`の条件は、`res`フィールドの削除に伴い不要になりました。
        *   `b.Reader == eofReader`の条件は、`eofReader`が直接`t.Body`に設定されるようになったため、`body`構造体の`Reader`フィールドが`eofReader`であるケースがなくなったため削除されました。
    *   これらの条件分岐の削除により、`Close`メソッドのロジックが簡素化され、実行パスが短縮されます。

これらの変更は全体として、HTTPボディの転送処理における不要なオブジェクトの生成を削減し、コードパスを簡素化することで、メモリ割り当ての削減とパフォーマンスの向上を実現しています。

## 関連リンク

*   Go Issue: [#5195](https://github.com/golang/go/issues/5195) (このコミットが解決した、または関連するGoのIssue)
*   Gerrit Change: [https://golang.org/cl/9492044](https://golang.org/cl/9492044) (このコミットの元のGerritレビューページ)

## 参考にした情報源リンク

*   Go言語の`io`パッケージに関する公式ドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
*   Go言語の`net/http`パッケージに関する公式ドキュメント: [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
*   Go言語のベンチマークに関する公式ドキュメント: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
*   HTTP/1.1仕様 (RFC 2616): [https://datatracker.ietf.org/doc/html/rfc2616](https://datatracker.ietf.org/doc/html/rfc2616) (特にContent-Length, Transfer-Encoding, Keep-Aliveに関するセクション)
*   Go言語のガベージコレクションに関する情報 (一般的な概念): [https://go.dev/doc/gc-guide](https://go.dev/doc/gc-guide) (GoのGCの仕組みを理解するのに役立ちます)
*   `ioutil.NopCloser`のドキュメント: [https://pkg.go.dev/io/ioutil#NopCloser](https://pkg.go.dev/io/ioutil#NopCloser)
*   `strings.NewReader`のドキュメント: [https://pkg.go.dev/strings#NewReader](https://pkg.go.dev/strings#NewReader)
*   `io.Copy`のドキュメント: [https://pkg.go.dev/io#Copy](https://pkg.go.dev/io#Copy) (特に`WriterTo`インターフェースの利用について)