[インデックス 15988] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内の server.go
ファイルに対する変更です。具体的には、HTTPレスポンスを扱う response
構造体において、チャンク転送エンコーディングを処理する chunkWriter
構造体をインライン化(埋め込み)することで、メモリ割り当ての削減とパフォーマンスの向上を図っています。
コミット
commit a891484a4edb241ae1b11f3b336f63fe08b10cb7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Mar 28 13:13:28 2013 -0700
net/http: inline chunkWriter in response
A chunkWriter and a response are 1:1. Make them contiguous in
memory and save an allocation.
benchmark old ns/op new ns/op delta
BenchmarkServerFakeConnWithKeepAliveLite 10715 10539 -1.64%
benchmark old allocs new allocs delta
BenchmarkServerFakeConnWithKeepAliveLite 21 20 -4.76%
benchmark old bytes new bytes delta
BenchmarkServerFakeConnWithKeepAliveLite 1626 1609 -1.05%
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/8114043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a891484a4edb241ae1b11f3b336f63fe08b10cb7
元コミット内容
このコミットの目的は、net/http
パッケージの response
構造体内に chunkWriter
をインライン化することです。これにより、chunkWriter
と response
がメモリ上で連続して配置され、個別のメモリ割り当てが不要になります。結果として、ベンチマークにおいて1操作あたりのナノ秒 (ns/op
)、メモリ割り当て回数 (allocs
)、割り当てバイト数 (bytes
) の全てが削減され、パフォーマンスとメモリ効率が向上しています。
変更の背景
Goの net/http
パッケージは、ウェブサーバーを構築するための基盤を提供します。HTTPレスポンスをクライアントに送信する際、特に大きなボディを持つレスポンスの場合、HTTP/1.1のチャンク転送エンコーディングが使用されることがあります。このチャンク転送を効率的に処理するために chunkWriter
という内部構造体が利用されます。
以前の実装では、response
構造体は chunkWriter
へのポインタ (*chunkWriter
) を保持していました。これは、response
が作成されるたびに chunkWriter
がヒープ上に個別に割り当てられることを意味します。HTTPリクエストごとに response
オブジェクトが生成されるため、この個別の割り当ては、特に高負荷なサーバー環境において、ガベージコレクションの頻度を増やし、パフォーマンスのオーバーヘッドとなる可能性がありました。
このコミットの背景にあるのは、このようなマイクロ最適化を通じて、Goの標準ライブラリのパフォーマンスをさらに向上させるという継続的な取り組みです。chunkWriter
と response
が常に1対1の関係にあるという事実を利用し、これらをメモリ上で連続させることで、不要なポープタ参照とヒープ割り当てを排除し、より効率的なメモリ利用と高速な処理を実現することが目的です。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびHTTPプロトコルに関する知識が役立ちます。
-
Go言語の構造体とポインタ:
- Goの構造体は値型です。構造体を別の構造体のフィールドとして宣言すると、そのフィールドは外側の構造体の一部としてメモリ上に直接配置されます(インライン化)。
- ポインタ (
*T
) は、メモリ上の特定のアドレスを指す変数です。ポインタを使用すると、間接参照が発生し、メモリへのアクセスに追加のステップが必要になります。また、new(T)
のようにポインタを返す関数は、通常、ヒープにメモリを割り当てます。 - ヒープ割り当ては、ガベージコレクタの対象となり、ガベージコレクションの頻度や実行時間はアプリケーションのパフォーマンスに影響を与えます。スタック割り当ては、関数呼び出しの終了とともに自動的に解放されるため、ガベージコレクションの対象外です。
-
HTTP/1.1 チャンク転送エンコーディング:
- HTTP/1.1では、レスポンスボディの長さを事前に知ることができない場合に、チャンク転送エンコーディング (
Transfer-Encoding: chunked
) を使用してボディを送信できます。 - この方式では、ボディは複数の「チャンク」に分割され、各チャンクはサイズ情報とデータを含みます。最後のチャンクはサイズが0で、転送の終了を示します。
net/http
パッケージのchunkWriter
は、このチャンク転送エンコーディングのロジックをカプセル化し、io.Writer
インターフェースを実装しています。これにより、アプリケーションは通常のWrite
操作を行うだけで、内部的にチャンク形式に変換されて送信されます。
- HTTP/1.1では、レスポンスボディの長さを事前に知ることができない場合に、チャンク転送エンコーディング (
-
io.Writer
インターフェース:- Goの標準ライブラリで定義されている基本的なインターフェースの一つで、
Write([]byte) (int, error)
メソッドを持ちます。 - ファイル、ネットワーク接続、バッファなど、様々な出力先にデータを書き込むための抽象化を提供します。
bufio.Writer
やchunkWriter
はこのインターフェースを実装しています。
- Goの標準ライブラリで定義されている基本的なインターフェースの一つで、
-
bufio.Writer
:bufio
パッケージは、バッファリングされたI/Oを提供します。bufio.Writer
は、基になるio.Writer
の上にバッファ層を追加し、小さな書き込み操作をまとめて効率的に処理します。- これにより、基になる
io.Writer
へのシステムコールやネットワークI/Oの回数を減らし、パフォーマンスを向上させます。
-
ベンチマークの指標:
ns/op
(nanoseconds per operation): 1操作あたりの平均実行時間。値が小さいほど高速であることを示します。allocs
(allocations per operation): 1操作あたりのメモリ割り当て回数。値が小さいほどメモリ割り当てのオーバーヘッドが少ないことを示します。bytes
(bytes allocated per operation): 1操作あたりの平均メモリ割り当てバイト数。値が小さいほどメモリ使用量が少ないことを示します。
技術的詳細
このコミットの核心は、Go言語の構造体の埋め込み(インライン化)とメモリ割り当ての最適化にあります。
以前の response
構造体は、chunkWriter
をポインタとして保持していました。
type response struct {
// ...
cw *chunkWriter // chunkWriterへのポインタ
// ...
}
この定義では、response
構造体が作成される際に、cw
フィールド自体はポインタを格納するためのメモリを消費しますが、chunkWriter
の実際のデータは別のメモリ領域(通常はヒープ)に割り当てられ、そのアドレスが cw
に格納されます。new(chunkWriter)
の呼び出しは、この個別のヒープ割り当てを引き起こします。
変更後、response
構造体は chunkWriter
を直接埋め込むようになりました。
type response struct {
// ...
cw chunkWriter // chunkWriterを直接埋め込み
// ...
}
この変更により、response
構造体がメモリに割り当てられる際(スタック上またはヒープ上)、chunkWriter
の全てのフィールドも response
構造体の一部として連続したメモリ領域に配置されます。これにより、以下の利点が得られます。
- メモリ割り当ての削減:
chunkWriter
のための個別のnew
呼び出しとそれに伴うヒープ割り当てが不要になります。response
構造体全体の割り当てが1回行われるだけで、chunkWriter
のメモリも同時に確保されます。これにより、ガベージコレクタの負担が軽減され、全体的なパフォーマンスが向上します。ベンチマーク結果のallocs
が21から20に減少しているのは、この1回のアロケーション削減を明確に示しています。 - キャッシュ効率の向上:
response
とchunkWriter
がメモリ上で連続して配置されるため、CPUのキャッシュ効率が向上します。response
のデータにアクセスする際にchunkWriter
のデータも同時にキャッシュにロードされる可能性が高まり、メモリアクセスのレイテンシが減少します。 - ポインタの間接参照の排除:
cw
がポインタではなくなったため、response
からchunkWriter
のフィールドにアクセスする際にポインタの間接参照が不要になります。これはわずかながらCPUサイクルを節約し、命令キャッシュの効率も向上させます。
また、newBufioWriterSize
関数に渡す引数も変更されています。以前は w.cw
(ポインタ) を渡していましたが、変更後は &w.cw
(埋め込まれた chunkWriter
のアドレス) を渡しています。これは、newBufioWriterSize
が io.Writer
インターフェースを期待しており、chunkWriter
がそのインターフェースを実装しているため、chunkWriter
の値のアドレスを渡す必要があるためです。
ベンチマーク結果は、この変更が実際にパフォーマンスに寄与していることを示しています。
BenchmarkServerFakeConnWithKeepAliveLite
のns/op
が10715
から10539
へと1.64%
減少。allocs
が21
から20
へと4.76%
減少。bytes
が1626
から1609
へと1.05%
減少。
これらの数値は、この比較的小さな変更が、高頻度で実行されるHTTPサーバーのコアパスにおいて、測定可能なパフォーマンス改善をもたらしたことを裏付けています。
コアとなるコードの変更箇所
変更は src/pkg/net/http/server.go
ファイルの2箇所です。
-
response
構造体のcw
フィールドの型変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -288,7 +288,7 @@ type response struct { wroteContinue bool // 100 Continue response was written w *bufio.Writer // buffers output in chunks to chunkWriter - cw *chunkWriter + cw chunkWriter sw *switchWriter // of the bufio.Writer, for return to putBufioWriter
cw
フィールドの型が*chunkWriter
(ポインタ) からchunkWriter
(値) に変更されました。 -
readRequest
関数内でのchunkWriter
の初期化とbufio.Writer
への渡し方:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -558,10 +558,9 @@ func (c *conn) readRequest() (w *response, err error) {\n req: req,\n handlerHeader: make(Header),\n contentLength: -1,\n - cw: new(chunkWriter),\n }\n w.cw.res = w - w.w, w.sw = newBufioWriterSize(w.cw, bufferBeforeChunkingSize) + w.w, w.sw = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize) return w, nil }
response
構造体の初期化からcw: new(chunkWriter),
の行が削除されました。cw
はresponse
構造体の一部として自動的にゼロ値で初期化されるため、明示的なnew
呼び出しは不要になります。newBufioWriterSize
関数に渡す引数がw.cw
から&w.cw
に変更されました。これは、newBufioWriterSize
がio.Writer
インターフェースを実装するオブジェクトを期待しており、chunkWriter
は値型としてio.Writer
を実装しているため、そのアドレスを渡す必要があるためです。
コアとなるコードの解説
このコミットの変更は、Go言語における構造体の埋め込みと、それによるメモリ管理の最適化の典型的な例を示しています。
response
構造体は、HTTPレスポンスの構築と送信に必要な様々な情報をカプセル化しています。その中には、レスポンスボディの書き込みを処理する io.Writer
のチェーンが含まれます。具体的には、*bufio.Writer
が chunkWriter
に書き込み、chunkWriter
が最終的に基になるネットワーク接続に書き込みます。
変更前は、response
構造体は chunkWriter
へのポインタ (*chunkWriter
) を保持していました。これは、response
オブジェクトが作成されるたびに、chunkWriter
のインスタンスがヒープに別途割り当てられ、そのポインタが response
の cw
フィールドに格納されることを意味します。
// 変更前: cwはポインタ
type response struct {
// ...
cw *chunkWriter // ヒープに割り当てられたchunkWriterへのポインタ
// ...
}
func (c *conn) readRequest() (w *response, err error) {
w = &response{
// ...
cw: new(chunkWriter), // ここでヒープ割り当てが発生
}
w.cw.res = w
w.w, w.sw = newBufioWriterSize(w.cw, bufferBeforeChunkingSize) // ポインタを渡す
return w, nil
}
変更後は、response
構造体内に chunkWriter
が直接埋め込まれるようになりました。
// 変更後: cwは値
type response struct {
// ...
cw chunkWriter // response構造体の一部としてメモリに直接配置される
// ...
}
func (c *conn) readRequest() (w *response, err error) {
w = &response{
// ...
// cw: new(chunkWriter), // この行は不要になり削除
}
w.cw.res = w // cwはresponseの一部なので、直接アクセス
w.w, w.sw = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize) // cwのアドレスを渡す
return w, nil
}
この変更により、response
構造体が割り当てられる際に、chunkWriter
のメモリも同時に確保されます。これにより、chunkWriter
のための個別のヒープ割り当てが不要となり、メモリ割り当ての回数が1回削減されます。これは、ベンチマーク結果の allocs
の減少に直接的に反映されています。
また、newBufioWriterSize
関数は io.Writer
インターフェースを引数として受け取ります。Goでは、値型がインターフェースを実装している場合、その値のアドレス(ポインタ)をインターフェース型として渡すことができます。chunkWriter
は io.Writer
インターフェースを実装しているため、変更後は &w.cw
(埋め込まれた chunkWriter
のアドレス) を渡すことで、bufio.Writer
が chunkWriter
を基になるライターとして使用できるようになります。
この最適化は、HTTPサーバーのような高スループットが求められるアプリケーションにおいて、小さなメモリ割り当ての削減が全体的なパフォーマンスに大きな影響を与える可能性があることを示しています。特に、ガベージコレクションの頻度を減らすことは、アプリケーションのレイテンシとスループットの安定性にとって重要です。
関連リンク
- Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
bufio
パッケージのドキュメント: https://pkg.go.dev/bufio - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - HTTP/1.1 チャンク転送エンコーディング (RFC 7230, Section 4.1): https://datatracker.ietf.org/doc/html/rfc7230#section-4.1
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (
src/pkg/net/http/server.go
) - Go言語のベンチマークに関する一般的な知識
- Go言語におけるメモリ管理とガベージコレクションに関する一般的な知識
- HTTP/1.1 プロトコル仕様 (RFC 7230)