[インデックス 13386] ファイルの概要
このコミットでは、Go言語の標準ライブラリである net/http
パッケージ内の Header.WriteSubset
メソッドのパフォーマンス改善が行われています。具体的には、HTTPヘッダーの書き出し処理を高速化するための変更が加えられました。
変更されたファイルは以下の通りです。
src/pkg/net/http/header.go
:Header.WriteSubset
メソッドの主要なロジックが変更され、新しいヘルパー型や関数が導入されました。src/pkg/net/http/header_test.go
:Header.WriteSubset
の新しいテストケースとベンチマークが追加され、パフォーマンスとメモリ割り当てのテストが行われています。src/pkg/net/textproto/textproto.go
: ヘッダー値のトリミングに使用される新しい文字列操作関数TrimString
、TrimBytes
、およびisASCIISpace
が追加されました。
コミット
commit 5e75337c4e6c67090c0e516408077a284861323b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Jun 25 08:54:36 2012 -0700
net/http: speed up Header.WriteSubset
A few performance improvements, but without the stack sorting
change to avoid allocating, which is instead waiting on better
escape analysis.
R=rsc
CC=golang-dev
https://golang.org/cl/6265047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5e75337c4e6c67090c0e516408077a284861323b
元コミット内容
このコミットは、Go言語の net/http
パッケージにおける Header.WriteSubset
関数の速度向上を目的としています。コミットメッセージには「いくつかのパフォーマンス改善」とあり、特に「スタックソートの変更によるアロケーション回避」は、より良いエスケープ解析の実現を待っているため、このコミットには含まれていないことが明記されています。これは、将来的なさらなる最適化の可能性を示唆しています。
Header.WriteSubset
は、HTTPヘッダーの一部をワイヤーフォーマット(ネットワーク経由で送信される形式)で書き出すために使用される関数です。この関数の効率化は、HTTPリクエストやレスポンスの処理性能に直接影響するため、ウェブアプリケーションやネットワークサービス全体のパフォーマンス向上に寄与します。
変更の背景
HTTPヘッダーの書き出しは、ウェブサーバーやクライアントにおいて頻繁に行われる操作です。そのため、この処理のわずかな非効率性も、大量のリクエストを処理する際には大きなボトルネックとなり得ます。このコミットの背景には、Header.WriteSubset
の既存の実装が、特にヘッダーのキーをソートする際に、不要なメモリ割り当て(アロケーション)や非効率な文字列操作を行っていたという問題がありました。
コミットメッセージにある「stack sorting change to avoid allocating, which is instead waiting on better escape analysis」という記述は、Go言語のコンパイラ最適化の一つであるエスケープ解析(Escape Analysis)が深く関わっていることを示唆しています。エスケープ解析は、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定するプロセスです。スタックへの割り当てはヒープへの割り当てよりも高速であり、ガベージコレクションの負担も軽減されるため、パフォーマンス向上に繋がります。
このコミットの時点では、ヘッダーキーのソート処理において、一時的なデータ構造がヒープに割り当てられてしまうことを完全に回避することが難しかったようです。これは、当時のエスケープ解析の能力に限界があったためと考えられます。そのため、このコミットでは、エスケープ解析の改善を待たずに実現できる範囲でのパフォーマンス改善に焦点を当てています。具体的には、io.WriteString
の効率化や、より効率的な文字列トリミング関数の導入などが行われました。
関連するGoのIssue 3761(net/http: Header.WriteSubset should avoid allocating
)もこの背景を裏付けており、Header.WriteSubset
におけるメモリ割り当ての削減が長期的な目標であったことがわかります。
前提知識の解説
Goの net/http
パッケージとHTTPヘッダーの構造
net/http
パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための主要なパッケージです。HTTPヘッダーは、HTTPメッセージ(リクエストまたはレスポンス)のメタデータを含む部分であり、キーと値のペアの集合として表現されます。Goの net/http
パッケージでは、Header
型(map[string][]string
のエイリアス)がこれに対応します。キーはヘッダー名(例: "Content-Type")、値は文字列のスライス(例: {"text/plain", "charset=utf-8"}
)です。
io.Writer
と io.WriteString
の基本的な使い方
io.Writer
は、Go言語におけるバイトストリームへの書き込み操作を抽象化するインターフェースです。Write([]byte) (n int, err error)
メソッドを持ちます。
io.WriteString
は、io.Writer
インターフェースを実装するオブジェクトに文字列を書き込むためのヘルパー関数です。内部的には、文字列をバイトスライスに変換して Write
メソッドを呼び出します。しかし、io.Writer
が WriteString
メソッドも実装している場合(io.StringWriter
インターフェース)、io.WriteString
はその WriteString
メソッドを直接呼び出すことで、文字列からバイトスライスへの変換オーバーヘッドを回避し、効率的な書き込みを行うことができます。
Goにおけるインターフェースと型アサーション
Goのインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。
型アサーションは、インターフェース値が特定の具象型または別のインターフェース型であるかどうかをチェックし、その型に変換する操作です。例えば、w.(writeStringer)
は、w
が writeStringer
インターフェースを満たすかどうかをチェックし、満たす場合はそのインターフェース型として ws
に代入します。これにより、特定の最適化されたメソッド(この場合は WriteString
)を呼び出すことが可能になります。
sort.Interface
とカスタムソート
Goの sort
パッケージは、スライスをソートするための汎用的なインターフェース sort.Interface
を提供します。このインターフェースは以下の3つのメソッドを定義します。
Len() int
: スライスの要素数を返します。Swap(i, j int)
: スライスのi
番目とj
番目の要素を入れ替えます。Less(i, j int) bool
:i
番目の要素がj
番目の要素よりも小さい場合にtrue
を返します。 カスタムソートを行うには、ソートしたいデータ型がsort.Interface
を実装し、sort.Sort
関数にその型の値を渡します。
Goのメモリ割り当てとエスケープ解析 (Escape Analysis) の基本
Goプログラムでは、変数はスタックまたはヒープに割り当てられます。
- スタック: 関数呼び出しごとに確保され、関数が終了すると自動的に解放されるメモリ領域です。高速で、ガベージコレクションの対象外です。
- ヒープ: プログラム実行中に動的に確保されるメモリ領域です。ガベージコレクションによって管理され、スタックよりもアクセスが遅く、ガベージコレクションのオーバーヘッドが発生します。
エスケープ解析は、Goコンパイラが行う最適化の一つで、変数が関数のスコープを「エスケープ」して、その関数が終了した後も参照され続ける可能性があるかどうかを判断します。もし変数がエスケープしないと判断されれば、その変数はスタックに割り当てられます。エスケープすると判断されれば、ヒープに割り当てられます。エスケープ解析は、不要なヒープ割り当てを減らし、ガベージコレクションの頻度を下げ、プログラムのパフォーマンスを向上させる上で非常に重要です。
strings.TrimSpace
とカスタムトリム関数の違い
strings.TrimSpace
は、Unicodeの定義するすべての空白文字(スペース、タブ、改行、CRなど)を文字列の先頭と末尾から削除する関数です。
このコミットで導入された textproto.TrimString
は、ASCIIの空白文字(スペース、タブ、改行、CR)のみを対象としています。HTTPヘッダーの値は通常ASCII文字で構成されるため、より限定的な範囲で動作するカスタム関数を導入することで、不要なUnicode処理のオーバーヘッドを避け、パフォーマンスを向上させる狙いがあります。
技術的詳細
このコミットにおける Header.WriteSubset
のパフォーマンス改善は、主に以下の3つの技術的アプローチによって実現されています。
-
io.WriteString
の最適化とwriteStringer
インターフェースの導入:- 従来の
Header.WriteSubset
では、io.WriteString(w, s)
をループ内で直接呼び出していました。io.WriteString
は、内部でw
がio.StringWriter
インターフェース(WriteString
メソッドを持つインターフェース)を実装しているかどうかをチェックし、実装していればそのメソッドを呼び出し、そうでなければ文字列をバイトスライスに変換してWrite
メソッドを呼び出します。 - このコミットでは、
writeStringer
という新しいインターフェース(WriteString(string) (int, error)
メソッドを持つ)と、それを実装するstringWriter
という構造体が導入されました。 Header.WriteSubset
の冒頭で、渡されたio.Writer
がwriteStringer
インターフェースを実装しているかを型アサーションw.(writeStringer)
でチェックします。もし実装していれば、そのwriteStringer
インターフェースとしてws
に代入し、以降の文字列書き込みにはws.WriteString(s)
を直接使用します。- これにより、ループ内で毎回
io.WriteString
が行う型チェックと変換のオーバーヘッドを削減し、特にbytes.Buffer
のようにWriteString
を効率的に実装している型に対して、より直接的なパスを提供することでパフォーマンスを向上させています。
- 従来の
-
ヘッダーキーのソートロジックの改善とメモリ割り当ての削減:
- HTTPヘッダーは、仕様上、キーの順序が保証されません。しかし、テストの安定性や特定のプロキシ/キャッシュの挙動を考慮して、ヘッダーをソートして書き出すことが一般的です。
- 以前の実装では、ヘッダーのキーを
make([]string, 0, len(h))
でスライスにコピーし、sort.Strings(keys)
でソートしていました。この際、キーの文字列スライスが一時的に作成され、ヒープに割り当てられる可能性がありました。 - このコミットでは、
keyValues
という構造体(キーと値のスライスを持つ)と、byKey
というsort.Interface
を実装する型が導入されました。 Header.sortedKeyValues
メソッドが追加され、Header
マップからkeyValues
のスライスを作成し、それをsort.Sort(byKey(kvs))
でソートして返します。- この変更により、ソート処理がより構造化され、将来的なエスケープ解析の改善によって、
keyValues
スライスがスタックに割り当てられる可能性が高まることを期待しています。コミットメッセージにある「without the stack sorting change to avoid allocating, which is instead waiting on better escape analysis」は、この部分のさらなる最適化がエスケープ解析の進歩に依存していることを示しています。
-
カスタム文字列トリミング関数
textproto.TrimString
の導入:- 従来の
Header.WriteSubset
では、ヘッダー値のトリミングにstrings.TrimSpace(v)
を使用していました。 - このコミットでは、
src/pkg/net/textproto/textproto.go
にTrimString
、TrimBytes
、isASCIISpace
という新しい関数が追加され、Header.WriteSubset
ではtextproto.TrimString(v)
が使用されるようになりました。 textproto.TrimString
は、strings.TrimSpace
と異なり、ASCIIの空白文字(スペース、タブ、改行、CR)のみを対象としています。HTTPヘッダーの値は通常ASCII文字で構成されるため、Unicodeの空白文字をすべて考慮する必要がなく、より高速なトリミング処理が期待できます。これは、不要な複雑なUnicode処理を回避し、パフォーマンスを向上させるためのマイクロ最適化です。
- 従来の
これらの変更は、HTTPヘッダーの書き出しパスにおけるCPUサイクルとメモリ割り当てを削減し、全体的なネットワークI/O性能を向上させることを目的としています。
コアとなるコードの変更箇所
src/pkg/net/http/header.go
// 新しいインターフェースと構造体の定義
type writeStringer interface {
WriteString(string) (int, error)
}
// stringWriter implements WriteString on a Writer.
type stringWriter struct {
w io.Writer
}
func (w stringWriter) WriteString(s string) (n int, err error) {
return w.w.Write([]byte(s))
}
type keyValues struct {
key string
values []string
}
type byKey []keyValues
func (s byKey) Len() int { return len(s) }
func (s byKey) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byKey) Less(i, j int) bool { return s[i].key < s[j].key }
// ヘッダーをソートしてkeyValuesスライスとして返す新しいメソッド
func (h Header) sortedKeyValues(exclude map[string]bool) []keyValues {
kvs := make([]keyValues, 0, len(h))
for k, vv := range h {
if !exclude[k] {
kvs = append(kvs, keyValues{k, vv})
}
}
sort.Sort(byKey(kvs))
return kvs
}
// WriteSubset メソッドの変更
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error {
ws, ok := w.(writeStringer) // writeStringerへの型アサーション
if !ok {
ws = stringWriter{w} // 失敗した場合、stringWriterでラップ
}
for _, kv := range h.sortedKeyValues(exclude) { // sortedKeyValuesを使用
for _, v := range kv.values {
v = headerNewlineToSpace.Replace(v)
v = textproto.TrimString(v) // textproto.TrimStringを使用
for _, s := range []string{kv.key, ": ", v, "\\r\\n"} {
if _, err := ws.WriteString(s); err != nil { // ws.WriteStringを使用
return err
}
}
}
}
return nil
}
src/pkg/net/http/header_test.go
// 新しいテストケースの追加
var headerWriteTests = []struct {
// ... 既存のテストケース ...
// Tests header sorting when over the insertion sort threshold side:
{
Header{
"k1": {"1a", "1b"},
"k2": {"2a", "2b"},
"k3": {"3a", "3b"},
"k4": {"4a", "4b"},
"k5": {"5a", "5b"},
"k6": {"6a", "6b"},
"k7": {"7a", "7b"},
"k8": {"8a", "8b"},
"k9": {"9a", "9b"},
},
map[string]bool{"k5": true},
"k1: 1a\\r\\nk1: 1b\\r\\nk2: 2a\\r\\nk2: 2b\\r\\nk3: 3a\\r\\nk3: 3b\\r\\n" +
"k4: 4a\\r\\nk4: 4b\\r\\nk6: 6a\\r\\nk6: 6b\\r\\n" +
"k7: 7a\\r\\nk7: 7b\\r\\nk8: 8a\\r\\nk8: 8b\\r\\nk9: 9a\\r\\nk9: 9b\\r\\n",
},
}
// ベンチマークとメモリ割り当てテストの追加
func BenchmarkHeaderWriteSubset(b *testing.B) {
doHeaderWriteSubset(b.N, b)
}
func TestHeaderWriteSubsetMallocs(t *testing.T) {
doHeaderWriteSubset(100, t)
}
type errorfer interface {
Errorf(string, ...interface{})
}
func doHeaderWriteSubset(n int, t errorfer) {
h := Header(map[string][]string{
"Content-Length": {"123"},
"Content-Type": {"text/plain"},
"Date": {"some date"},
"Server": {"Go http package"},
})
var buf bytes.Buffer
var m0 runtime.MemStats
runtime.ReadMemStats(&m0)
for i := 0; i < n; i++ {
buf.Reset()
h.WriteSubset(&buf, nil)
}
var m1 runtime.MemStats
runtime.ReadMemStats(&m1)
if mallocs := m1.Mallocs - m0.Mallocs; n >= 100 && mallocs >= uint64(n) {
// TODO(bradfitz,rsc): once we can sort with allocating,
// make this an error. See http://golang.org/issue/3761
// t.Errorf("did %d mallocs (>= %d iterations); should have avoided mallocs", mallocs, n)
}
}
src/pkg/net/textproto/textproto.go
// 新しい文字列トリミング関数の追加
// TrimString returns s without leading and trailing ASCII space.
func TrimString(s string) string {
for len(s) > 0 && isASCIISpace(s[0]) {
s = s[1:]
}
for len(s) > 0 && isASCIISpace(s[len(s)-1]) {
s = s[:len(s)-1]
}
return s
}
// TrimBytes returns b without leading and trailing ASCII space.
func TrimBytes(b []byte) []byte {
for len(b) > 0 && isASCIISpace(b[0]) {
b = b[1:]
}
for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
b = b[:len(b)-1]
}
return b
}
func isASCIISpace(b byte) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}
コアとなるコードの解説
src/pkg/net/http/header.go
の変更点
writeStringer
インターフェースとstringWriter
構造体:writeStringer
は、WriteString(string) (int, error)
メソッドを持つシンプルなインターフェースです。これは、io.StringWriter
と同じシグネチャですが、io
パッケージに依存しない形で定義されています。stringWriter
は、任意のio.Writer
をラップし、そのWrite
メソッドを使ってWriteString
メソッドを実装します。これにより、io.Writer
しか持たないオブジェクトでもwriteStringer
インターフェースを満たすことができるようになります。
keyValues
構造体とbyKey
型:keyValues
は、HTTPヘッダーのキーとそれに対応する値のスライスを保持する構造体です。byKey
は[]keyValues
のエイリアスで、sort.Interface
インターフェース(Len
,Swap
,Less
メソッド)を実装しています。Less
メソッドはkeyValues
のkey
フィールドに基づいて文字列比較を行い、ヘッダーキーをアルファベット順にソートできるようにします。
Header.sortedKeyValues
メソッド:- この新しいメソッドは、
Header
マップを受け取り、exclude
マップで指定されたキーを除外した上で、残りのヘッダーをkeyValues
のスライスとして抽出し、byKey
を使ってキーでソートします。これにより、WriteSubset
が常にソートされた順序でヘッダーを書き出すことが保証されます。
- この新しいメソッドは、
Header.WriteSubset
メソッドの変更:ws, ok := w.(writeStringer)
: 渡されたio.Writer
w
がwriteStringer
インターフェースを直接実装しているか(例:bytes.Buffer
)をチェックします。if !ok { ws = stringWriter{w} }
: もしw
が直接writeStringer
を実装していなければ、stringWriter
でw
をラップし、ws
をwriteStringer
として扱えるようにします。これにより、以降の書き込みでWriteString
メソッドを直接呼び出すことが可能になり、io.WriteString
の内部的な型チェックとバイトスライス変換のオーバーヘッドを削減します。for _, kv := range h.sortedKeyValues(exclude)
: ヘッダーのイテレーションに、新しく導入されたsortedKeyValues
メソッドを使用します。これにより、ヘッダーが常にソートされた順序で処理されます。v = textproto.TrimString(v)
: ヘッダー値のトリミングに、strings.TrimSpace
の代わりにtextproto.TrimString
を使用します。これは、ASCIIスペースのみを対象とすることで、より高速な処理を実現します。if _, err := ws.WriteString(s); err != nil
: ヘッダーの各部分(キー、コロン、値、改行)を書き出す際に、io.WriteString
の代わりに、最適化されたws.WriteString
を使用します。
src/pkg/net/http/header_test.go
の変更点
- 新しいテストケース:
headerWriteTests
に、多数のヘッダーキーを持つ場合のソート順を検証する新しいテストケースが追加されました。これは、sortedKeyValues
メソッドの導入によってヘッダーのソートが正しく行われることを確認するためです。 - ベンチマークとメモリ割り当てテスト:
BenchmarkHeaderWriteSubset
は、Header.WriteSubset
の実行時間を測定するためのベンチマークです。TestHeaderWriteSubsetMallocs
は、Header.WriteSubset
の実行中に発生するメモリ割り当ての数をテストするためのものです。doHeaderWriteSubset
ヘルパー関数は、ベンチマークとメモリ割り当てテストの両方で共通のロジックを提供します。この関数内でruntime.ReadMemStats
を使用して、処理前後のメモリ統計を比較し、Mallocs
(割り当てられたオブジェクトの総数)の差分を計算しています。TODO
コメント:// TODO(bradfitz,rsc): once we can sort with allocating, // make this an error. See http://golang.org/issue/3761
というコメントは、将来的にエスケープ解析が改善され、ソート処理におけるヒープ割り当てを完全に回避できるようになれば、このテストをエラーとして扱うべきであるという意図を示しています。これは、このコミットが完全な最適化ではなく、当時のエスケープ解析の限界内で可能な最善の改善であったことを裏付けています。
src/pkg/net/textproto/textproto.go
の変更点
TrimString
関数: 文字列の先頭と末尾からASCIIの空白文字(スペース、タブ、改行、CR)を削除する関数です。strings.TrimSpace
と異なり、Unicodeの空白文字を考慮しないため、HTTPヘッダーのようなASCIIベースのデータに対してはより効率的です。TrimBytes
関数:TrimString
と同様の機能を持つが、バイトスライスを操作する関数です。isASCIISpace
関数: 与えられたバイトがASCIIの空白文字であるかを判定するヘルパー関数です。
これらの変更は、net/http
パッケージのパフォーマンスと効率性を向上させるための、細部にわたる慎重な最適化の例と言えます。
関連リンク
- Go CL (Code Review): https://golang.org/cl/6265047
- Go Issue 3761:
net/http: Header.WriteSubset should avoid allocating
(このコミットの背景にあるメモリ割り当て削減の目標に関するIssue)
参考にした情報源リンク
- Go言語の公式ドキュメント:
net/http
パッケージ,io
パッケージ,sort
パッケージ - Go言語のエスケープ解析に関する記事 (例: "Go Escape Analysis" で検索すると多数の記事が見つかります)
- HTTP/1.1 RFC 2616 (HTTPヘッダーの仕様に関する情報)