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

[インデックス 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の減少)と、メモリ割り当ての削減(allocsbytesの減少)が主要な目標でした。メモリ割り当ての削減は、ガベージコレクションの頻度を減らし、全体的なアプリケーションのレイテンシとスループットを向上させる上で非常に重要です。

前提知識の解説

  • 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/textprotoreadContinuedLineSlice 関数は、この継続行を適切に処理するために存在します。
  • bufio.Reader: Go言語の io パッケージで提供されるバッファリングされたリーダーです。ディスクI/OやネットワークI/Oの効率を向上させるために、データを内部バッファに読み込んでからアプリケーションに提供します。
    • Buffered(): bufio.Reader のメソッドで、現在バッファリングされている読み取り可能なバイト数を返します。
    • Peek(n): bufio.Reader のメソッドで、次の n バイトを読み取らずに(バッファから削除せずに)返します。バッファに十分なバイトがない場合、エラーを返します。
  • make(map, hint): Go言語の make 関数は、マップを初期化する際にオプションで容量のヒント(hint)を受け取ることができます。このヒントは、マップが事前に割り当てるメモリ量を指定し、要素が追加される際の動的な再ハッシュや再割り当ての回数を減らすことで、パフォーマンスを向上させることができます。

技術的詳細

このコミットは、主に以下の2つの技術的な最適化を導入しています。

  1. 非継続行のための高速パス (readContinuedLineSlice 内): readContinuedLineSlice 関数は、ヘッダー行とその継続行を読み取るために使用されます。従来のロジックでは、常に継続行の可能性を考慮して、次の行の先頭がスペースやタブであるかをチェックしていました。しかし、ほとんどのヘッダー行は継続行ではありません。 この変更では、bufio.Reader.Buffered()bufio.Reader.Peek(1) を使用して、次のバイトがバッファに存在し、それがASCII文字(ヘッダーキーの開始文字)であるかどうかを「楽観的に」チェックします。

    • もし次のバイトがバッファにあり、それがASCII文字(isASCIILetter で判定)であれば、それは新しいヘッダー行の開始であると判断できます。この場合、現在の行は継続行ではないことが確定するため、余分なバッファのコピーや空白のスキップ処理を回避し、すぐに現在の行を返します。これにより、不要な処理をスキップし、パフォーマンスを向上させます。
    • この最適化は、特に継続行が少ない一般的なHTTPリクエストの解析において効果を発揮します。
  2. MIMEヘッダーマップの初期サイズヒント (ReadMIMEHeader 内): ReadMIMEHeader 関数は、複数のヘッダー行を読み取り、それらを MIMEHeader (実体は map[string][]string) に格納します。従来のコードでは、マップを m := make(MIMEHeader) のように初期化していました。これは、マップの初期容量を指定しないため、要素が追加されるたびに必要に応じてマップが動的に拡張され、その際にメモリの再割り当てと要素の再ハッシュが発生する可能性がありました。 この変更では、m := make(MIMEHeader, 4) のように、初期容量のヒントとして 4 を渡しています。これは、一般的なHTTPリクエストには少なくとも数個のヘッダー(例: Content-Type, Content-Length, Date, Server など)が含まれることを考慮したものです。

    • マップの初期容量を適切に設定することで、マップの成長に伴う再割り当ての回数を減らすことができます。これにより、メモリ割り当てのオーバーヘッドが削減され、ガベージコレクションの負荷が軽減され、全体的なパフォーマンスが向上します。ベンチマーク結果の allocsbytes の減少は、この最適化の直接的な効果を示しています。

コアとなるコードの変更箇所

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

コアとなるコードの解説

  1. 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に設定され、一般的なヘッダー数に対応できるようになり、動的な再割り当ての頻度が減少します。
  2. 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' の範囲にあるかをチェックします。

これらの変更により、特にヘッダーの継続行が少ない一般的なケースにおいて、ヘッダー解析のオーバーヘッドが大幅に削減され、全体的なパフォーマンスとメモリ効率が向上しています。

関連リンク

参考にした情報源リンク

  • Go言語の net/textproto パッケージのドキュメント
  • Go言語の bufio パッケージのドキュメント
  • MIMEヘッダーの仕様 (RFC 2045など)
  • Go言語のマップの内部実装に関する一般的な知識