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

[インデックス 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: ヘッダー値のトリミングに使用される新しい文字列操作関数 TrimStringTrimBytes、および 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.Writerio.WriteString の基本的な使い方

io.Writer は、Go言語におけるバイトストリームへの書き込み操作を抽象化するインターフェースです。Write([]byte) (n int, err error) メソッドを持ちます。 io.WriteString は、io.Writer インターフェースを実装するオブジェクトに文字列を書き込むためのヘルパー関数です。内部的には、文字列をバイトスライスに変換して Write メソッドを呼び出します。しかし、io.WriterWriteString メソッドも実装している場合(io.StringWriter インターフェース)、io.WriteString はその WriteString メソッドを直接呼び出すことで、文字列からバイトスライスへの変換オーバーヘッドを回避し、効率的な書き込みを行うことができます。

Goにおけるインターフェースと型アサーション

Goのインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。 型アサーションは、インターフェース値が特定の具象型または別のインターフェース型であるかどうかをチェックし、その型に変換する操作です。例えば、w.(writeStringer) は、wwriteStringer インターフェースを満たすかどうかをチェックし、満たす場合はそのインターフェース型として 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つの技術的アプローチによって実現されています。

  1. io.WriteString の最適化と writeStringer インターフェースの導入:

    • 従来の Header.WriteSubset では、io.WriteString(w, s) をループ内で直接呼び出していました。io.WriteString は、内部で wio.StringWriter インターフェース(WriteString メソッドを持つインターフェース)を実装しているかどうかをチェックし、実装していればそのメソッドを呼び出し、そうでなければ文字列をバイトスライスに変換して Write メソッドを呼び出します。
    • このコミットでは、writeStringer という新しいインターフェース(WriteString(string) (int, error) メソッドを持つ)と、それを実装する stringWriter という構造体が導入されました。
    • Header.WriteSubset の冒頭で、渡された io.WriterwriteStringer インターフェースを実装しているかを型アサーション w.(writeStringer) でチェックします。もし実装していれば、その writeStringer インターフェースとして ws に代入し、以降の文字列書き込みには ws.WriteString(s) を直接使用します。
    • これにより、ループ内で毎回 io.WriteString が行う型チェックと変換のオーバーヘッドを削減し、特に bytes.Buffer のように WriteString を効率的に実装している型に対して、より直接的なパスを提供することでパフォーマンスを向上させています。
  2. ヘッダーキーのソートロジックの改善とメモリ割り当ての削減:

    • 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」は、この部分のさらなる最適化がエスケープ解析の進歩に依存していることを示しています。
  3. カスタム文字列トリミング関数 textproto.TrimString の導入:

    • 従来の Header.WriteSubset では、ヘッダー値のトリミングに strings.TrimSpace(v) を使用していました。
    • このコミットでは、src/pkg/net/textproto/textproto.goTrimStringTrimBytesisASCIISpace という新しい関数が追加され、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 メソッドは keyValueskey フィールドに基づいて文字列比較を行い、ヘッダーキーをアルファベット順にソートできるようにします。
  • Header.sortedKeyValues メソッド:
    • この新しいメソッドは、Header マップを受け取り、exclude マップで指定されたキーを除外した上で、残りのヘッダーを keyValues のスライスとして抽出し、byKey を使ってキーでソートします。これにより、WriteSubset が常にソートされた順序でヘッダーを書き出すことが保証されます。
  • Header.WriteSubset メソッドの変更:
    • ws, ok := w.(writeStringer): 渡された io.Writer wwriteStringer インターフェースを直接実装しているか(例: bytes.Buffer)をチェックします。
    • if !ok { ws = stringWriter{w} }: もし w が直接 writeStringer を実装していなければ、stringWriterw をラップし、wswriteStringer として扱えるようにします。これにより、以降の書き込みで 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ヘッダーの仕様に関する情報)