Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Headermap[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プロトコルに関する前提知識が必要です。

  1. 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のチャンク転送エンコーディングを処理するために利用されます。レスポンスヘッダーの書き込みも担当します。
  2. 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 はリクエスト/レスポンスの後に接続を閉じることを示します。
  3. Go言語のメモリ管理とガベージコレクション (GC):

    • Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラムがメモリを割り当てると、GCが不要になったメモリを自動的に解放します。
    • アロケーション (Allocation): プログラムが新しいデータ構造(例: マップ、スライス、構造体)を作成する際に、メモリを確保することです。
    • エスケープ解析 (Escape Analysis): Goコンパイラが行う最適化の一つで、変数がヒープに割り当てられるべきか(エスケープする)、スタックに割り当てられるべきか(エスケープしない)を決定します。ヒープへのアロケーションはGCの対象となり、パフォーマンスに影響を与える可能性があります。
    • アロケーションの削減の重要性: アロケーションの数を減らすことは、GCの頻度と作業量を減らすことにつながり、結果としてアプリケーションのレイテンシを改善し、スループットを向上させることができます。特に、HTTPサーバーのような高負荷なアプリケーションでは、マイクロ最適化が全体的なパフォーマンスに大きな影響を与えることがあります。
  4. 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 関数におけるメモリ割り当ての削減に焦点を当てています。主な変更点は以下の通りです。

  1. chunkWriter.header の役割変更:

    • 変更前: chunkWriter.headerres.handlerHeader のディープクローン、またはハンドラが Header() を呼び出した場合の同じ参照でした。これは、ハンドラが WriteHeader 呼び出し後に handlerHeader を変更する可能性を考慮し、書き込まれるヘッダーがその時点のスナップショットであることを保証するためのものでした。しかし、このクローン作成が不要なアロケーションを引き起こしていました。
    • 変更後: chunkWriter.header は、res.WriteHeader が呼び出され、かつ追加のバッファリング(Content-TypeContent-Length の計算など)が行われる場合にのみ、res.handlerHeader のディープクローンとして設定されるようになりました。それ以外の場合は nil となります。これにより、常にクローンを作成するのではなく、必要な場合にのみアロケーションが発生するように変更されました。
  2. 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 をミューテートしないことを十分に賢く認識できないため)。
  3. ヘッダーの削除ロジックの改善と excludeHeader マップの導入:

    • 特定の条件(例: StatusNotModified の場合)でヘッダーを削除する必要がある場合、以前は cw.header.Del(key) を直接呼び出していました。
    • 変更後、delHeader というヘルパー関数が導入されました。この関数は、ヘッダーマップが「所有されている」場合(つまり、cw.headerhandlerHeader のクローンである場合)は直接 Del を呼び出します。
    • そうでない場合(cw.headerhandlerHeader への参照である場合)、excludeHeader という map[string]bool が遅延初期化され、削除すべきヘッダーのキーがこのマップに追加されます。
    • 最終的にヘッダーをワイヤーに書き出す際に、cw.header.WriteSubset(w.conn.buf, excludeHeader) が使用されます。WriteSubset は、excludeHeader に含まれるキーのヘッダーを書き出さないようにフィルタリングする機能を提供します。これにより、元の handlerHeader マップを実際に変更することなく、特定のヘッダーを除外して書き出すことが可能になり、不要なマップ操作によるアロケーションを回避します。
  4. 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 ファイルに集中しています。

  1. 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 になるかの条件が明確化されました。

  2. crlfcolonSpace の定義:

    --- 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 が追加され、ヘッダーのキーと値の間の : を表すバイトスライスとして再利用されます。

  3. 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 メソッドが追加されました。これは特定のヘッダーを効率的に管理し、書き出すためのものです。

  4. 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 関数におけるヘッダー処理のロジックを根本的に見直すことで、メモリ割り当てを削減しています。

  1. chunkWriter.header の扱い:

    • 変更前は、cw.header は常に w.handlerHeader のクローンとして初期化されるか、直接参照されていました。特に w.handlerDonefalse の場合(ハンドラがまだヘッダーマップを変更する可能性がある場合)、w.handlerHeader.clone() が呼び出され、新しいマップがヒープに割り当てられていました。
    • 変更後、header := cw.headerowned := header != nil が導入されます。cw.headernil の場合(つまり、まだクローンが作成されていない場合)、headerw.handlerHeader を直接参照します。これにより、不要なクローン作成が回避されます。owned フラグは、現在操作している headerchunkWriter 自身が所有するクローンであるか(true)、それとも response オブジェクトの handlerHeader への参照であるか(false)を示します。
  2. delHeader ヘルパー関数と excludeHeader マップ:

    • ヘッダーを削除する必要がある場合(例: StatusNotModified の場合や、Content-LengthTransfer-Encoding が同時に存在する場合など)、以前は cw.header.Del(key) を直接呼び出していました。
    • 新しい delHeader 関数は、owned フラグに基づいて動作が変わります。
      • ownedtrue の場合(cw.header がクローンである場合)、header.Del(key) を直接呼び出し、マップからエントリを削除します。
      • ownedfalse の場合(headerw.handlerHeader への参照である場合)、excludeHeader という map[string]bool が遅延初期化され、削除すべきヘッダーのキーがこのマップに追加されます。この場合、実際の handlerHeader マップは変更されません。
  3. 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 マップへの書き込み操作(マップの再割り当てや要素の追加)が不要になり、アロケーションが削減されます。
  4. ヘッダーの最終的な書き出し:

    • ヘッダーを 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 メソッドは、extraHeaderKeyscolonSpace を再利用し、文字列の結合ではなくバイトスライスの書き込みを効率的に行うことで、ここでもアロケーションを最小限に抑えています。

これらの変更の組み合わせにより、chunkWriter.WriteHeader 関数は、HTTPレスポンスヘッダーを構築しワイヤーに書き出す過程で発生する不要なマップ操作とそれに伴うメモリ割り当てを大幅に削減し、結果としてパフォーマンスの向上とガベージコレクションの負荷軽減を実現しています。

関連リンク

参考にした情報源リンク

  • (Web検索で得られた情報があればここに記載)
    • Go言語のメモリ管理とガベージコレクションに関する一般的な情報
    • net/http パッケージのドキュメント
    • HTTP/1.1 チャンク転送エンコーディングに関するRFC
    • Go言語におけるパフォーマンス最適化、特にアロケーション削減に関する記事や議論