[インデックス 16059] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内の chunkWriter.WriteHeader
関数におけるメモリ割り当て(アロケーション)を削減することを目的としています。具体的には、HTTPレスポンスヘッダーの書き込み処理において、不要なマップのクローン作成と変更を排除することで、パフォーマンスの向上とメモリ使用量の削減を図っています。
コミット
- コミットハッシュ:
babbd55e5d0c940b8c527ef27261ab7f87e42f17
- 作者: Brad Fitzpatrick bradfitz@golang.org
- 日付: 2013年4月2日 火曜日 16:27:23 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/babbd55e5d0c940b8c527ef27261ab7f87e42f17
元コミット内容
net/http: fewer allocations in chunkWriter.WriteHeader
It was unnecessarily cloning and then mutating a map that had
a very short lifetime (just that function).
No new tests, because they were added in revision 833bf2ef1527
(TestHeaderToWire). The benchmarks below are from the earlier
commit, revision 52e3407d.
I noticed this inefficiency when reviewing a change Peter Buhr
is looking into, which will also use these benchmarks.
benchmark old ns/op new ns/op delta
BenchmarkServerHandlerTypeLen 12547 12325 -1.77%
BenchmarkServerHandlerNoLen 12466 11167 -10.42%
BenchmarkServerHandlerNoType 12699 11800 -7.08%
BenchmarkServerHandlerNoHeader 11901 9210 -22.61%
benchmark old allocs new allocs delta
BenchmarkServerHandlerTypeLen 21 20 -4.76%
BenchmarkServerHandlerNoLen 20 18 -10.00%
BenchmarkServerHandlerNoType 20 18 -10.00%
BenchmarkServerHandlerNoHeader 17 13 -23.53%
benchmark old bytes new bytes delta
BenchmarkServerHandlerTypeLen 1930 1913 -0.88%
BenchmarkServerHandlerNoLen 1912 1879 -1.73%
BenchmarkServerHandlerNoType 1912 1878 -1.78%
BenchmarkServerHandlerNoHeader 1491 1086 -27.16%
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/8268046
変更の背景
この変更の主な背景は、Goの net/http
パッケージにおけるHTTPレスポンスヘッダーの処理効率の改善です。特に chunkWriter.WriteHeader
関数内で、http.Header
型のマップが不必要にクローンされ、その場で変更されるという非効率な処理が行われていました。
http.Header
は map[string][]string
のエイリアスであり、HTTPヘッダーを表すために使用されます。以前の実装では、WriteHeader
が呼び出された際に、response
オブジェクトが持つ handlerHeader
(ハンドラが設定したヘッダー) をディープクローンしていました。このクローンされたマップは、その関数内でのみ使用される非常に短い寿命のものでした。
Go言語において、マップのクローン作成はメモリ割り当てを伴います。特に、HTTPリクエストごとにこの処理が頻繁に発生する場合、小さなアロケーションであっても累積するとパフォーマンスに大きな影響を与え、ガベージコレクションの頻度を増加させる可能性があります。
作者のBrad Fitzpatrickは、Peter Buhrが検討していた変更のレビュー中にこの非効率性に気づきました。この変更は、既存のベンチマーク(BenchmarkServerHandlerTypeLen
など)を使用して、アロケーション数、処理時間、およびメモリ使用量の削減効果を定量的に示しています。ベンチマーク結果は、特に BenchmarkServerHandlerNoHeader
のケースで、ns/op
(操作あたりのナノ秒) が22.61%削減され、allocs
(アロケーション数) が23.53%削減され、bytes
(割り当てられたバイト数) が27.16%削減されるなど、顕著な改善が見られることを示しています。
この最適化は、GoのHTTPサーバーの全体的なスループットと効率を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびHTTPプロトコルに関する前提知識が必要です。
-
Go言語の
net/http
パッケージ:http.ResponseWriter
: HTTPハンドラがクライアントにHTTPレスポンスを送信するために使用するインターフェースです。これを通じてステータスコードの設定、ヘッダーの追加、レスポンスボディの書き込みが行われます。http.Request
: クライアントからのHTTPリクエストを表す構造体です。http.Header
:map[string][]string
のエイリアスであり、HTTPヘッダーのキーと値のペアを格納するために使用されます。キーはヘッダー名(例: "Content-Type")、値はそのヘッダーの複数の値を保持できる文字列スライスです。http.Server
: HTTPリクエストをリッスンし、ハンドラにディスパッチするHTTPサーバーの実装です。chunkWriter
:net/http
内部で使用される構造体で、特にHTTP/1.1のチャンク転送エンコーディングを処理するために利用されます。レスポンスヘッダーの書き込みも担当します。
-
HTTPプロトコル:
- HTTPヘッダー: HTTPリクエストおよびレスポンスのメタデータを提供するキーと値のペアです。例:
Content-Type
,Content-Length
,Connection
,Transfer-Encoding
,Date
など。 - HTTPステータスコード: HTTPレスポンスの最初の行に含まれる3桁の数字で、リクエストの結果を示します(例:
200 OK
,404 Not Found
,500 Internal Server Error
)。 - チャンク転送エンコーディング (Chunked Transfer Encoding): HTTP/1.1で導入された転送エンコーディングの一種で、レスポンスボディの長さを事前に知らなくても、動的にコンテンツを送信できるようにします。各チャンクはサイズ情報とデータを含み、最後のチャンクはサイズ0で終了します。
Transfer-Encoding: chunked
ヘッダーで示されます。 Content-Length
ヘッダー: レスポンスボディのバイト単位の長さを指定します。チャンク転送エンコーディングが使用される場合、このヘッダーは通常存在しません。Connection
ヘッダー: 接続のオプションを指定します。keep-alive
は接続を再利用することを示し、close
はリクエスト/レスポンスの後に接続を閉じることを示します。
- HTTPヘッダー: HTTPリクエストおよびレスポンスのメタデータを提供するキーと値のペアです。例:
-
Go言語のメモリ管理とガベージコレクション (GC):
- Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラムがメモリを割り当てると、GCが不要になったメモリを自動的に解放します。
- アロケーション (Allocation): プログラムが新しいデータ構造(例: マップ、スライス、構造体)を作成する際に、メモリを確保することです。
- エスケープ解析 (Escape Analysis): Goコンパイラが行う最適化の一つで、変数がヒープに割り当てられるべきか(エスケープする)、スタックに割り当てられるべきか(エスケープしない)を決定します。ヒープへのアロケーションはGCの対象となり、パフォーマンスに影響を与える可能性があります。
- アロケーションの削減の重要性: アロケーションの数を減らすことは、GCの頻度と作業量を減らすことにつながり、結果としてアプリケーションのレイテンシを改善し、スループットを向上させることができます。特に、HTTPサーバーのような高負荷なアプリケーションでは、マイクロ最適化が全体的なパフォーマンスに大きな影響を与えることがあります。
-
http.Header.clone()
とhttp.Header.WriteSubset()
:http.Header.clone()
: 以前のGoのバージョンでは、http.Header
オブジェクトをディープコピーするためにclone()
メソッドが使用されていました。これは新しいマップを作成し、元のマップのすべてのキーと値をコピーするため、アロケーションが発生します。http.Header.WriteSubset()
: このコミットで導入された、または既存のGoのhttp.Header
に存在するメソッドで、特定のヘッダーを除外してヘッダーをio.Writer
に書き込む機能を提供します。これにより、不要なヘッダーを書き出す前に削除するのではなく、書き込み時にフィルタリングできるようになります。
これらの知識が、コミットの変更内容とそのパフォーマンス上の利点を深く理解するための基盤となります。
技術的詳細
このコミットの技術的詳細は、net/http
パッケージの chunkWriter.WriteHeader
関数におけるメモリ割り当ての削減に焦点を当てています。主な変更点は以下の通りです。
-
chunkWriter.header
の役割変更:- 変更前:
chunkWriter.header
はres.handlerHeader
のディープクローン、またはハンドラがHeader()
を呼び出した場合の同じ参照でした。これは、ハンドラがWriteHeader
呼び出し後にhandlerHeader
を変更する可能性を考慮し、書き込まれるヘッダーがその時点のスナップショットであることを保証するためのものでした。しかし、このクローン作成が不要なアロケーションを引き起こしていました。 - 変更後:
chunkWriter.header
は、res.WriteHeader
が呼び出され、かつ追加のバッファリング(Content-Type
やContent-Length
の計算など)が行われる場合にのみ、res.handlerHeader
のディープクローンとして設定されるようになりました。それ以外の場合はnil
となります。これにより、常にクローンを作成するのではなく、必要な場合にのみアロケーションが発生するように変更されました。
- 変更前:
-
extraHeader
構造体の導入:- HTTPレスポンスヘッダーの中で、
Content-Type
,Content-Length
,Connection
,Date
,Transfer-Encoding
といった特定のヘッダーは、WriteHeader
処理中に動的に追加または変更されることがよくあります。 - 以前はこれらのヘッダーも
http.Header
マップに直接追加されていましたが、これはマップの変更や、場合によってはマップの再割り当て(リハッシュ)を引き起こし、アロケーションの原因となっていました。 - 新しい
extraHeader
構造体は、これらの特定のヘッダーをstring
型のフィールドとして直接保持します。これにより、これらのヘッダーを追加する際にマップのアロケーションや操作が不要になります。 extraHeader
にはWrite
メソッドが実装されており、io.Writer
に直接これらのヘッダーを書き込むことができます。このWrite
メソッドは、値レシーバ (func (h extraHeader) Write(...)
) を使用していますが、これは5つの文字列をスタックにコピーするものの、余分なアロケーションを防ぐ効果があるとコメントで説明されています(エスケープ解析がh
をミューテートしないことを十分に賢く認識できないため)。
- HTTPレスポンスヘッダーの中で、
-
ヘッダーの削除ロジックの改善と
excludeHeader
マップの導入:- 特定の条件(例:
StatusNotModified
の場合)でヘッダーを削除する必要がある場合、以前はcw.header.Del(key)
を直接呼び出していました。 - 変更後、
delHeader
というヘルパー関数が導入されました。この関数は、ヘッダーマップが「所有されている」場合(つまり、cw.header
がhandlerHeader
のクローンである場合)は直接Del
を呼び出します。 - そうでない場合(
cw.header
がhandlerHeader
への参照である場合)、excludeHeader
というmap[string]bool
が遅延初期化され、削除すべきヘッダーのキーがこのマップに追加されます。 - 最終的にヘッダーをワイヤーに書き出す際に、
cw.header.WriteSubset(w.conn.buf, excludeHeader)
が使用されます。WriteSubset
は、excludeHeader
に含まれるキーのヘッダーを書き出さないようにフィルタリングする機能を提供します。これにより、元のhandlerHeader
マップを実際に変更することなく、特定のヘッダーを除外して書き出すことが可能になり、不要なマップ操作によるアロケーションを回避します。
- 特定の条件(例:
-
http.Header.WriteSubset
の活用:- ヘッダーの書き出しは、以前は
cw.header.Write(w.conn.buf)
で行われていました。 - 変更後、
cw.header.WriteSubset(w.conn.buf, excludeHeader)
が使用され、excludeHeader
に基づいて一部のヘッダーが除外されます。 - その後、
setHeader.Write(w.conn.buf)
が呼び出され、extraHeader
構造体に格納された追加のヘッダーが書き込まれます。
- ヘッダーの書き出しは、以前は
これらの変更により、chunkWriter.WriteHeader
関数内で発生していた、短命なマップのクローン作成、頻繁なマップ操作、およびそれに伴うガベージコレクションの負荷が大幅に削減され、HTTPサーバーのパフォーマンスが向上しました。
コアとなるコードの変更箇所
変更は src/pkg/net/http/server.go
ファイルに集中しています。
-
chunkWriter
構造体のheader
フィールドのコメント変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -224,8 +224,10 @@ const bufferBeforeChunkingSize = 2048 type chunkWriter struct { res *response - // header is either the same as res.handlerHeader, - // or a deep clone if the handler called Header. + // header is either nil or a deep clone of res.handlerHeader + // at the time of res.WriteHeader, if res.WriteHeader is + // called and extra buffering is being done to calculate + // Content-Type and/or Content-Length. header Header
header
フィールドがいつクローンされるか、またはnil
になるかの条件が明確化されました。 -
crlf
とcolonSpace
の定義:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -238,7 +240,10 @@ type chunkWriter struct { chunking bool // using chunked transfer encoding for reply body } -var crlf = []byte("\r\n") +var ( + crlf = []byte("\r\n") + colonSpace = []byte(": ") +)
colonSpace
が追加され、ヘッダーのキーと値の間の:
を表すバイトスライスとして再利用されます。 -
extraHeader
構造体の新規追加:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -613,6 +618,37 @@ func (w *response) WriteHeader(code int) { } } +// extraHeader is the set of headers sometimes added by chunkWriter.writeHeader. +// This type is used to avoid extra allocations from cloning and/or populating +// the response Header map and all its 1-element slices. +type extraHeader struct { + contentType string + contentLength string + connection string + date string + transferEncoding string +} + +// Sorted the same as extraHeader.Write's loop. +var extraHeaderKeys = [][]byte{ + []byte("Content-Type"), []byte("Content-Length"), + []byte("Connection"), []byte("Date"), []byte("Transfer-Encoding"), +} + +// The value receiver, despite copying 5 strings to the stack, +// prevents an extra allocation. The escape analysis isn't smart +// enough to realize this doesn't mutate h. +func (h extraHeader) Write(w io.Writer) { + for i, v := range []string{h.contentType, h.contentLength, h.connection, h.date, h.transferEncoding} { + if v != "" { + w.Write(extraHeaderKeys[i]) + w.Write(colonSpace) + io.WriteString(w, v) + w.Write(crlf) + } + } +} + // writeHeader finalizes the header sent to the client and writes it // to cw.res.conn.buf. //
extraHeader
構造体と、そのWrite
メソッドが追加されました。これは特定のヘッダーを効率的に管理し、書き出すためのものです。 -
chunkWriter.writeHeader
関数の大幅な変更:cw.header
の初期化ロジックが変更され、必要な場合にのみクローンされるようになりました。owned
フラグとexcludeHeader
マップが導入され、ヘッダーの削除ロジックが変更されました。delHeader
ヘルパー関数が追加されました。setHeader
(型はextraHeader
) が導入され、動的に追加されるヘッダーがこれに格納されるようになりました。cw.header.Set
の呼び出しの多くがsetHeader
のフィールド設定に置き換えられました。- ヘッダーの最終的な書き出しが
cw.header.WriteSubset(w.conn.buf, excludeHeader)
とsetHeader.Write(w.conn.buf)
に変更されました。
これらの変更により、http.Header
マップの不要なクローン作成と、頻繁なマップ操作が回避され、アロケーションが削減されています。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、chunkWriter.writeHeader
関数におけるヘッダー処理のロジックを根本的に見直すことで、メモリ割り当てを削減しています。
-
chunkWriter.header
の扱い:- 変更前は、
cw.header
は常にw.handlerHeader
のクローンとして初期化されるか、直接参照されていました。特にw.handlerDone
がfalse
の場合(ハンドラがまだヘッダーマップを変更する可能性がある場合)、w.handlerHeader.clone()
が呼び出され、新しいマップがヒープに割り当てられていました。 - 変更後、
header := cw.header
とowned := header != nil
が導入されます。cw.header
がnil
の場合(つまり、まだクローンが作成されていない場合)、header
はw.handlerHeader
を直接参照します。これにより、不要なクローン作成が回避されます。owned
フラグは、現在操作しているheader
がchunkWriter
自身が所有するクローンであるか(true
)、それともresponse
オブジェクトのhandlerHeader
への参照であるか(false
)を示します。
- 変更前は、
-
delHeader
ヘルパー関数とexcludeHeader
マップ:- ヘッダーを削除する必要がある場合(例:
StatusNotModified
の場合や、Content-Length
とTransfer-Encoding
が同時に存在する場合など)、以前はcw.header.Del(key)
を直接呼び出していました。 - 新しい
delHeader
関数は、owned
フラグに基づいて動作が変わります。owned
がtrue
の場合(cw.header
がクローンである場合)、header.Del(key)
を直接呼び出し、マップからエントリを削除します。owned
がfalse
の場合(header
がw.handlerHeader
への参照である場合)、excludeHeader
というmap[string]bool
が遅延初期化され、削除すべきヘッダーのキーがこのマップに追加されます。この場合、実際のhandlerHeader
マップは変更されません。
- ヘッダーを削除する必要がある場合(例:
-
extraHeader
構造体とsetHeader
:Content-Type
,Content-Length
,Connection
,Date
,Transfer-Encoding
といった、writeHeader
関数内で動的に設定される可能性のあるヘッダーは、extraHeader
型のローカル変数setHeader
に格納されるようになりました。- 例えば、
Content-Type
の設定はcw.header.Set("Content-Type", DetectContentType(p))
からsetHeader.contentType = DetectContentType(p)
に変更されました。 - これにより、これらのヘッダーを設定する際に
http.Header
マップへの書き込み操作(マップの再割り当てや要素の追加)が不要になり、アロケーションが削減されます。
-
ヘッダーの最終的な書き出し:
- ヘッダーを
w.conn.buf
に書き出す際、以前はcw.header.Write(w.conn.buf)
を使用していました。 - 変更後、まず
cw.header.WriteSubset(w.conn.buf, excludeHeader)
が呼び出されます。これは、header
マップ(cw.header
またはw.handlerHeader
)の内容をw.conn.buf
に書き出しますが、excludeHeader
マップにキーが存在するヘッダーは除外されます。これにより、delHeader
でマークされたヘッダーがワイヤーに送信されなくなります。 - 次に、
setHeader.Write(w.conn.buf)
が呼び出されます。これはextraHeader
構造体に格納されたヘッダーを直接w.conn.buf
に書き出します。extraHeader.Write
メソッドは、extraHeaderKeys
とcolonSpace
を再利用し、文字列の結合ではなくバイトスライスの書き込みを効率的に行うことで、ここでもアロケーションを最小限に抑えています。
- ヘッダーを
これらの変更の組み合わせにより、chunkWriter.WriteHeader
関数は、HTTPレスポンスヘッダーを構築しワイヤーに書き出す過程で発生する不要なマップ操作とそれに伴うメモリ割り当てを大幅に削減し、結果としてパフォーマンスの向上とガベージコレクションの負荷軽減を実現しています。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/8268046
参考にした情報源リンク
- (Web検索で得られた情報があればここに記載)
- Go言語のメモリ管理とガベージコレクションに関する一般的な情報
net/http
パッケージのドキュメント- HTTP/1.1 チャンク転送エンコーディングに関するRFC
- Go言語におけるパフォーマンス最適化、特にアロケーション削減に関する記事や議論