[インデックス 16343] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるHTTPサーバーのパフォーマンス改善を目的としています。具体的には、HTTPレスポンスヘッダーの Date
および Content-Length
の生成時に発生するメモリ割り当てを削減することで、サーバーパスの効率化を図っています。これにより、全体的な処理速度の向上とメモリ使用量の削減が実現されています。
コミット
commit d4cbc80d106a3f3b53631aa60b400c790b14bb52
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sun May 19 20:15:40 2013 -0700
net/http: fewer allocations in the server path
Don't allocate for the Date or Content-Length headers.
A custom Date header formatter replaces use of time.Format.
benchmark old ns/op new ns/op delta
BenchmarkClientServer 67791 64424 -4.97%
BenchmarkClientServerParallel4 62956 58533 -7.03%
BenchmarkClientServerParallel64 62043 54789 -11.69%
BenchmarkServer 254609 229060 -10.03%
BenchmarkServerFakeConnNoKeepAlive 17038 16316 -4.24%
BenchmarkServerFakeConnWithKeepAlive 14184 13226 -6.75%
BenchmarkServerFakeConnWithKeepAliveLite 8591 7532 -12.33%
BenchmarkServerHandlerTypeLen 10750 9961 -7.34%
BenchmarkServerHandlerNoLen 9535 8935 -6.29%
BenchmarkServerHandlerNoType 9858 9362 -5.03%
BenchmarkServerHandlerNoHeader 7754 6856 -11.58%
benchmark old allocs new allocs delta
BenchmarkClientServer 68 66 -2.94%
BenchmarkClientServerParallel4 68 66 -2.94%
BenchmarkClientServerParallel64 68 66 -2.94%
BenchmarkServer 21 19 -9.52%
BenchmarkServerFakeConnNoKeepAlive 32 30 -6.25%
BenchmarkServerFakeConnWithKeepAlive 27 25 -7.41%
BenchmarkServerFakeConnWithKeepAliveLite 12 10 -16.67%
BenchmarkServerHandlerTypeLen 19 18 -5.26%
BenchmarkServerHandlerNoLen 17 15 -11.76%
BenchmarkServerHandlerNoType 17 16 -5.88%
BenchmarkServerHandlerNoHeader 12 10 -16.67%
Update #5195
R=nigeltao
CC=golang-dev
https://golang.org/cl/9432046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d4cbc80d106a3f3b53631aa60b400c790b14bb52
元コミット内容
このコミットの目的は、Goの net/http
パッケージにおけるサーバーパスでのメモリ割り当てを削減することです。特に、HTTPレスポンスヘッダーの Date
と Content-Length
の生成時に発生する不要なメモリ割り当てを排除することに焦点を当てています。time.Format
の使用をカスタムの Date
ヘッダーフォーマッタに置き換えることで、この最適化を実現しています。
ベンチマーク結果は以下の通り、ns/op
(ナノ秒/操作) と allocs
(メモリ割り当て数) の両方で顕著な改善を示しています。
処理速度 (ns/op) の改善:
BenchmarkClientServer
: 約 5% 改善BenchmarkClientServerParallel64
: 約 11.7% 改善BenchmarkServer
: 約 10% 改善BenchmarkServerFakeConnWithKeepAliveLite
: 約 12.3% 改善
メモリ割り当て数 (allocs) の改善:
BenchmarkClientServer
: 約 3% 改善BenchmarkServer
: 約 9.5% 改善BenchmarkServerFakeConnWithKeepAliveLite
: 約 16.7% 改善BenchmarkServerHandlerNoHeader
: 約 16.7% 改善
これらの結果は、本コミットがHTTPサーバーのパフォーマンスと効率性に大きな影響を与えることを示しています。
変更の背景
Goの net/http
パッケージは、Go言語でWebアプリケーションを構築する際の基盤となる重要なコンポーネントです。Webサーバーは、大量のリクエストを同時に処理することが求められるため、パフォーマンスと効率性が非常に重要になります。特に、HTTPレスポンスヘッダーの生成は、すべてのリクエストに対して行われるため、ここでのわずかな非効率性も、高負荷時には大きなボトルネックとなり得ます。
従来の net/http
サーバーでは、Date
ヘッダーの生成に time.Format
関数が使用されていました。time.Format
は柔軟な日付フォーマット機能を提供しますが、その内部で文字列の割り当て(ヒープ割り当て)が発生します。同様に、Content-Length
ヘッダーも、整数値を文字列に変換する際に割り当てが発生する可能性がありました。これらの割り当ては、リクエストごとに繰り返されるため、特に多数の同時接続を処理するサーバーでは、ガベージコレクションの頻度を増加させ、CPUサイクルを消費し、結果として全体的なスループットを低下させる要因となっていました。
このコミットの背景には、Goの標準ライブラリが提供するHTTPサーバーの性能をさらに向上させ、より高効率なWebサービスを構築できるようにするという明確な目標がありました。メモリ割り当ての削減は、ガベージコレクションの負荷を軽減し、CPUキャッシュの効率を高めることで、サーバーの応答性とスループットを向上させるための一般的な最適化手法です。
前提知識の解説
このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。
-
HTTPヘッダー: HTTPプロトコルにおいて、クライアントとサーバー間で送受信されるメッセージのメタデータを提供する部分です。
Date
ヘッダーはメッセージが生成された日時を示し、Content-Length
ヘッダーはメッセージボディのバイト数を示します。これらはHTTP通信において非常に頻繁に使用されます。 -
Go言語のメモリ割り当てとガベージコレクション (GC):
- メモリ割り当て (Allocation): Goプログラムが実行時にメモリを要求し、使用可能なメモリ領域を確保するプロセスです。Goでは、主にスタックとヒープの2種類のメモリ領域があります。
- スタック: 関数呼び出しやローカル変数など、生存期間が短いデータが格納されます。コンパイル時にサイズが決定され、高速にアクセスできます。割り当てと解放は自動的に行われます。
- ヒープ: プログラムの実行中に動的にサイズが変化するデータ(例: スライス、マップ、チャネル、インターフェース、大きな構造体など)が格納されます。ヒープへの割り当てはスタックよりもコストが高く、ガベージコレクションの対象となります。
- ガベージコレクション (GC): ヒープに割り当てられたメモリのうち、もはやプログラムから参照されなくなった(不要になった)メモリ領域を自動的に解放し、再利用可能にするプロセスです。GoのGCは並行的に動作しますが、GCが実行されると少なからずCPUリソースを消費し、プログラムの実行を一時的に停止させる(ストップ・ザ・ワールド)可能性があります。GCの頻度や実行時間は、ヒープ割り当ての量に直接影響されます。割り当てが多ければ多いほど、GCの頻度が増え、パフォーマンスに悪影響を与える可能性があります。
- メモリ割り当て (Allocation): Goプログラムが実行時にメモリを要求し、使用可能なメモリ領域を確保するプロセスです。Goでは、主にスタックとヒープの2種類のメモリ領域があります。
-
time.Format
の特性: Goのtime
パッケージのFormat
メソッドは、time.Time
オブジェクトを指定されたレイアウト文字列に基づいてフォーマットし、新しい文字列を返します。この新しい文字列はヒープに割り当てられるため、頻繁に呼び出されるとメモリ割り当てが蓄積され、GCの負荷が増大します。 -
strconv
パッケージ: Goの標準ライブラリで、プリミティブ型(整数、浮動小数点数、ブール値など)と文字列との間の変換を提供します。strconv.Itoa
は整数を文字列に変換しますが、これも新しい文字列を割り当てます。strconv.AppendInt
のように、既存のバイトスライスに追記する形式の関数は、不要な割り当てを避けるのに役立ちます。 -
[]byte
と文字列 (string): Goでは、文字列は不変のバイトスライスとして扱われます。文字列操作は新しい文字列の割り当てを伴うことが多いですが、[]byte
を直接操作することで、メモリ割り当てをより細かく制御し、既存のバッファを再利用することが可能になります。 -
bufio.Writer
: バッファリングされたI/O操作を提供し、書き込み操作の回数を減らすことでパフォーマンスを向上させます。小さな書き込みをまとめて一度に基になるio.Writer
に書き込むことで、システムコールなどのオーバーヘッドを削減します。
これらの知識を背景に、本コミットがどのようにしてメモリ割り当てを削減し、パフォーマンスを向上させているかを深く理解することができます。
技術的詳細
このコミットの技術的詳細な変更点は、主に src/pkg/net/http/server.go
ファイルに集中しており、HTTPレスポンスヘッダーの Date
と Content-Length
の生成と書き込み方法を最適化しています。
-
response
構造体へのバッファ追加:response
構造体(HTTPレスポンスを構築するための内部構造体)に、Date
ヘッダーとContent-Length
ヘッダーの値を格納するための固定サイズのバイト配列が追加されました。type response struct { // ... // Buffers for Date and Content-Length dateBuf [len(TimeFormat)]byte clenBuf [10]byte }
dateBuf
はTimeFormat
(例: "Mon, 02 Jan 2006 15:04:05 GMT") の長さに合わせて確保され、clenBuf
は最大10桁のContent-Length (約10GBまで) を格納できるように確保されています。これにより、これらのヘッダー値を文字列としてヒープに割り当てる必要がなくなり、スタック上に直接バッファを確保して再利用できるようになります。
-
カスタム
appendTime
関数の導入:Date
ヘッダーのフォーマットのために、time.Now().UTC().Format(TimeFormat)
の代わりに、新しいappendTime
関数が導入されました。func appendTime(b []byte, t time.Time) []byte { // ... 手動での日付フォーマットロジック ... return append(b, /* ... */) }
- この関数は、
time.Time
オブジェクトから年、月、日、時、分、秒、曜日などを抽出し、それらを直接バイトスライスb
に追記することで、time.Format
が内部で行う文字列割り当てを完全に回避します。append
関数は、必要に応じてスライスの容量を拡張しますが、dateBuf[:0]
のように既存のバッファを再利用することで、ほとんどの場合で新たな割り当てを発生させません。
-
extraHeader
構造体の変更:- HTTPレスポンスの追加ヘッダーを管理する
extraHeader
構造体において、date
とcontentLength
フィールドの型がstring
から[]byte
に変更されました。type extraHeader struct { // ... date []byte // written if not nil contentLength []byte // written if not nil }
- これにより、ヘッダー値を文字列として保持する際の割り当てが不要になり、直接バイトスライスとして扱うことで、メモリ効率が向上します。
- HTTPレスポンスの追加ヘッダーを管理する
-
extraHeader.Write
メソッドの最適化:extraHeader
の内容をio.Writer
に書き込むWrite
メソッドが変更されました。- 変更前は、
Date
やContent-Length
を含むすべてのヘッダーが[]string
のループで処理され、io.WriteString
を使用していました。 - 変更後は、
Date
とContent-Length
は個別に、かつ[]byte
型のフィールドとして直接w.Write
で書き込まれるようになりました。func (h extraHeader) Write(w *bufio.Writer) { // w の型が io.Writer から *bufio.Writer に変更 if h.date != nil { w.Write(headerDate) // headerDate は []byte("Date: ") w.Write(h.date) w.Write(crlf) } if h.contentLength != nil { w.Write(headerContentLength) // headerContentLength は []byte("Content-Length: ") w.Write(h.contentLength) w.Write(crlf) } // その他のヘッダーはループで処理 for i, v := range []string{h.contentType, h.connection, h.transferEncoding} { if v != "" { w.Write(extraHeaderKeys[i]) w.Write(colonSpace) w.WriteString(v) // io.WriteString から w.WriteString に変更 w.Write(crlf) } } }
- 変更前は、
w
の型がio.Writer
から*bufio.Writer
に変更されたことで、WriteString
メソッドが*bufio.Writer
のメソッドとして直接呼び出せるようになり、インターフェース呼び出しによるオーバーヘッドが削減されます。headerDate
やheaderContentLength
のようなヘッダー名とコロン・スペースの組み合わせも[]byte
定数として定義され、再利用されることで、ここでの割り当ても回避されています。
-
ヘッダー値の設定方法の変更:
Content-Length
の設定:// 変更前: setHeader.contentLength = strconv.Itoa(len(p)) // 変更後: setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10)
strconv.Itoa
は新しい文字列を割り当てますが、strconv.AppendInt
は既存のバイトスライス (cw.res.clenBuf[:0]
) に整数値を追記するため、割り当てを回避できます。Date
の設定:
前述のカスタム// 変更前: setHeader.date = time.Now().UTC().Format(TimeFormat) // 変更後: setHeader.date = appendTime(cw.res.dateBuf[:0], time.Now())
appendTime
関数を使用することで、time.Format
による割り当てを排除しています。
これらの変更は、HTTPレスポンスヘッダーの生成パスにおけるヒープ割り当てを徹底的に排除し、サーバーのパフォーマンスを向上させることを目的としています。特に、リクエストごとに繰り返し実行される処理であるため、これらの最適化は高負荷環境下でのスループットとレイテンシに大きな影響を与えます。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更は、主に src/pkg/net/http/server.go
にあります。
-
response
構造体へのバッファ追加:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -320,6 +320,10 @@ type response struct { requestBodyLimitHit bool handlerDone bool // set true when the handler exits + + // Buffers for Date and Content-Length + dateBuf [len(TimeFormat)]byte + clenBuf [10]byte }
-
カスタム
appendTime
関数の追加:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -525,6 +529,27 @@ func (ecr *expectContinueReader) Close() error { // It is like time.RFC1123 but hard codes GMT as the time zone. const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" +// appendTime is a non-allocating version of []byte(time.Now().UTC().Format(TimeFormat)) +func appendTime(b []byte, t time.Time) []byte { + const days = "SunMonTueWedThuFriSat" + const months = "JanFebMarAprMayJunJulAugSepOctNovDec" + + yy, mm, dd := t.Date() + hh, mn, ss := t.Clock() + day := days[3*t.Weekday():] + mon := months[3*(mm-1):] + + return append(b, + day[0], day[1], day[2], ',', ' ', + byte('0'+dd/10), byte('0'+dd%10), ' ', + mon[0], mon[1], mon[2], ' ', + byte('0'+yy/1000), byte('0'+(yy/100)%10), byte('0'+(yy/10)%10), byte('0'+yy%10), ' ', + byte('0'+hh/10), byte('0'+hh%10), ':', + byte('0'+mn/10), byte('0'+mn%10), ':', + byte('0'+ss/10), byte('0'+ss%10), ' ', + 'G', 'M', 'T') +} + var errTooLarge = errors.New("http: request too large")
-
extraHeader
構造体のフィールド型変更とWrite
メソッドの最適化:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -620,27 +645,45 @@ func (w *response) WriteHeader(code int) { // the response Header map and all its 1-element slices. type extraHeader struct { contentType string - contentLength string connection string - date string transferEncoding string + date []byte // written if not nil + contentLength []byte // written if not nil } // Sorted the same as extraHeader.Write's loop. var extraHeaderKeys = [][]byte{ - []byte("Content-Type"), []byte("Content-Length"), - []byte("Connection"), []byte("Date"), []byte("Transfer-Encoding"), + []byte("Content-Type"), + []byte("Connection"), + []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} { +var ( + headerContentLength = []byte("Content-Length: ") + headerDate = []byte("Date: ") +) + +// Write writes the headers described in h to w. +// +// This method has a value receiver, despite the somewhat large size +// of h, because it prevents an allocation. The escape analysis isn't +// smart enough to realize this function doesn't mutate h. +func (h extraHeader) Write(w *bufio.Writer) { + if h.date != nil { + w.Write(headerDate) + w.Write(h.date) + w.Write(crlf) + } + if h.contentLength != nil { + w.Write(headerContentLength) + w.Write(h.contentLength) + w.Write(crlf) + } + for i, v := range []string{h.contentType, h.connection, h.transferEncoding} { if v != "" { w.Write(extraHeaderKeys[i]) w.Write(colonSpace) - io.WriteString(w, v) + w.WriteString(v) w.Write(crlf) } }
-
Content-Length
とDate
ヘッダーの設定ロジックの変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -694,7 +737,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { // "keep-alive" connections alive. if w.handlerDone && header.get("Content-Length") == "" && w.req.Method != "HEAD" { w.contentLength = int64(len(p)) - setHeader.contentLength = strconv.Itoa(len(p)) + setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10) } // If this was an HTTP/1.0 request with keep-alive and we sent a @@ -755,7 +798,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { } if _, ok := header["Date"]; !ok { - setHeader.date = time.Now().UTC().Format(TimeFormat) + setHeader.date = appendTime(cw.res.dateBuf[:0], time.Now()) } te := header.get("Transfer-Encoding") @@ -806,7 +849,7 @@ func (cw *chunkWriter) writeHeader(p []byte) { io.WriteString(w.conn.buf, statusLine(w.req, code)) cw.header.WriteSubset(w.conn.buf, excludeHeader) - setHeader.Write(w.conn.buf) + setHeader.Write(w.conn.buf.Writer) w.conn.buf.Write(crlf) }
コアとなるコードの解説
このコミットの核心は、HTTPレスポンスヘッダー、特に Date
と Content-Length
の生成と書き込みにおけるメモリ割り当てを排除することです。
-
response
構造体内の固定バッファ (dateBuf
,clenBuf
):response
構造体は、各HTTPリクエストの処理中に生成されるレスポンスの状態を保持します。ここにdateBuf
とclenBuf
という固定サイズのバイト配列が追加されたことで、Date
とContent-Length
ヘッダーの値を文字列としてヒープに割り当てる必要がなくなりました。- これらのバッファはスタック上に確保されるか、または
response
構造体自体がヒープに割り当てられる場合でも、その内部に埋め込まれるため、ヘッダー値ごとに個別の割り当てが発生するのを防ぎます。これにより、ガベージコレクションの対象となるオブジェクトの数が減少し、GCの頻度と負荷が軽減されます。
-
appendTime
関数の役割:- Goの
time.Format
関数は非常に便利ですが、内部で新しい文字列を生成するため、ヒープ割り当てが発生します。HTTPサーバーのようにDate
ヘッダーを頻繁に生成する場面では、これがパフォーマンスのボトルネックになり得ます。 appendTime
関数は、time.Time
オブジェクトから日付と時刻の各要素(年、月、日、時、分、秒、曜日)を抽出し、それらを直接バイトスライスに変換して既存のバッファ (dateBuf
) に追記します。この手動でのフォーマットにより、time.Format
が引き起こす文字列割り当てを完全に回避し、ゼロアロケーションでDate
ヘッダーを生成できるようになります。
- Goの
-
extraHeader
の[]byte
化とWrite
メソッドの最適化:extraHeader
構造体は、HTTPレスポンスに含める追加のヘッダーを効率的に管理するためのものです。date
とcontentLength
フィールドがstring
から[]byte
に変更されたことで、ヘッダー値を文字列として保持する際の割り当てが不要になりました。extraHeader.Write
メソッドでは、Date
とContent-Length
ヘッダーが、事前に定義されたバイトスライス (headerDate
,headerContentLength
) と、response
構造体内のバッファ (h.date
,h.contentLength
) を直接w.Write
で書き込むように変更されました。これにより、文字列からバイトスライスへの変換や、io.WriteString
のようなヘルパー関数を介した間接的な書き込みが不要になり、より直接的かつ効率的なバイト列の書き込みが可能になります。- また、
w
の型がio.Writer
から*bufio.Writer
に変更されたことで、WriteString
メソッドが*bufio.Writer
の具体的なメソッドとして呼び出されるようになり、インターフェース呼び出しによるわずかなオーバーヘッドも削減されています。
-
strconv.AppendInt
の活用:Content-Length
ヘッダーの値を設定する際に、strconv.Itoa
(整数を文字列に変換し、新しい文字列を割り当てる) の代わりにstrconv.AppendInt
が使用されるようになりました。strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10)
は、response
構造体内のclenBuf
バッファを再利用し、そこにContent-Length
の整数値を10進数文字列として追記します。これにより、Content-Length
ヘッダーの生成時にもヒープ割り当てが発生しなくなります。
これらの変更は、GoのHTTPサーバーが各リクエストを処理する際のメモリフットプリントを最小限に抑え、ガベージコレクションの負担を軽減することで、全体的なスループットと応答性を向上させることに貢献しています。これは、Go言語の「ゼロアロケーション」または「低アロケーション」プログラミングの原則を実践した良い例であり、高性能なシステムを構築する上で重要な最適化手法です。
関連リンク
- Go言語の
net/http
パッケージ: https://pkg.go.dev/net/http - Go言語の
time
パッケージ: https://pkg.go.dev/time - Go言語の
strconv
パッケージ: https://pkg.go.dev/strconv - Go言語のメモリ管理とガベージコレクションに関する一般的な情報:
- The Go Memory Model: https://go.dev/ref/mem
- Go's work-stealing garbage collector: https://go.dev/blog/go15gc
参考にした情報源リンク
- Goのコミット履歴とソースコード: https://github.com/golang/go
- Goのコードレビューシステム (Gerrit): https://go.dev/cl/9432046 (コミットメッセージに記載されているCLリンク)
- GoのIssue Tracker: https://go.dev/issue/5195 (コミットメッセージに記載されているUpdate #5195)
- HTTP/1.1 RFC 2616 (DateヘッダーとContent-Lengthヘッダーの定義): https://www.rfc-editor.org/rfc/rfc2616
- Goにおけるパフォーマンス最適化とアロケーション削減に関する記事 (一般的な情報源):
- "Go: Optimizing for zero allocations" by Dave Cheney: https://dave.cheney.net/2014/06/07/optimizing-for-zero-allocations
- "Go Performance Tuning" by Ardan Labs: https://www.ardanlabs.com/blog/2017/05/go-performance-tuning-part-1-optimizing-for-cpu.html (シリーズ記事)
- Goのベンチマークに関するドキュメント: https://pkg.go.dev/testing#hdr-Benchmarks
b.ReportAllocs()
の使用法: https://pkg.go.dev/testing#B.ReportAllocs- Goの
bufio
パッケージ: https://pkg.go.dev/bufio - Goの
io
パッケージ: https://pkg.go.dev/io - Goの
append
関数: https://go.dev/blog/slices