[インデックス 14424] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージおよび net/http/httputil
パッケージにおけるHTTPチャンク転送エンコーディングの読み書き処理において、メモリ割り当て(アロケーション)を削減することを目的としています。具体的には、チャンクの終端を示すCRLF(Carriage Return, Line Feed)の読み取り時と、チャンクサイズをヘッダとして書き出す際に発生する一時的なメモリ割り当てを排除し、パフォーマンスの向上とガベージコレクション(GC)負荷の軽減を図っています。
コミット
commit f3e6b2060679a6f430c9e711cf797d76d4226a15
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Nov 16 13:25:01 2012 -0800
net/http: reduce allocations in chunk reading & writing
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6847063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f3e6b2060679a6f430c9e711cf797d76d4226a15
元コミット内容
net/http: reduce allocations in chunk reading & writing
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6847063
変更の背景
この変更の主な背景は、Goプログラムのパフォーマンス最適化とガベージコレクション(GC)の効率化です。
HTTP/1.1では、メッセージボディの長さを事前に知ることができない場合に「チャンク転送エンコーディング (Chunked Transfer Encoding)」を使用します。これは、メッセージボディを複数の「チャンク」に分割し、各チャンクの前にそのサイズ(16進数表記)とCRLFを付加し、最後にゼロサイズのチャンクとCRLFで終了するという仕組みです。
Goの net/http
パッケージは、このチャンク転送エンコーディングを透過的に処理します。しかし、以前の実装では、チャンクの読み取り時(特に各チャンクの終端にあるCRLFを読み取る際)や、チャンクサイズを文字列として書き出す際に、一時的なバイトスライスや文字列がヒープ上に頻繁に割り当てられていました。
Goのガベージコレクタは、ヒープ上に割り当てられたオブジェクトを追跡し、不要になったものを回収します。一時的なオブジェクトが大量に生成されると、GCが頻繁に実行され、そのオーバーヘッドがアプリケーションのパフォーマンスに影響を与える可能性があります。特に、HTTPサーバーのような高スループットが求められるアプリケーションでは、わずかなアロケーションの削減でも全体的なパフォーマンスに大きな改善をもたらすことがあります。
このコミットは、これらの小さな、しかし頻繁に発生するアロケーションを排除することで、GCの負荷を軽減し、HTTPチャンク処理の効率を向上させることを目的としています。
前提知識の解説
1. HTTP チャンク転送エンコーディング (Chunked Transfer Encoding)
HTTP/1.1の機能の一つで、メッセージボディの長さを事前に決定できない場合(例: 動的に生成されるコンテンツ、ストリーミングデータ)に利用されます。
-
仕組み:
- メッセージボディは複数の「チャンク」に分割されます。
- 各チャンクの前に、そのチャンクのデータサイズ(バイト数)が16進数で記述され、その後にCRLF (
\r\n
) が続きます。 - 次に実際のチャンクデータが続き、その後にCRLF (
\r\n
) が続きます。 - 最後のチャンクはサイズが
0
であり、その後にCRLF (\r\n
) が続きます。 - 最後に、オプションのトレーラーヘッダ(追加のHTTPヘッダ)が続き、その後にCRLF (
\r\n
) が2回続きます。
-
例:
4\r\n (サイズ: 4バイト) Wiki\r\n (データ: "Wiki") 5\r\n (サイズ: 5バイト) pedia\r\n (データ: "pedia") E\r\n (サイズ: 14バイト) in\r\nchunks.\r\n (データ: " in\r\nchunks.") 0\r\n (最後のチャンク、サイズ0) \r\n (トレーラーヘッダなし、終端)
2. Go言語のメモリ管理とガベージコレクション (GC)
Goは自動メモリ管理(ガベージコレクション)を採用しています。
- ヒープとスタック:
- スタック: 関数呼び出し、ローカル変数、関数の引数などが格納される領域。サイズがコンパイル時に決定できる、または非常に小さい一時的なデータが置かれます。スタック上のメモリは関数が終了すると自動的に解放されます。アロケーションと解放が非常に高速です。
- ヒープ: プログラム実行中に動的にサイズが変化するデータ(例: スライス、マップ、チャネル、構造体のポインタなど)が格納される領域。ヒープ上のメモリはGCによって管理されます。
- アロケーション:
make
やnew
を使って作成されるデータは通常ヒープに割り当てられます。 - ガベージコレクション (GC): ヒープ上に割り当てられたオブジェクトのうち、どのオブジェクトも参照されなくなった(到達不可能になった)ものを自動的に検出し、そのメモリを再利用可能にするプロセスです。
- GCのオーバーヘッド: GCはプログラムの実行を一時停止させたり(Stop-the-World)、バックグラウンドで実行されたりするため、その実行頻度や時間がパフォーマンスに影響を与えます。ヒープアロケーションが頻繁に発生すると、GCがより頻繁に実行される必要があり、これがアプリケーションのレイテンシやスループットに悪影響を及ぼす可能性があります。
- アロケーションの削減の重要性: ヒープアロケーションを減らすことは、GCの実行回数を減らし、GCが処理すべきオブジェクトの数を減らすことにつながります。これにより、GCのオーバーヘッドが減少し、アプリケーション全体のパフォーマンスが向上します。
3. Goの io
パッケージと fmt
パッケージ
io.Reader
/io.Writer
: GoにおけるI/O操作の基本的なインターフェース。データを読み書きするための抽象化を提供します。io.ReadFull(r Reader, buf []byte) (n int, err error)
:r
からbuf
のサイズ分だけデータを読み込もうとします。buf
が完全に埋まるまで読み込むか、エラーが発生するまでブロックします。io.WriteString(w Writer, s string) (n int, err error)
: 文字列s
をw
に書き込みます。fmt.Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
:format
文字列に従ってフォーマットされたデータをw
に書き込みます。printf
スタイルのフォーマットをサポートし、直接io.Writer
に書き込むため、中間的な文字列生成を避けることができます。
4. strconv
パッケージ
strconv.FormatInt(i int64, base int) string
: 整数i
を指定されたbase
(基数)で文字列に変換します。この関数は新しい文字列を生成するため、ヒープアロケーションが発生します。
技術的詳細
このコミットは、HTTPチャンク転送エンコーディングの処理における2つの主要なアロケーションポイントをターゲットにしています。
1. チャンク読み取り時のCRLF処理の最適化
HTTPチャンクの各データブロックの終わりには、チャンクデータと次のチャンクサイズの間を区切るCRLF (\r\n
) が存在します。以前の実装では、この2バイトのCRLFを読み取るために、毎回 make([]byte, 2)
を使用して新しい2バイトのスライスをヒープ上に割り当てていました。
このコミットでは、chunkedReader
構造体に buf [2]byte
という固定サイズの配列を追加しました。これはスタック上に割り当てられるか、構造体の一部としてヒープに割り当てられたとしても、そのサイズは固定であり、読み取りごとに新しいスライスを動的に割り当てる必要がなくなります。
io.ReadFull(cr.r, cr.buf[:])
のように、この構造体内の固定配列のスライス (cr.buf[:]
) を io.ReadFull
に渡すことで、ヒープアロケーションなしでCRLFを読み取ることが可能になります。これにより、チャンクが多数存在するHTTPレスポンスを処理する際に発生する、頻繁な2バイトスライスの割り当てとGCの負荷が大幅に削減されます。
2. チャンク書き込み時のサイズヘッダ処理の最適化
HTTPチャンクを書き出す際には、各チャンクデータの前にそのサイズを16進数で記述した文字列とCRLFを付加する必要があります。以前の実装では、strconv.FormatInt
を使用してチャンクサイズを文字列に変換し、その後に io.WriteString
を使用してその文字列を書き込んでいました。
strconv.FormatInt
は、変換結果の文字列をヒープ上に割り当てます。チャンクごとにこの処理が行われるため、書き込み時にも頻繁な文字列アロケーションが発生していました。
このコミットでは、fmt.Fprintf(cw.Wire, "%x\r\n", len(data))
を使用するように変更しました。fmt.Fprintf
は、io.Writer
インターフェースを実装する任意のオブジェクトに直接フォーマットされた文字列を書き込むことができます。%x
フォーマット指定子は整数を16進数で出力するために使用されます。
fmt.Fprintf
の内部実装は、多くの場合、中間的な文字列バッファを再利用したり、直接バイト列として書き込んだりすることで、strconv.FormatInt
で文字列を生成し、その後に io.WriteString
で書き込むよりも効率的に処理を行うことができます。これにより、チャンクサイズを表す文字列のヒープアロケーションが削減され、書き込み処理のパフォーマンスが向上します。
これらの変更は、Goの net/http
パッケージが提供するHTTPサーバーやクライアントが、チャンク転送エンコーディングを扱う際のメモリ効率を大幅に改善し、特に高負荷な環境下でのGCによるパフォーマンス低下を抑制する効果があります。
コアとなるコードの変更箇所
src/pkg/net/http/chunked.go
および src/pkg/net/http/httputil/chunked.go
両ファイルで同様の変更が行われています。
1. chunkedReader
構造体へのフィールド追加
--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -39,6 +40,7 @@ type chunkedReader struct {
r *bufio.Reader
n uint64 // unread bytes in chunk
err error
+ buf [2]byte // 追加
}
2. chunkedReader.Read
メソッド内のCRLF読み取り部分の変更
--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -74,9 +76,8 @@ func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
cr.n -= uint64(n)
if cr.n == 0 && cr.err == nil {
// end of chunk (CRLF)
- b := make([]byte, 2) // 削除: 新しいスライスのアロケーション
- if _, cr.err = io.ReadFull(cr.r, b); cr.err == nil {
- if b[0] != '\r' || b[1] != '\n' {
+ if _, cr.err = io.ReadFull(cr.r, cr.buf[:]); cr.err == nil { // 変更: 構造体内の固定バッファを使用
+ if cr.buf[0] != '\r' || cr.buf[1] != '\n' { // 変更: 固定バッファを参照
cr.err = errors.New("malformed chunked encoding")
}
}
3. chunkedWriter.Write
メソッド内のチャンクサイズ書き込み部分の変更
--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -147,9 +148,7 @@ func (cw *chunkedWriter) Write(data []byte) (n int, err error) {
return 0, nil
}
- head := strconv.FormatInt(int64(len(data)), 16) + "\r\n" // 削除: 文字列生成とアロケーション
-
- if _, err = io.WriteString(cw.Wire, head); err != nil { // 削除: 文字列書き込み
+ if _, err = fmt.Fprintf(cw.Wire, "%x\r\n", len(data)); err != nil { // 変更: fmt.Fprintfで直接書き込み
return 0, err
}
if n, err = cw.Wire.Write(data); err != nil {
コアとなるコードの解説
chunkedReader
の変更
-
buf [2]byte
の追加:chunkedReader
構造体にbuf [2]byte
という2バイトの固定長配列が追加されました。これは、各チャンクの終端にあるCRLF(2バイト)を読み取るための一時バッファとして再利用されます。固定長配列は、そのサイズがコンパイル時に決定されるため、ヒープではなくスタックに割り当てられる可能性が高く(エスケープ解析の結果による)、たとえヒープに割り当てられたとしても、読み取りごとに動的にmake([]byte, 2)
で新しいスライスを割り当てるオーバーヘッドがなくなります。 -
io.ReadFull(cr.r, cr.buf[:])
への変更: 以前はb := make([]byte, 2)
で新しいスライスを生成し、そこにCRLFを読み込んでいました。このmake
呼び出しがヒープアロケーションを引き起こしていました。 変更後は、cr.buf[:]
をio.ReadFull
に渡すことで、chunkedReader
インスタンスが持つ既存の固定バッファを再利用してCRLFを読み込みます。これにより、チャンクごとに発生していた2バイトスライスのヒープアロケーションが完全に排除されます。
chunkedWriter
の変更
fmt.Fprintf(cw.Wire, "%x\r\n", len(data))
への変更: 以前は、チャンクサイズを16進数文字列に変換するためにstrconv.FormatInt(int64(len(data)), 16)
を使用していました。この関数は結果の文字列をヒープに割り当てるため、チャンクごとにアロケーションが発生していました。その後、io.WriteString
でその文字列をcw.Wire
(基となるio.Writer
)に書き込んでいました。 変更後は、fmt.Fprintf
を使用しています。fmt.Fprintf
は、指定されたフォーマット文字列 ("%x\r\n"
) と引数 (len(data)
) を直接cw.Wire
に書き込みます。%x
は整数を16進数でフォーマットする指示子です。fmt.Fprintf
の内部実装は、多くの場合、中間的な文字列オブジェクトをヒープに割り当てることなく、直接バイト列としてio.Writer
に書き込むように最適化されています。これにより、チャンクサイズ文字列の生成と書き込みに伴うヒープアロケーションが削減され、チャンク書き込み処理の効率が向上します。
これらの変更は、GoのHTTPチャンク処理におけるメモリフットプリントとGCオーバーヘッドを削減し、特に多数のチャンクを含むHTTPメッセージを扱う際のパフォーマンスを向上させることに貢献しています。
関連リンク
- HTTP/1.1 RFC 2616 - 3.6.1 Chunked Transfer Coding: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
- Go言語の
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
fmt
パッケージドキュメント: https://pkg.go.dev/fmt - Go言語の
strconv
パッケージドキュメント: https://pkg.go.dev/strconv
参考にした情報源リンク
- Go言語の公式ドキュメント (pkg.go.dev)
- HTTP/1.1 RFC 2616
- Go言語のメモリ管理とガベージコレクションに関する一般的な知識
- Go言語のソースコード (net/http/chunked.go, net/http/httputil/chunked.go)
- Goのコードレビューシステム (golang.org/cl/6847063)
- Goのパフォーマンス最適化に関する一般的な記事や議論(アロケーション削減の重要性)