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

[インデックス 19050] ファイルの概要

このコミットは、Go言語の net/textproto パッケージにおけるMIMEヘッダーの正規化とインターニング処理を最適化するものです。特に、canonicalMIMEHeaderKey 関数が、map[string]string を利用して共通ヘッダーのルックアップを効率化するように変更されました。これにより、パフォーマンスが向上し、コードが簡素化されています。

コミット

commit 8072f46abdf5b2d3ed0ee7d691823b7fcdaa7c21
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 7 10:39:24 2014 -0700

    net/textproto: simplify common header interning
    
    Takes advantage of CL 83740044, to optimize map[string] lookup
    from []byte key.
    
    Deletes code.
    
    No conditional check for gccgo, since Ian plans to add this
    to gccgo before GCC 4.10 (Go 1.3).
    
    benchmark                   old ns/op     new ns/op     delta
    BenchmarkReadMIMEHeader     6066          5086          -16.16%
    
    benchmark                   old allocs     new allocs     delta
    BenchmarkReadMIMEHeader     12             12             +0.00%
    
    benchmark                   old bytes     new bytes     delta
    BenchmarkReadMIMEHeader     1317          1317          +0.00%
    
    Update #3512
    
    LGTM=rsc
    R=rsc, dave
    CC=golang-codereviews, iant
    https://golang.org/cl/84230043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8072f46abdf5b2d3ed0ee7d691823b7fcdaa7c21

元コミット内容

このコミットは、net/textproto パッケージ内の canonicalMIMEHeaderKey 関数を修正し、共通ヘッダーのインターニング処理を簡素化します。具体的には、以前の commonHeaders というソートされた文字列スライスを用いた線形探索に近いロジックを削除し、commonHeader という map[string]string を使用したルックアップに置き換えています。この変更は、[]byte から string キーへの map ルックアップを最適化する Go コンパイラの変更 (CL 83740044) を活用しています。結果として、BenchmarkReadMIMEHeader の実行時間が約16%改善されています。

変更の背景

この変更の主な背景は、net/textproto パッケージにおけるMIMEヘッダーの処理性能を向上させることです。特に、HTTPなどのプロトコルで頻繁に登場する共通ヘッダー(例: "Content-Type", "Host", "User-Agent"など)の正規化とインターニングの効率化が求められていました。

以前の実装では、canonicalMIMEHeaderKey 関数内で、入力されたバイトスライスを正規化した後、commonHeaders というソートされた文字列スライスに対して、二分探索に似たロジックで既存の共通ヘッダーを探していました。この方法は、文字列の比較とスライス内での探索に一定のコストがかかり、特にヘッダーの数が増えるにつれて性能が劣化する可能性がありました。

このコミットは、Goコンパイラに導入された新しい最適化 (CL 83740044) を活用することで、この問題を解決します。CL 83740044は、map[string] のルックアップにおいて、キーが []byte から string に変換される際に発生する余分なメモリ割り当てを回避するものです。この最適化により、[]byte を直接 string にキャストして map のキーとして使用しても、効率的なルックアップが可能になりました。

このコンパイラ最適化の恩恵を受けることで、commonHeaders スライスでの探索ロジックを、より高速な map[string]string ルックアップに置き換えることが可能になり、コードの簡素化と性能向上が同時に実現されました。

前提知識の解説

1. net/textproto パッケージ

net/textproto はGo言語の標準ライブラリの一部で、テキストベースのネットワークプロトコル(HTTP、SMTP、NNTPなど)を解析するための低レベルな機能を提供します。このパッケージは、ヘッダーの読み取り、行の読み取り、MIMEヘッダーの正規化など、プロトコル処理における共通のタスクを効率的に行うためのユーティリティを提供します。

2. ヘッダーのインターニング (Header Interning)

インターニングとは、プログラム内で同じ値を持つオブジェクトが複数存在する場合に、それらをメモリ上で単一のインスタンスとして共有する最適化手法です。文字列のインターニングは特に一般的で、同じ文字列リテラルや動的に生成された文字列が複数回出現する場合に、それらを同じメモリ上のアドレスにマッピングすることで、メモリ使用量を削減し、文字列比較のパフォーマンスを向上させます(ポインタ比較で済むため)。

MIMEヘッダーの場合、"Content-Type"や"Host"などのヘッダー名は、多くのリクエストやレスポンスで繰り返し使用されます。これらのヘッダー名をインターニングすることで、毎回新しい文字列オブジェクトを生成したり、文字列比較を繰り返したりするオーバーヘッドを削減できます。

3. Goコンパイラの map[string] ルックアップ最適化 (CL 83740044)

Go言語において、map のキーは通常 string 型です。しかし、ネットワークから読み取られるヘッダー名などは []byte 型で提供されることがよくあります。[]bytemap[string] のキーとして使用するには、通常 string(byteSlice) のように string 型に変換する必要があります。この変換は、新しい文字列オブジェクトをメモリに割り当てるため、パフォーマンスに影響を与える可能性があります。

CL 83740044 (Go CL 83740044, "cmd/gc, runtime: optimize map[string] lookup from []byte key") は、この問題に対処するためのGoコンパイラの最適化です。この変更により、コンパイラは m[string(byteSlice)] のようなパターンを特別に認識し、byteSlice の内容を新しい文字列にコピーすることなく、直接 map のルックアップを実行できるようになりました。これにより、余分なメモリ割り当てとコピーのオーバーヘッドが削減され、[]byte から string への変換を伴う map ルックアップが大幅に高速化されます。

4. gccgo と Go 1.3

gccgo は、GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。Go言語の公式コンパイラである gc とは異なる実装ですが、Go言語の仕様に準拠しています。コミットメッセージに「No conditional check for gccgo, since Ian plans to add this to gccgo before GCC 4.10 (Go 1.3)」とあるのは、この最適化が gc コンパイラだけでなく、gccgo にもGo 1.3のリリース(またはGCC 4.10のリリース)までに組み込まれる予定であることを示しています。これは、Goエコシステム全体でこの最適化が利用可能になることを意味します。

技術的詳細

このコミットの技術的な核心は、net/textproto パッケージの canonicalMIMEHeaderKey 関数における共通ヘッダーのインターニング戦略の変更です。

変更前: 変更前は、canonicalMIMEHeaderKey 関数は、入力された []byte スライスをMIMEヘッダーの正規形式(例: "content-length" -> "Content-Length")に変換した後、commonHeaders というグローバルな []string スライスに対して、その正規化されたヘッダー名が存在するかどうかを探索していました。commonHeaders は事前にソートされており、探索は二分探索に似た効率的な方法で行われていました。もし見つかれば、commonHeaders 内の既存の文字列インスタンスを返していました。これは、メモリ割り当てを避けるためのインターニング手法でした。

しかし、この方法は以下の課題を抱えていました。

  • 探索ロジックの複雑さ: canonicalMIMEHeaderKey 関数内に、正規化と同時に commonHeaders を探索するための比較的複雑なループと条件分岐が含まれていました。
  • パフォーマンスの限界: スライス内での探索は、map ルックアップと比較して、特に要素数が多い場合にオーバーヘッドが大きくなる可能性があります。

変更後: このコミットでは、commonHeaders スライスとその探索ロジックが完全に削除され、代わりに commonHeader という map[string]string が導入されました。この map は、init 関数内で一般的なMIMEヘッダー名をキーと値の両方として事前にロードされます。

canonicalMIMEHeaderKey 関数は、入力された []byte スライスを正規化する処理はそのまま維持します。しかし、正規化が完了した後、map[string]string である commonHeader に対して、string(a) (ここで a は正規化された []byte スライス) をキーとしてルックアップを行います。

この変更が機能する鍵は、前述の Goコンパイラの map[string] ルックアップ最適化 (CL 83740044) です。この最適化により、string(a) の部分で新しい文字列が割り当てられることなく、[]byte の内容を直接利用して map のルックアップが実行されます。もし map 内に一致するエントリが見つかれば、その map に格納されている既存の文字列インスタンスが返されます。見つからなければ、string(a) によって新しい文字列が作成され、それが返されます。

パフォーマンスへの影響: コミットメッセージに示されているベンチマーク結果は、この最適化の有効性を示しています。

  • BenchmarkReadMIMEHeader:
    • old ns/op: 6066 ns/op
    • new ns/op: 5086 ns/op
    • delta: -16.16%

これは、MIMEヘッダーの読み取り処理が約16%高速化されたことを意味します。メモリ割り当て (allocs) とメモリ使用量 (bytes) は変化がないことから、この性能向上は主にCPUサイクル(ナノ秒あたりの操作数)の削減によるものであり、map ルックアップが以前の探索ロジックよりも効率的になったことを裏付けています。特に、[]byte から string への変換時の余分な割り当てが回避されているため、メモリ使用量が増加することなく性能が向上しています。

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

src/pkg/net/textproto/reader.gosrc/pkg/net/textproto/reader_test.go の2つのファイルが変更されています。

--- a/src/pkg/net/textproto/reader.go
+++ b/src/pkg/net/textproto/reader.go
@@ -562,19 +562,12 @@ const toLower = 'a' - 'A'
 // allowed to mutate the provided byte slice before returning the
 // string.
 func canonicalMIMEHeaderKey(a []byte) string {
-	// Look for it in commonHeaders , so that we can avoid an
-	// allocation by sharing the strings among all users
-	// of textproto. If we don't find it, a has been canonicalized
-	// so just return string(a).
 	upper := true
-	lo := 0
-	hi := len(commonHeaders)
-	for i := 0; i < len(a); i++ {
+	for i, c := range a {
 		// 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.
-		c := a[i]
 		if c == ' ' {
 			c = '-'
 		} else if upper && 'a' <= c && 'z' <= c {
@@ -584,60 +577,61 @@ func canonicalMIMEHeaderKey(a []byte) string {
 		}
 		a[i] = c
 		upper = c == '-' // for next time
-
-		if lo < hi {
-			for lo < hi && (len(commonHeaders[lo]) <= i || commonHeaders[lo][i] < c) {
-				lo++
-			}
-			for hi > lo && commonHeaders[hi-1][i] > c {
-				hi--
-			}
-		}
 	}
-	if lo < hi && len(commonHeaders[lo]) == len(a) {
-		return commonHeaders[lo]
+	// The compiler recognizes m[string(byteSlice)] as a special
+	// case, so a copy of a's bytes into a new string does not
+	// happen in this map lookup:
+	if v := commonHeader[string(a)]; v != "" {
+		return v
 	}
 	return string(a)
 }
  
-var commonHeaders = []string{
-	"Accept",
-	"Accept-Charset",
-	"Accept-Encoding",
-	"Accept-Language",
-	"Accept-Ranges",
-	"Cache-Control",
-	"Cc",
-	"Connection",
-	"Content-Id",
-	"Content-Language",
-	"Content-Length",
-	"Content-Transfer-Encoding",
-	"Content-Type",
-	"Cookie",
-	"Date",
-	"Dkim-Signature",
-	"Etag",
-	"Expires",
-	"From",
-	"Host",
-	"If-Modified-Since",
-	"If-None-Match",
-	"In-Reply-To",
-	"Last-Modified",
-	"Location",
-	"Message-Id",
-	"Mime-Version",
-	"Pragma",
-	"Received",
-	"Return-Path",
-	"Server",
-	"Set-Cookie",
-	"Subject",
-	"To",
-	"User-Agent",
-	"Via",
-	"X-Forwarded-For",
-	"X-Imforwards",
-	"X-Powered-By",
+// commonHeader interns common header strings.
+var commonHeader = make(map[string]string)
+
+func init() {
+	for _, v := range []string{
+		"Accept",
+		"Accept-Charset",
+		"Accept-Encoding",
+		"Accept-Language",
+		"Accept-Ranges",
+		"Cache-Control",
+		"Cc",
+		"Connection",
+		"Content-Id",
+		"Content-Language",
+		"Content-Length",
+		"Content-Transfer-Encoding",
+		"Content-Type",
+		"Cookie",
+		"Date",
+		"Dkim-Signature",
+		"Etag",
+		"Expires",
+		"From",
+		"Host",
+		"If-Modified-Since",
+		"If-None-Match",
+		"In-Reply-To",
+		"Last-Modified",
+		"Location",
+		"Message-Id",
+		"Mime-Version",
+		"Pragma",
+		"Received",
+		"Return-Path",
+		"Server",
+		"Set-Cookie",
+		"Subject",
+		"To",
+		"User-Agent",
+		"Via",
+		"X-Forwarded-For",
+		"X-Imforwards",
+		"X-Powered-By",
+	} {
+		commonHeader[v] = v
+	}
+}
diff --git a/src/pkg/net/textproto/reader_test.go b/src/pkg/net/textproto/reader_test.go
index cc12912b63..cbc0ed183e 100644
--- a/src/pkg/net/textproto/reader_test.go
+++ b/src/pkg/net/textproto/reader_test.go
@@ -247,24 +247,20 @@ func TestRFC959Lines(t *testing.T) {
 }
  
 func TestCommonHeaders(t *testing.T) {
-	// need to disable the commonHeaders-based optimization
-	// during this check, or we'd not be testing anything
-	oldch := commonHeaders
-	commonHeaders = []string{}
-	defer func() { commonHeaders = oldch }()
-
-	last := ""
-	for _, h := range oldch {
-		if last > h {
-			t.Errorf("%v is out of order", h)
-		}
-		if last == h {
-			t.Errorf("%v is duplicated", h)
+	for h := range commonHeader {
+		if h != CanonicalMIMEHeaderKey(h) {
+			t.Errorf("Non-canonical header %q in commonHeader", h)
 		}
-		if canon := CanonicalMIMEHeaderKey(h); h != canon {
-			t.Errorf("%v is not canonical", h)
+	}
+	b := []byte("content-Length")
+	want := "Content-Length"
+	n := testing.AllocsPerRun(200, func() {
+		if x := canonicalMIMEHeaderKey(b); x != want {
+			t.Fatalf("canonicalMIMEHeaderKey(%q) = %q; want %q", b, x, want)
 		}
-		last = h
+	})
+	if n > 0 {
+		t.Errorf("canonicalMIMEHeaderKey allocs = %v; want 0", n)
 	}
 }
  

コアとなるコードの解説

src/pkg/net/textproto/reader.go の変更

  1. canonicalMIMEHeaderKey 関数の変更:

    • 削除されたコード: 以前の commonHeaders スライスに対する探索ロジック(lo, hi 変数とそれに続く for ループ)が完全に削除されました。このロジックは、正規化されたヘッダー名が commonHeaders 内に存在するかどうかを効率的に見つけるためのものでした。
    • 追加されたコード: 正規化処理の後に、以下の行が追加されました。
      	// The compiler recognizes m[string(byteSlice)] as a special
      	// case, so a copy of a's bytes into a new string does not
      	// happen in this map lookup:
      	if v := commonHeader[string(a)]; v != "" {
      		return v
      	}
      
      このコードは、正規化されたバイトスライス astring(a) として commonHeader マップのキーとして使用し、ルックアップを行います。前述のコンパイラ最適化により、この string(a) 変換でメモリ割り当てが発生しないことがコメントで明記されています。もしマップ内に対応するエントリが見つかれば(v != "")、そのマップに格納されているインターニングされた文字列が返されます。見つからなければ、return string(a) によって新しい文字列が作成され、それが返されます。
  2. commonHeaders スライスの削除と commonHeader マップの導入:

    • 以前の var commonHeaders = []string{...} というソートされた文字列スライスが削除されました。
    • 代わりに、var commonHeader = make(map[string]string) という map[string]string 型の変数が導入されました。
    • init() 関数が追加され、この init() 関数内で、以前 commonHeaders に含まれていたすべての共通ヘッダー名が commonHeader マップにキーと値の両方として追加されます。これにより、マップが事前に共通ヘッダーで初期化され、高速なルックアップが可能になります。

src/pkg/net/textproto/reader_test.go の変更

  1. TestCommonHeaders 関数の変更:
    • 以前のテストコードは、commonHeaders スライスを一時的に無効化し、その内容がソートされているか、重複がないか、正規化されているかをチェックしていました。
    • 新しいテストコードは、commonHeader マップのキーがすべて正規化された形式であるかをチェックします。
    • さらに重要な変更として、canonicalMIMEHeaderKey 関数が []byte を受け取り、map ルックアップを行う際にメモリ割り当てが発生しないことを検証するベンチマークスタイルのテストが追加されました。
      	b := []byte("content-Length")
      	want := "Content-Length"
      	n := testing.AllocsPerRun(200, func() {
      		if x := canonicalMIMEHeaderKey(b); x != want {
      			t.Fatalf("canonicalMIMEHeaderKey(%q) = %q; want %q", b, x, want)
      		}
      	})
      	if n > 0 {
      		t.Errorf("canonicalMIMEHeaderKey allocs = %v; want 0", n)
      	}
      
      このテストは、"content-Length" というバイトスライスを canonicalMIMEHeaderKey に渡し、その結果が "Content-Length" であることを確認しつつ、この操作中にメモリ割り当てがゼロであることを testing.AllocsPerRun を使って検証しています。これは、CL 83740044 の最適化が正しく機能していることを確認するための重要なテストです。

これらの変更により、net/textproto パッケージは、Goコンパイラの最新の最適化を活用し、MIMEヘッダーの処理をより効率的かつ簡潔に行えるようになりました。

関連リンク

参考にした情報源リンク