[インデックス 15237] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/textproto
パッケージにおけるMIMEヘッダーの解析効率を向上させるものです。特に、HTTPヘッダーなどのテキストプロトコルヘッダーの読み込みにおいて、パフォーマンスの最適化とメモリ割り当ての削減を目的としています。主な変更点は、ヘッダーの継続行でない場合に高速パスを追加することと、MIMEヘッダーマップの初期サイズを適切にヒントとして与えることで、動的な再割り当てを減らすことです。
コミット
commit 2803744b86a58054e052a9520d7c17ab41acd96c
Author: Dave Cheney <dave@cheney.net>
Date: Thu Feb 14 19:35:38 2013 +1100
net/textproto: more efficient header parsing
A co creation with bradfitz
* add fast path for header lines which are not continuations
* pass hint to better size initial mime header map
lucky(~/go/src/pkg/net/http) % ~/go/misc/benchcmp {golden,new}.txt
benchmark old ns/op new ns/op delta
BenchmarkReadRequestChrome 10073 8348 -17.12%
BenchmarkReadRequestCurl 4368 4350 -0.41%
BenchmarkReadRequestApachebench 4412 4397 -0.34%
BenchmarkReadRequestSiege 6431 5924 -7.88%
BenchmarkReadRequestWrk 2820 3146 +11.56%
benchmark old MB/s new MB/s speedup
BenchmarkReadRequestChrome 60.66 73.18 1.21x
BenchmarkReadRequestCurl 17.85 17.93 1.00x
BenchmarkReadRequestApachebench 18.58 18.65 1.00x
BenchmarkReadRequestSiege 23.48 25.49 1.09x
BenchmarkReadRequestWrk 14.18 12.71 0.90x
benchmark old allocs new allocs delta
BenchmarkReadRequestChrome 32 26 -18.75%
BenchmarkReadRequestCurl 15 15 0.00%
BenchmarkReadRequestApachebench 16 15 -6.25%
BenchmarkReadRequestSiege 22 19 -13.64%
BenchmarkReadRequestWrk 11 11 0.00%
benchmark old bytes new bytes delta
BenchmarkReadRequestChrome 3148 2216 -29.61%
BenchmarkReadRequestCurl 905 1413 56.13%
BenchmarkReadRequestApachebench 956 1413 47.80%
BenchmarkReadRequestSiege 1397 1522 8.95%
BenchmarkReadRequestWrk 757 1369 80.85%
R=bradfitz
CC=golang-dev
https://golang.org/cl/7300098
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2803744b86a58054e052a9520d7c17ab41acd96c
元コミット内容
net/textproto
パッケージにおけるヘッダー解析の効率化。
Brad Fitzpatrickとの共同作業。
- 継続行ではないヘッダー行のための高速パスを追加。
- MIMEヘッダーマップの初期サイズをより適切に設定するためのヒントを渡す。
ベンチマーク結果が示されており、特に BenchmarkReadRequestChrome
において、ns/op
(操作あたりのナノ秒) が17.12%減少し、allocs
(メモリ割り当て回数) が18.75%減少し、bytes
(割り当てバイト数) が29.61%減少するなど、顕著な改善が見られます。
変更の背景
この変更の背景には、Go言語のネットワークプログラミングにおいて、HTTPヘッダーなどのテキストベースのプロトコルヘッダーの解析がボトルネックとなる可能性があったことが挙げられます。特に、多数のヘッダーを持つリクエストや、継続行(複数行にわたるヘッダー値)が少ない一般的なケースにおいて、既存のヘッダー解析ロジックが非効率であるという課題がありました。
コミットメッセージに示されているベンチマーク結果は、この最適化の必要性を明確に示しています。特に BenchmarkReadRequestChrome
のような現実的なシナリオ(Chromeブラウザからのリクエストをシミュレート)で大幅なパフォーマンス改善が見られることは、この変更が実際のアプリケーションの応答性向上に寄与することを示唆しています。具体的には、処理時間の短縮(ns/op
の減少)と、メモリ割り当ての削減(allocs
とbytes
の減少)が主要な目標でした。メモリ割り当ての削減は、ガベージコレクションの頻度を減らし、全体的なアプリケーションのレイテンシとスループットを向上させる上で非常に重要です。
前提知識の解説
net/textproto
パッケージ: Go言語の標準ライブラリの一部で、テキストベースのネットワークプロトコル(HTTP、SMTP、NNTPなど)を解析するための低レベルなプリミティブを提供します。行指向の読み込みや、MIMEヘッダーの解析などを行います。- MIMEヘッダー: Multipurpose Internet Mail Extensions (MIME) で定義されるヘッダー形式で、HTTPヘッダーもこれに準拠しています。
Key: Value
の形式で、複数のヘッダーが連続して記述されます。 - 継続行 (Continuation Lines): MIMEヘッダーの仕様では、ヘッダーの値が非常に長い場合、次の行の先頭にスペースまたはタブを置くことで、その行が前のヘッダーの継続であることを示すことができます。例:
Subject: This is a very long subject line that continues on multiple lines.
net/textproto
のreadContinuedLineSlice
関数は、この継続行を適切に処理するために存在します。 bufio.Reader
: Go言語のio
パッケージで提供されるバッファリングされたリーダーです。ディスクI/OやネットワークI/Oの効率を向上させるために、データを内部バッファに読み込んでからアプリケーションに提供します。Buffered()
:bufio.Reader
のメソッドで、現在バッファリングされている読み取り可能なバイト数を返します。Peek(n)
:bufio.Reader
のメソッドで、次のn
バイトを読み取らずに(バッファから削除せずに)返します。バッファに十分なバイトがない場合、エラーを返します。
make(map, hint)
: Go言語のmake
関数は、マップを初期化する際にオプションで容量のヒント(hint
)を受け取ることができます。このヒントは、マップが事前に割り当てるメモリ量を指定し、要素が追加される際の動的な再ハッシュや再割り当ての回数を減らすことで、パフォーマンスを向上させることができます。
技術的詳細
このコミットは、主に以下の2つの技術的な最適化を導入しています。
-
非継続行のための高速パス (
readContinuedLineSlice
内):readContinuedLineSlice
関数は、ヘッダー行とその継続行を読み取るために使用されます。従来のロジックでは、常に継続行の可能性を考慮して、次の行の先頭がスペースやタブであるかをチェックしていました。しかし、ほとんどのヘッダー行は継続行ではありません。 この変更では、bufio.Reader.Buffered()
とbufio.Reader.Peek(1)
を使用して、次のバイトがバッファに存在し、それがASCII文字(ヘッダーキーの開始文字)であるかどうかを「楽観的に」チェックします。- もし次のバイトがバッファにあり、それがASCII文字(
isASCIILetter
で判定)であれば、それは新しいヘッダー行の開始であると判断できます。この場合、現在の行は継続行ではないことが確定するため、余分なバッファのコピーや空白のスキップ処理を回避し、すぐに現在の行を返します。これにより、不要な処理をスキップし、パフォーマンスを向上させます。 - この最適化は、特に継続行が少ない一般的なHTTPリクエストの解析において効果を発揮します。
- もし次のバイトがバッファにあり、それがASCII文字(
-
MIMEヘッダーマップの初期サイズヒント (
ReadMIMEHeader
内):ReadMIMEHeader
関数は、複数のヘッダー行を読み取り、それらをMIMEHeader
(実体はmap[string][]string
) に格納します。従来のコードでは、マップをm := make(MIMEHeader)
のように初期化していました。これは、マップの初期容量を指定しないため、要素が追加されるたびに必要に応じてマップが動的に拡張され、その際にメモリの再割り当てと要素の再ハッシュが発生する可能性がありました。 この変更では、m := make(MIMEHeader, 4)
のように、初期容量のヒントとして4
を渡しています。これは、一般的なHTTPリクエストには少なくとも数個のヘッダー(例:Content-Type
,Content-Length
,Date
,Server
など)が含まれることを考慮したものです。- マップの初期容量を適切に設定することで、マップの成長に伴う再割り当ての回数を減らすことができます。これにより、メモリ割り当てのオーバーヘッドが削減され、ガベージコレクションの負荷が軽減され、全体的なパフォーマンスが向上します。ベンチマーク結果の
allocs
とbytes
の減少は、この最適化の直接的な効果を示しています。
- マップの初期容量を適切に設定することで、マップの成長に伴う再割り当ての回数を減らすことができます。これにより、メモリ割り当てのオーバーヘッドが削減され、ガベージコレクションの負荷が軽減され、全体的なパフォーマンスが向上します。ベンチマーク結果の
コアとなるコードの変更箇所
src/pkg/net/textproto/reader.go
--- a/src/pkg/net/textproto/reader.go
+++ b/src/pkg/net/textproto/reader.go
@@ -128,6 +128,17 @@ func (r *Reader) readContinuedLineSlice() ([]byte, error) {
\treturn line, nil
}
+\t// Optimistically assume that we have started to buffer the next line
+\t// and it starts with an ASCII letter (the next header key), so we can
+\t// avoid copying that buffered data around in memory and skipping over
+\t// non-existent whitespace.
+\tif r.R.Buffered() > 1 {
+\t\tpeek, err := r.R.Peek(1)
+\t\tif err == nil && isASCIILetter(peek[0]) {
+\t\t\treturn trim(line), nil
+\t\t}\n+\t}
+\n \t// ReadByte or the next readLineSlice will flush the read buffer;
\t// copy the slice into buf.
\tr.buf = append(r.buf[:0], trim(line)...)
@@ -445,7 +456,7 @@ func (r *Reader) ReadDotLines() ([]string, error) {
//\t}\n //\n func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
-\tm := make(MIMEHeader)\n+\tm := make(MIMEHeader, 4)
\tfor {\n \t\tkv, err := r.readContinuedLineSlice()\n \t\tif len(kv) == 0 {
src/pkg/net/textproto/textproto.go
--- a/src/pkg/net/textproto/textproto.go
+++ b/src/pkg/net/textproto/textproto.go
@@ -147,3 +147,8 @@ func TrimBytes(b []byte) []byte {
func isASCIISpace(b byte) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}
+\n+func isASCIILetter(b byte) bool {
+\tb |= 0x20 // make lower case
+\treturn 'a' <= b && b <= 'z'
+}\n
コアとなるコードの解説
-
src/pkg/net/textproto/reader.go
の変更:readContinuedLineSlice
関数内に新しい最適化ブロックが追加されました。if r.R.Buffered() > 1
は、bufio.Reader
のバッファに少なくとも1バイト(次の行の最初の文字)が読み込まれていることを確認します。これにより、Peek
を安全に呼び出せることを保証します。peek, err := r.R.Peek(1)
は、次の1バイトをバッファから読み取らずに覗き見ます。if err == nil && isASCIILetter(peek[0])
は、エラーがなく、かつ覗き見したバイトがASCII文字('a'
から'z'
または'A'
から'Z'
)であるかをチェックします。isASCIILetter
関数は、大文字小文字を区別しないようにb |= 0x20
で小文字に変換してから比較します。- もしこの条件が真であれば、次の行は新しいヘッダーキーの開始であると「楽観的に」判断し、現在の行が継続行ではないと結論付けます。そのため、
trim(line)
を適用してすぐに現在の行を返します。これにより、継続行の解析ロジック(空白のスキップなど)を完全にバイパスできます。 ReadMIMEHeader
関数では、m := make(MIMEHeader)
がm := make(MIMEHeader, 4)
に変更されました。これにより、マップの初期容量が4に設定され、一般的なヘッダー数に対応できるようになり、動的な再割り当ての頻度が減少します。
-
src/pkg/net/textproto/textproto.go
の変更:- 新しく
isASCIILetter(b byte) bool
関数が追加されました。 - この関数は、与えられたバイト
b
がASCIIのアルファベット文字(a-zまたはA-Z)であるかを判定します。 b |= 0x20
は、ビット演算を使用してバイトを小文字に変換する効率的な方法です。例えば、'A'
(0x41) に0x20
をORすると'a'
(0x61) になります。これにより、大文字と小文字の両方を一度にチェックできます。- その後、変換されたバイトが
'a'
から'z'
の範囲にあるかをチェックします。
- 新しく
これらの変更により、特にヘッダーの継続行が少ない一般的なケースにおいて、ヘッダー解析のオーバーヘッドが大幅に削減され、全体的なパフォーマンスとメモリ効率が向上しています。
関連リンク
- https://golang.org/cl/7300098 (このコミットのChange List)
参考にした情報源リンク
- Go言語の
net/textproto
パッケージのドキュメント - Go言語の
bufio
パッケージのドキュメント - MIMEヘッダーの仕様 (RFC 2045など)
- Go言語のマップの内部実装に関する一般的な知識