[インデックス 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 Headerheaderフィールドがいつクローンされるか、または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言語におけるパフォーマンス最適化、特にアロケーション削減に関する記事や議論