[インデックス 13267] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/textproto
パッケージ内のreader.go
およびreader_test.go
ファイルに対する変更です。net/textproto
パッケージは、HTTPやSMTPなどのテキストベースのプロトコルでMIMEヘッダーやその他のテキストデータを効率的に読み書きするための機能を提供します。
reader.go
は、MIMEヘッダーの読み取りロジック、特にヘッダーキーの正規化(Canonicalization)に関する処理を担っています。reader_test.go
は、その機能に対するテストと、このコミットで追加されるベンチマークが含まれています。
コミット
このコミットの主な目的は、net/textproto
パッケージのReadMIMEHeader
関数にベンチマークを追加し、関連するコードのクリーンアップとコメントの更新を行うことです。クリーンアップの一環として、MIMEヘッダーキーのパース処理が改善され、約5%のパフォーマンス向上が副次的に達成されていますが、コミットメッセージにもある通り、これは主要な目的ではありません。将来的な最適化の基盤を整えることが意図されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e3d87f315f80ff6b5c0275c98a04f635679ef6b
元コミット内容
commit 6e3d87f315f80ff6b5c0275c98a04f635679ef6b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Jun 4 07:18:06 2012 -0700
net/textproto: add benchmark, cleanup, update comment
The cleanup also makes it ~5% faster, but that's
not the point of this CL.
Optimizations can come in future CLs.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6286043
--
src/pkg/net/textproto/reader.go | 31 ++++++++++++++++++------------
src/pkg/net/textproto/reader_test.go | 17 +++++++++++++++++
2 files changed, 35 insertions(+), 13 deletions(-)
変更の背景
この変更は、主に以下の目的のために行われました。
- ベンチマークの追加:
ReadMIMEHeader
関数のパフォーマンスを測定し、将来的な最適化のベースラインとするためのベンチマークを追加します。これにより、コード変更がパフォーマンスに与える影響を定量的に評価できるようになります。 - コードのクリーンアップと改善:
ReadMIMEHeader
関数内のMIMEヘッダーキーのパースロジックを改善し、より堅牢で効率的な処理を実現します。特に、MIMEヘッダーの仕様に違反してキーにスペースが含まれる場合があるという現実世界の状況に対応するための修正が含まれています。 - コメントの更新: コードの意図をより正確に反映するようにコメントを更新します。
- 副次的なパフォーマンス向上: クリーンアップとリファクタリングの結果として、約5%のパフォーマンス向上が見られましたが、これは意図された主要な目的ではなく、コードの健全性を高める過程で得られた副産物です。
前提知識の解説
MIMEヘッダー
MIME (Multipurpose Internet Mail Extensions) ヘッダーは、電子メールやHTTPなどのインターネットプロトコルにおいて、メッセージのメタデータ(送信者、受信者、件名、コンテンツタイプなど)を記述するために使用される構造です。MIMEヘッダーは通常、「Key: Value
」の形式で構成され、各ヘッダーは改行で区切られます。
重要な点として、MIMEヘッダーのキー(フィールド名)は、大文字・小文字を区別しないとされています(例: Content-Type
とcontent-type
は同じ意味)。そのため、プログラムでこれらのヘッダーを扱う際には、一貫した形式(正規化された形式、またはCanonical Form)に変換する「正規化(Canonicalization)」処理がしばしば行われます。例えば、Content-Type
のように、各単語の先頭が大文字で、それ以外が小文字、単語間はハイフンで区切られる形式が一般的です。
また、MIMEヘッダーの仕様(RFC 2045など)では、ヘッダーキーにスペースを含めることは許可されていませんが、現実世界では仕様に違反した形式のヘッダーが送られてくることがあります。堅牢なパーサーは、このような「不正な」入力にも対応できる必要があります。
Goのnet/textproto
パッケージ
net/textproto
パッケージは、Go言語の標準ライブラリの一部であり、テキストベースのネットワークプロトコル(HTTP、SMTP、NNTPなど)で共通して使用されるテキストデータの読み書きを支援する機能を提供します。このパッケージは、特にMIMEヘッダーのパースや生成、行ベースのデータの読み取りなどに特化しています。
Reader
型:bufio.Reader
をラップし、行の読み取り、MIMEヘッダーの読み取り、マルチパートメッセージのパースなどの高レベルな機能を提供します。MIMEHeader
型:map[string][]string
のエイリアスであり、MIMEヘッダーのキーと値のペアを表現するために使用されます。キーは正規化された形式で格納されます。ReadMIMEHeader()
メソッド: 入力ストリームからMIMEヘッダーブロック全体を読み取り、MIMEHeader
マップとして返します。このメソッドは、ヘッダーキーの正規化も行います。CanonicalMIMEHeaderKey()
関数: 与えられた文字列をMIMEヘッダーの正規形式に変換します。
Goにおけるバイトスライスと文字列の変換
Go言語では、文字列(string
)は不変(immutable)なバイトのシーケンスであり、UTF-8エンコードされたテキストを表します。一方、バイトスライス([]byte
)は可変(mutable)なバイトのシーケンスです。
文字列とバイトスライスの間には相互変換が可能ですが、変換にはコストがかかる場合があります。特に、文字列からバイトスライスへの変換([]byte(s)
)やその逆(string(b)
)は、新しいメモリ割り当てとデータのコピーを伴うため、頻繁に行われるとパフォーマンスのボトルネックになることがあります。
パフォーマンスが重要な場面では、可能な限りバイトスライスで直接操作を行い、文字列への変換は必要な場合にのみ行うことが推奨されます。このコミットの変更点も、この原則に基づいています。
技術的詳細
このコミットにおける技術的な変更は、主にsrc/pkg/net/textproto/reader.go
内のMIMEヘッダーのパースと正規化ロジックに集中しています。
-
ReadMIMEHeader()
におけるヘッダーキーのスペース処理の改善: 以前のコードでは、MIMEヘッダーキーにスペースが含まれている場合、strings.Index
とstrings.TrimRight
を使用してスペースをトリミングしていました。しかし、これは効率的ではなく、また、キーの末尾にスペースがある場合にのみ対応していました。 新しいコードでは、bytes.IndexByte
でコロンの位置を見つけた後、for endKey > 0 && kv[endKey-1] == ' ' { endKey-- }
というループを使って、コロンの直前のスペースを効率的に削除しています。これにより、MIMEヘッダーの仕様に違反してキーにスペースが含まれる場合でも、より堅牢に処理できるようになりました。コメントも「should not have spaces but they appear in the wild, violating specs, so we remove them if present.」と更新され、この変更の意図が明確になっています。 -
CanonicalMIMEHeaderKey()
関数のリファクタリングと最適化: 既存のCanonicalMIMEHeaderKey(s string)
関数は、与えられた文字列s
が既に正規形式であるかをチェックし、そうでない場合にgoto MustRewrite
を使って正規化処理を行うという構造でした。このgoto
の使用はGoのイディオムとしては推奨されません。 新しい実装では、goto
を廃止し、正規化が必要な場合に新しい内部ヘルパー関数canonicalMIMEHeaderKey([]byte)
を呼び出すように変更されました。 -
新しい内部ヘルパー関数
canonicalMIMEHeaderKey(a []byte) string
の導入: このコミットの最も重要な技術的変更点の一つは、canonicalMIMEHeaderKey
という新しい非公開(小文字で始まる)ヘルパー関数の導入です。この関数は[]byte
スライスを引数に取り、そのスライスを直接変更(mutate)して正規化を行います。- パフォーマンスの向上: 従来の
CanonicalMIMEHeaderKey
が文字列を引数に取り、内部で[]byte(s)
と変換して操作していたのに対し、この新しい関数は既にバイトスライスとして渡されたデータを直接操作します。これにより、頻繁な文字列とバイトスライスの間の変換に伴うメモリ割り当てとコピーのオーバーヘッドが削減され、約5%のパフォーマンス向上に寄与しています。 - 可変性(Mutability)の活用: Goの文字列は不変であるため、文字列を正規化する際には新しい文字列を生成する必要がありました。しかし、バイトスライスは可変であるため、既存のバイトスライスをその場で変更し、最後に
string(a)
で文字列に変換して返すことができます。これにより、中間的なメモリ割り当てを減らすことが可能になります。
- パフォーマンスの向上: 従来の
-
ベンチマークの追加:
src/pkg/net/textproto/reader_test.go
にBenchmarkReadMIMEHeader
という新しいベンチマーク関数が追加されました。このベンチマークは、典型的なMIMEヘッダーブロックを繰り返し読み取ることで、ReadMIMEHeader
関数のパフォーマンスを測定します。これにより、将来の変更がこの関数の性能に与える影響を容易に評価できるようになります。
コアとなるコードの変更箇所
src/pkg/net/textproto/reader.go
--- a/src/pkg/net/textproto/reader.go
+++ b/src/pkg/net/textproto/reader.go
@@ -452,16 +452,18 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
return m, err
}
- // Key ends at first colon; must not have spaces.
+ // Key ends at first colon; should not have spaces but
+ // they appear in the wild, violating specs, so we
+ // remove them if present.
i := bytes.IndexByte(kv, ':')
if i < 0 {
return m, ProtocolError("malformed MIME header line: " + string(kv))
}
- key := string(kv[0:i])
- if strings.Index(key, " ") >= 0 {
- key = strings.TrimRight(key, " ")
+ endKey := i
+ for endKey > 0 && kv[endKey-1] == ' ' {
+ endKey--
}
- key = CanonicalMIMEHeaderKey(key)
+ key := canonicalMIMEHeaderKey(kv[:endKey])
// Skip initial spaces in value.
i++ // skip colon
@@ -486,25 +488,28 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
// canonical key for "accept-encoding" is "Accept-Encoding".
func CanonicalMIMEHeaderKey(s string) string {
// Quick check for canonical encoding.
- needUpper := true
+ upper := true
for i := 0; i < len(s); i++ {
c := s[i]
- if needUpper && 'a' <= c && c <= 'z' {
- goto MustRewrite
+ if upper && 'a' <= c && c <= 'z' {
+ return canonicalMIMEHeaderKey([]byte(s))
}
- if !needUpper && 'A' <= c && c <= 'Z' {
- goto MustRewrite
+ if !upper && 'A' <= c && c <= 'Z' {
+ return canonicalMIMEHeaderKey([]byte(s))
}
- needUpper = c == '-'
+ upper = c == '-'
}
return s
+}
-MustRewrite:
+// canonicalMIMEHeaderKey is like CanonicalMIMEHeaderKey but is
+// allowed to mutate the provided byte slice before returning the
+// string.
+func canonicalMIMEHeaderKey(a []byte) string {
// Canonicalize: first letter upper case
// and upper case after each dash.
// (Host, User-Agent, If-Modified-Since).
// MIME headers are ASCII only, so no Unicode issues.
- a := []byte(s)
upper := true
for i, v := range a {
if v == ' ' {
src/pkg/net/textproto/reader_test.go
--- a/src/pkg/net/textproto/reader_test.go
+++ b/src/pkg/net/textproto/reader_test.go
@@ -6,6 +6,7 @@ package textproto
import (
"bufio"
+ "bytes"
"io"
"reflect"
"strings"
@@ -239,3 +240,19 @@ func TestRFC959Lines(t *testing.T) {
}
}
}\n+\n+func BenchmarkReadMIMEHeader(b *testing.B) {\n+\tvar buf bytes.Buffer\n+\tbr := bufio.NewReader(&buf)\n+\tr := NewReader(br)\n+\tfor i := 0; i < b.N; i++ {\n+\t\tbuf.WriteString("User-Agent: not mozilla\\r\\nContent-Length: 23452\\r\\nContent-Type: text/html; charset-utf8\\r\\nFoo-Bar: foobar\\r\\nfoo-bar: some more string\\r\\n\\r\\n")\n+\t\th, err := r.ReadMIMEHeader()\n+\t\tif err != nil {\n+\t\t\tb.Fatal(err)\n+\t\t}\n+\t\tif len(h) != 4 {\n+\t\t\tb.Fatalf("want 4")\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/net/textproto/reader.go`の変更点
1. **`ReadMIMEHeader()`関数内の変更**:
* **コメントの更新**: `// Key ends at first colon; must not have spaces.` から `// Key ends at first colon; should not have spaces but // they appear in the wild, violating specs, so we // remove them if present.` に変更されました。これは、MIMEヘッダーの仕様ではキーにスペースを含めるべきではないが、現実にはそのような不正なヘッダーが存在するため、それらを適切に処理する必要があるという背景を明確にしています。
* **スペース除去ロジックの改善**:
* 旧: `if strings.Index(key, " ") >= 0 { key = strings.TrimRight(key, " ") }`
* 新: `endKey := i; for endKey > 0 && kv[endKey-1] == ' ' { endKey-- }`
この変更により、コロンの直前のスペースを効率的に削除できるようになりました。`strings.Index`や`strings.TrimRight`は文字列操作であり、内部で新しい文字列を生成する可能性があるため、バイトスライスを直接操作するこの新しいループの方がパフォーマンスが向上します。
* **正規化関数の呼び出し変更**: `key = CanonicalMIMEHeaderKey(key)` から `key := canonicalMIMEHeaderKey(kv[:endKey])` に変更されました。これは、新しく導入された内部ヘルパー関数`canonicalMIMEHeaderKey`を呼び出すようにしたものです。このヘルパー関数はバイトスライスを引数に取るため、`kv[:endKey]`というバイトスライスを直接渡すことで、文字列への変換コストを削減しています。
2. **`CanonicalMIMEHeaderKey(s string)`関数の変更**:
* **`goto MustRewrite`の廃止**: 以前のコードでは、正規化が必要な場合に`goto MustRewrite`を使用していました。これはGoのイディオムとしては推奨されないため、`return canonicalMIMEHeaderKey([]byte(s))`という形で、新しく導入されたヘルパー関数を呼び出すように変更されました。これにより、コードの可読性と保守性が向上します。
* **`needUpper`変数のリネーム**: `needUpper`が`upper`にリネームされました。これは意味的な変更ではなく、単なる変数名の改善です。
3. **`canonicalMIMEHeaderKey(a []byte) string`関数の新規追加**:
* この関数は、MIMEヘッダーキーの正規化処理を担う非公開(エクスポートされない)ヘルパー関数です。
* 引数として`[]byte`スライス`a`を受け取ります。この関数は、引数として渡されたバイトスライスを**直接変更(mutate)することを許可**しています。これは、Goの文字列が不変であるのに対し、バイトスライスは可変であるという特性を利用した重要な最適化です。
* 正規化ロジック自体は、各単語の先頭を大文字にし、それ以外を小文字にするというMIMEヘッダーの標準的な正規化ルールに従っています(例: `content-type` -> `Content-Type`)。
* バイトスライスを直接操作することで、中間的な文字列の生成とコピーを避けることができ、これが全体のパフォーマンス向上に寄与しています。最後に`string(a)`で正規化されたバイトスライスを文字列に変換して返します。
### `src/pkg/net/textproto/reader_test.go`の変更点
1. **`bytes`パッケージのインポート**: `bytes.Buffer`を使用するために`"bytes"`パッケージが追加されました。
2. **`BenchmarkReadMIMEHeader`関数の新規追加**:
* このベンチマーク関数は、`testing`パッケージの`Benchmark`規約に従って実装されています。
* `bytes.Buffer`を使用して、テスト用のMIMEヘッダーブロック(`User-Agent`, `Content-Length`, `Content-Type`, `Foo-Bar`, `foo-bar`)を構築します。
* `b.N`回ループし、各イテレーションで`buf.WriteString`でヘッダーデータをバッファに書き込み、`r.ReadMIMEHeader()`を呼び出してヘッダーを読み取ります。
* 読み取ったヘッダーの数(`len(h)`)が期待通りであるか(この場合は4)をチェックし、エラーが発生した場合は`b.Fatal`でベンチマークを停止します。
* このベンチマークは、`ReadMIMEHeader`関数が実際のMIMEヘッダーをパースする際のパフォーマンスを測定するために使用されます。
これらの変更は、コードのクリーンアップ、堅牢性の向上、そしてパフォーマンスの微細な最適化を同時に実現しています。特に、バイトスライスを直接操作する新しい正規化ヘルパー関数の導入は、Goにおけるパフォーマンスチューニングの典型的なアプローチを示しています。
## 関連リンク
* Go言語公式ドキュメント: [https://golang.org/](https://golang.org/)
* `net/textproto`パッケージのドキュメント: [https://pkg.go.dev/net/textproto](https://pkg.go.dev/net/textproto)
* Goの`testing`パッケージ(ベンチマークについて): [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
## 参考にした情報源リンク
* RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies: [https://datatracker.ietf.org/doc/html/rfc2045](https://datatracker.ietf.org/doc/html/rfc2045)
* Go言語の文字列とバイトスライスに関する一般的な情報源(例: Goの公式ブログやGoに関する技術記事)
* Goのコードレビューシステム (Gerrit) の変更リスト (CL): [https://golang.org/cl/6286043](https://golang.org/cl/6286043) (コミットメッセージに記載されているリンク)