[インデックス 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 putBufioWritercwフィールドの型が*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)