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

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

このコミットは、Go言語の標準ライブラリである net/url パッケージ内の URL 型の String() メソッドに対するパフォーマンス改善を目的としています。具体的には、URLオブジェクトを文字列として再構築する際の内部処理を最適化し、メモリ割り当て(allocations)と実行時間を削減しています。変更は主に src/pkg/net/url/url.go にある (*URL).String() メソッドの実装と、そのパフォーマンスを測定するためのベンチマークテスト src/pkg/net/url/url_test.go に追加されています。

コミット

  • コミットハッシュ: da82dfaccd6608a8e6d6b1a2633977dfa1e41c73
  • Author: Andrew Gerrand adg@golang.org
  • Date: Wed Jan 23 12:17:11 2013 +1100

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

https://github.com/golang/go/commit/da82dfaccd6608a8e6d6b1a2633977dfa1e41c73

元コミット内容

    net/url: use bytes.Buffer in (*URL).String
    
    BenchmarkString before:
    
            11990 ns/op            1621 B/op         73 allocs/op
    
    Using bytes.Buffer:
    
            8774 ns/op            1994 B/op         40 allocs/op
    
    I also tried making a version of escape() that writes directly to the
    bytes.Buffer, but it only saved 1 alloc/op and increased CPU time by
    about 10%. Didn't seem worth the extra code path.
    
    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/7182050

変更の背景

このコミットの主な背景は、Go言語における文字列操作のパフォーマンス最適化です。特に、複数の小さな文字列を結合して大きな文字列を構築する際に発生する非効率性を解消することが目的でした。

元の (*URL).String() メソッドでは、result := "" で空の文字列を初期化し、その後 result += ... の形式で繰り返し文字列を結合していました。Go言語において、文字列はイミュータブル(不変)な型です。そのため、result += "..." のような操作が行われるたびに、新しい文字列がメモリ上に割り当てられ、古い文字列の内容と新しい文字列の内容がコピーされます。このプロセスは、特にループ内で頻繁に行われる場合、大量のメモリ割り当て(allocations)とCPU時間の消費を引き起こし、パフォーマンスのボトルネックとなります。

コミットメッセージに示されているベンチマーク結果は、この問題点を明確に示しています。

  • 変更前: 11990 ns/op (操作あたりのナノ秒), 1621 B/op (操作あたりのバイト割り当て), 73 allocs/op (操作あたりのメモリ割り当て回数)
  • 変更後 (bytes.Buffer 使用): 8774 ns/op, 1994 B/op, 40 allocs/op

この結果から、bytes.Buffer を使用することで、操作あたりの実行時間が約27%短縮され、メモリ割り当て回数が約45%削減されていることがわかります。バイト割り当ては増加していますが、これは bytes.Buffer が事前にバッファを確保するためであり、全体的なパフォーマンス改善に寄与しています。

この最適化は、net/url パッケージがWebアプリケーションやネットワーク通信において頻繁に使用されることを考えると、Goアプリケーション全体のパフォーマンス向上に大きく貢献します。

前提知識の解説

Go言語における文字列とパフォーマンス

Go言語の文字列はバイトの不変シーケンスです。これは、一度作成された文字列の内容を変更できないことを意味します。したがって、s = s + "new" のような文字列結合操作は、実際には新しい文字列をメモリに割り当て、既存の s の内容と "new" の内容をその新しいメモリ領域にコピーする処理を伴います。この操作が繰り返されると、ガベージコレクションの負荷が増大し、アプリケーションのパフォーマンスが低下する可能性があります。

bytes.Buffer

bytes.Buffer は、bytes パッケージで提供される可変長のバイトバッファです。これは、文字列を効率的に構築するためのGo言語における標準的な方法です。bytes.Buffer は内部的にバイトスライスを保持し、Write メソッドや WriteString メソッドが呼び出されるたびに、必要に応じて内部バッファを拡張します。これにより、文字列結合のたびに新しいメモリを割り当てるのではなく、既存のバッファを再利用または効率的に拡張するため、メモリ割り当ての回数を大幅に削減できます。最終的に String() メソッドを呼び出すことで、バッファの内容を単一の文字列として取得できます。

net/url パッケージと URL.String() メソッド

net/url パッケージは、URLの解析、構築、エンコード、デコードを行うための機能を提供します。URL 型は、URLの各コンポーネント(スキーム、ユーザー情報、ホスト、パス、クエリ、フラグメントなど)を構造体として表現します。

(*URL).String() メソッドは、URL 構造体の内容をRFC 3986で定義されている標準的なURL文字列形式に再構築する役割を担います。このメソッドは、URLオブジェクトをログ出力したり、HTTPリクエストの構築に使用したりするなど、様々な場面で利用されます。そのため、このメソッドのパフォーマンスは、ネットワーク関連のGoアプリケーションの全体的な応答性に直接影響します。

技術的詳細

このコミットの技術的な核心は、(*URL).String() メソッド内で文字列を構築する際に、従来の string 型の結合 (+=) から bytes.Buffer の利用へと切り替えた点にあります。

変更前のアプローチ(非効率な文字列結合)

変更前のコードでは、以下のようなパターンで文字列が構築されていました。

func (u *URL) String() string {
	result := ""
	if u.Scheme != "" {
		result += u.Scheme + ":"
	}
	// ... 他のコンポーネントも同様に result += ...
	return result
}

このアプローチでは、result += ... が実行されるたびに、Goランタイムは以下の処理を行います。

  1. result の現在の内容と追加される文字列を格納するのに十分な大きさの新しいメモリ領域を割り当てる。
  2. result の現在の内容を新しいメモリ領域にコピーする。
  3. 追加される文字列を新しいメモリ領域にコピーする。
  4. result 変数が新しいメモリ領域を指すように更新する。

URLのコンポーネントの数が多い場合や、このメソッドが頻繁に呼び出される場合、これらのメモリ割り当てとコピー操作が累積的に大きなオーバーヘッドとなります。コミットメッセージのベンチマーク結果が示すように、これが 73 allocs/op という高いメモリ割り当て回数と、それに伴うCPU時間の消費の原因でした。

変更後のアプローチ(bytes.Buffer の利用)

変更後のコードでは、bytes.Buffer を使用して文字列を構築します。

func (u *URL) String() string {
	var buf bytes.Buffer // bytes.Buffer のインスタンスを作成
	if u.Scheme != "" {
		buf.WriteString(u.Scheme) // 文字列をバッファに書き込む
		buf.WriteByte(':')        // 1バイト文字をバッファに書き込む
	}
	// ... 他のコンポーネントも buf.WriteString や buf.WriteByte を使用
	return buf.String() // バッファの内容を最終的な文字列として取得
}

bytes.Buffer は、内部的にバイトスライスを管理し、書き込み操作が行われる際に必要に応じてそのスライスの容量を拡張します。この拡張は、通常、現在の容量の2倍など、効率的なアルゴリズムで行われるため、頻繁な再割り当てとコピーを避けることができます。

  • WriteString(s string): 文字列 s のバイトをバッファに書き込みます。
  • WriteByte(c byte): 単一のバイト c をバッファに書き込みます。

この変更により、メモリ割り当ての回数が 73 allocs/op から 40 allocs/op へと大幅に削減されました。これは、bytes.Buffer が内部バッファを効率的に管理し、中間的な文字列オブジェクトの生成を抑制するためです。実行時間も 11990 ns/op から 8774 ns/op へと改善されています。

バイト割り当ての増加について

ベンチマーク結果では、バイト割り当てが 1621 B/op から 1994 B/op へと増加しています。これは、bytes.Buffer が効率的な拡張のために、実際に必要とされるよりも少し大きめの内部バッファを事前に確保する傾向があるためと考えられます。しかし、このわずかなバイト割り当ての増加は、割り当て回数の大幅な削減とCPU時間の短縮によって相殺され、全体としてパフォーマンスが向上しています。

escape() 関数の最適化に関する考察

コミットメッセージには、escape() 関数(URLエンコードを行う関数)を bytes.Buffer に直接書き込むように変更することも試みたが、採用しなかった旨が記載されています。その理由は、「1 alloc/op しか削減できず、CPU時間が約10%増加した」ためです。これは、最適化を行う際には、単一のメトリック(この場合はメモリ割り当て)だけでなく、複数のメトリック(CPU時間、コードの複雑さ)を考慮し、全体として最もバランスの取れた改善を選択することの重要性を示しています。このケースでは、追加のコードパスを導入するほどの価値がないと判断されました。

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

src/pkg/net/url/url.go(*URL).String() メソッドが変更されています。

--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -434,32 +434,35 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) {
 
 // String reassembles the URL into a valid URL string.
 func (u *URL) String() string {
 -	// TODO: Rewrite to use bytes.Buffer
 -	result := ""
 +	var buf bytes.Buffer
  	if u.Scheme != "" {
 -		result += u.Scheme + ":"
 +		buf.WriteString(u.Scheme)
 +		buf.WriteByte(':')
  	}
  	if u.Opaque != "" {
 -		result += u.Opaque
 +		buf.WriteString(u.Opaque)
  	} else {
  		if u.Scheme != "" || u.Host != "" || u.User != nil {
 -			result += "//"
 +			buf.WriteString("//")
  			if u := u.User; u != nil {
 -				result += u.String() + "@"
 +				buf.WriteString(u.String())
 +				buf.WriteByte('@')
  			}
  			if h := u.Host; h != "" {
 -				result += u.Host
 +				buf.WriteString(h)
  			}
  		}
 -		result += escape(u.Path, encodePath)
 +		buf.WriteString(escape(u.Path, encodePath))
  	}
  	if u.RawQuery != "" {
 -		result += "?" + u.RawQuery
 +		buf.WriteByte('?')
 +		buf.WriteString(u.RawQuery)
  	}
  	if u.Fragment != "" {
 -		result += "#" + escape(u.Fragment, encodeFragment)
 +		buf.WriteByte('#')
 +		buf.WriteString(escape(u.Fragment, encodeFragment))
  	}
 -	return result
 +	return buf.String()
  }
 
 // Values maps a string key to a list of values.

また、src/pkg/net/url/url_test.go には、BenchmarkString という新しいベンチマーク関数が追加されています。

--- a/src/pkg/net/url/url_test.go
+++ b/src/pkg/net/url/url_test.go
@@ -280,6 +280,30 @@ func DoTest(t *testing.T, parse func(string) (*URL, error), name string, tests [\
 	}
 }
 
+func BenchmarkString(b *testing.B) {
+	b.StopTimer()
+	b.ReportAllocs()
+	for _, tt := range urltests {
+		u, err := Parse(tt.in)
+		if err != nil {
+			b.Errorf("Parse(%q) returned error %s", tt.in, err)
+			continue
+		}
+		if tt.roundtrip == "" {
+			continue
+		}
+		b.StartTimer()
+		var g string
+		for i := 0; i < b.N; i++ {
+			g = u.String()
+		}
+		b.StopTimer()
+		if w := tt.roundtrip; g != w {
+			b.Errorf("Parse(%q).String() == %q, want %q", tt.in, g, w)
+		}
+	}
+}
+
 func TestParse(t *testing.T) {
 	DoTest(t, Parse, "Parse", urltests)
 }

コアとなるコードの解説

src/pkg/net/url/url.go の変更

  1. result 変数の削除と bytes.Buffer の導入:

    • - result := "" の行が削除され、代わりに + var buf bytes.Buffer が追加されています。これにより、文字列結合のたびに新しい文字列を生成するのではなく、bytes.Buffer の内部バッファを利用するようになります。
    • コメント - // TODO: Rewrite to use bytes.Buffer が削除されており、この変更が以前からの計画であったことが示唆されます。
  2. 文字列結合の置き換え:

    • result += u.Scheme + ":" のような形式の文字列結合が、buf.WriteString(u.Scheme)buf.WriteByte(':') に置き換えられています。
      • WriteString() は文字列全体をバッファに書き込むために使用されます。
      • WriteByte() は単一の文字(この場合は :@?#)を書き込むために使用されます。単一のバイトを書き込む場合、WriteByteWriteString よりもわずかに効率的である可能性があります。
    • URLの各コンポーネント(Opaque, User, Host, Path, RawQuery, Fragment)の処理において、同様の置き換えが行われています。
  3. 最終的な文字列の取得:

    • - return result の代わりに + return buf.String() が使用されています。bytes.BufferString() メソッドは、バッファに蓄積されたバイト列を最終的な文字列として返します。この操作は一度だけ行われるため、効率的です。

src/pkg/net/url/url_test.go の変更

  1. BenchmarkString 関数の追加:
    • この関数は、(*URL).String() メソッドのパフォーマンスを測定するために追加されました。
    • b.StopTimer()b.StartTimer() を使用して、ベンチマーク対象のコード(u.String() の呼び出し)のみが測定されるようにしています。
    • b.ReportAllocs() は、ベンチマーク実行中に発生したメモリ割り当ての回数とバイト数を報告するように設定します。これにより、最適化の効果を数値で確認できます。
    • urltests という既存のテストデータセットをループし、各URLに対して Parse を行ってから String() メソッドを呼び出すことで、実際の使用シナリオに近い形でベンチマークを行っています。
    • if w := tt.roundtrip; g != w の部分は、String() メソッドが正しくURLを再構築しているか(つまり、機能的な回帰がないか)を確認するためのアサーションです。

このベンチマークの追加は、パフォーマンス改善のコミットにおいて非常に重要です。なぜなら、変更が実際にパフォーマンスを向上させたことを客観的に証明し、将来的な変更がパフォーマンスを劣化させないための回帰テストとしても機能するからです。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語の標準ライブラリである net/url パッケージ内の URL 型の String() メソッドに対するパフォーマンス改善を目的としています。具体的には、URLオブジェクトを文字列として再構築する際の内部処理を最適化し、メモリ割り当て(allocations)と実行時間を削減しています。変更は主に src/pkg/net/url/url.go にある (*URL).String() メソッドの実装と、そのパフォーマンスを測定するためのベンチマークテスト src/pkg/net/url/url_test.go に追加されています。

コミット

  • コミットハッシュ: da82dfaccd6608a8e6d6b1a2633977dfa1e41c73
  • Author: Andrew Gerrand adg@golang.org
  • Date: Wed Jan 23 12:17:11 2013 +1100

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

https://github.com/golang/go/commit/da82dfaccd6608a8e6d6b1a2633977dfa1e41c73

元コミット内容

    net/url: use bytes.Buffer in (*URL).String
    
    BenchmarkString before:
    
            11990 ns/op            1621 B/op         73 allocs/op
    
    Using bytes.Buffer:
    
            8774 ns/op            1994 B/op         40 allocs/op
    
    I also tried making a version of escape() that writes directly to the
    bytes.Buffer, but it only saved 1 alloc/op and increased CPU time by
    about 10%. Didn't seem worth the extra code path.
    
    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/7182050

変更の背景

このコミットの主な背景は、Go言語における文字列操作のパフォーマンス最適化です。特に、複数の小さな文字列を結合して大きな文字列を構築する際に発生する非効率性を解消することが目的でした。

元の (*URL).String() メソッドでは、result := "" で空の文字列を初期化し、その後 result += ... の形式で繰り返し文字列を結合していました。Go言語において、文字列はイミュータブル(不変)な型です。そのため、result += "..." のような操作が行われるたびに、新しい文字列がメモリ上に割り当てられ、古い文字列の内容と新しい文字列の内容がコピーされます。このプロセスは、特にループ内で頻繁に行われる場合、大量のメモリ割り当て(allocations)とCPU時間の消費を引き起こし、パフォーマンスのボトルネックとなります。

コミットメッセージに示されているベンチマーク結果は、この問題点を明確に示しています。

  • 変更前: 11990 ns/op (操作あたりのナノ秒), 1621 B/op (操作あたりのバイト割り当て), 73 allocs/op (操作あたりのメモリ割り当て回数)
  • 変更後 (bytes.Buffer 使用): 8774 ns/op, 1994 B/op, 40 allocs/op

この結果から、bytes.Buffer を使用することで、操作あたりの実行時間が約27%短縮され、メモリ割り当て回数が約45%削減されていることがわかります。バイト割り当ては増加していますが、これは bytes.Buffer が事前にバッファを確保するためであり、全体的なパフォーマンス改善に寄与しています。

この最適化は、net/url パッケージがWebアプリケーションやネットワーク通信において頻繁に使用されることを考えると、Goアプリケーション全体のパフォーマンス向上に大きく貢献します。

前提知識の解説

Go言語における文字列とパフォーマンス

Go言語の文字列はバイトの不変シーケンスです。これは、一度作成された文字列の内容を変更できないことを意味します。したがって、s = s + "new" のような文字列結合操作は、実際には新しい文字列をメモリに割り当て、既存の s の内容と "new" の内容をその新しいメモリ領域にコピーする処理を伴います。この操作が繰り返されると、ガベージコレクションの負荷が増大し、アプリケーションのパフォーマンスが低下する可能性があります。

bytes.Buffer

bytes.Buffer は、bytes パッケージで提供される可変長のバイトバッファです。これは、文字列を効率的に構築するためのGo言語における標準的な方法です。bytes.Buffer は内部的にバイトスライスを保持し、Write メソッドや WriteString メソッドが呼び出されるたびに、必要に応じて内部バッファを拡張します。これにより、文字列結合のたびに新しいメモリを割り当てるのではなく、既存のバッファを再利用または効率的に拡張するため、メモリ割り当ての回数を大幅に削減できます。最終的に String() メソッドを呼び出すことで、バッファの内容を単一の文字列として取得できます。

net/url パッケージと URL.String() メソッド

net/url パッケージは、URLの解析、構築、エンコード、デコードを行うための機能を提供します。URL 型は、URLの各コンポーネント(スキーム、ユーザー情報、ホスト、パス、クエリ、フラグメントなど)を構造体として表現します。

(*URL).String() メソッドは、URL 構造体の内容をRFC 3986で定義されている標準的なURL文字列形式に再構築する役割を担います。このメソッドは、URLオブジェクトをログ出力したり、HTTPリクエストの構築に使用したりするなど、様々な場面で利用されます。そのため、このメソッドのパフォーマンスは、ネットワーク関連のGoアプリケーションの全体的な応答性に直接影響します。

技術的詳細

このコミットの技術的な核心は、(*URL).String() メソッド内で文字列を構築する際に、従来の string 型の結合 (+=) から bytes.Buffer の利用へと切り替えた点にあります。

変更前のアプローチ(非効率な文字列結合)

変更前のコードでは、以下のようなパターンで文字列が構築されていました。

func (u *URL) String() string {
	result := ""
	if u.Scheme != "" {
		result += u.Scheme + ":"
	}
	// ... 他のコンポーネントも同様に result += ...
	return result
}

このアプローチでは、result += ... が実行されるたびに、Goランタイムは以下の処理を行います。

  1. result の現在の内容と追加される文字列を格納するのに十分な大きさの新しいメモリ領域を割り当てる。
  2. result の現在の内容を新しいメモリ領域にコピーする。
  3. 追加される文字列を新しいメモリ領域にコピーする。
  4. result 変数が新しいメモリ領域を指すように更新する。

URLのコンポーネントの数が多い場合や、このメソッドが頻繁に呼び出される場合、これらのメモリ割り当てとコピー操作が累積的に大きなオーバーヘッドとなります。コミットメッセージのベンチマーク結果が示すように、これが 73 allocs/op という高いメモリ割り当て回数と、それに伴うCPU時間の消費の原因でした。

変更後のアプローチ(bytes.Buffer の利用)

変更後のコードでは、bytes.Buffer を使用して文字列を構築します。

func (u *URL) String() string {
	var buf bytes.Buffer // bytes.Buffer のインスタンスを作成
	if u.Scheme != "" {
		buf.WriteString(u.Scheme) // 文字列をバッファに書き込む
		buf.WriteByte(':')        // 1バイト文字をバッファに書き込む
	}
	// ... 他のコンポーネントも buf.WriteString や buf.WriteByte を使用
	return buf.String() // バッファの内容を最終的な文字列として取得
}

bytes.Buffer は、内部的にバイトスライスを管理し、書き込み操作が行われる際に必要に応じてそのスライスの容量を拡張します。この拡張は、通常、現在の容量の2倍など、効率的なアルゴリズムで行われるため、頻繁な再割り当てとコピーを避けることができます。

  • WriteString(s string): 文字列 s のバイトをバッファに書き込みます。
  • WriteByte(c byte): 単一のバイト c をバッファに書き込みます。

この変更により、メモリ割り当ての回数が 73 allocs/op から 40 allocs/op へと大幅に削減されました。これは、bytes.Buffer が内部バッファを効率的に管理し、中間的な文字列オブジェクトの生成を抑制するためです。実行時間も 11990 ns/op から 8774 ns/op へと改善されています。

バイト割り当ての増加について

ベンチマーク結果では、バイト割り当てが 1621 B/op から 1994 B/op へと増加しています。これは、bytes.Buffer が効率的な拡張のために、実際に必要とされるよりも少し大きめの内部バッファを事前に確保する傾向があるためと考えられます。しかし、このわずかなバイト割り当ての増加は、割り当て回数の大幅な削減とCPU時間の短縮によって相殺され、全体としてパフォーマンスが向上しています。

escape() 関数の最適化に関する考察

コミットメッセージには、escape() 関数(URLエンコードを行う関数)を bytes.Buffer に直接書き込むように変更することも試みたが、採用しなかった旨が記載されています。その理由は、「1 alloc/op しか削減できず、CPU時間が約10%増加した」ためです。これは、最適化を行う際には、単一のメトリック(この場合はメモリ割り当て)だけでなく、複数のメトリック(CPU時間、コードの複雑さ)を考慮し、全体として最もバランスの取れた改善を選択することの重要性を示しています。このケースでは、追加のコードパスを導入するほどの価値がないと判断されました。

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

src/pkg/net/url/url.go(*URL).String() メソッドが変更されています。

--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -434,32 +434,35 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) {
 
 // String reassembles the URL into a valid URL string.
 func (u *URL) String() string {
 -	// TODO: Rewrite to use bytes.Buffer
 -	result := ""
 +	var buf bytes.Buffer
  	if u.Scheme != "" {
 -		result += u.Scheme + ":"
 +		buf.WriteString(u.Scheme)
 +		buf.WriteByte(':')
  	}
  	if u.Opaque != "" {
 -		result += u.Opaque
 +		buf.WriteString(u.Opaque)
  	} else {
  		if u.Scheme != "" || u.Host != "" || u.User != nil {
 -			result += "//"
 +			buf.WriteString("//")
  			if u := u.User; u != nil {
 -				result += u.String() + "@"
 +				buf.WriteString(u.String())
 +				buf.WriteByte('@')
  			}
  			if h := u.Host; h != "" {
 -				result += u.Host
 +				buf.WriteString(h)
  			}
  		}
 -		result += escape(u.Path, encodePath)
 +		buf.WriteString(escape(u.Path, encodePath))
  	}
  	if u.RawQuery != "" {
 -		result += "?" + u.RawQuery
 +		buf.WriteByte('?')
 +		buf.WriteString(u.RawQuery)
  	}
  	if u.Fragment != "" {
 -		result += "#" + escape(u.Fragment, encodeFragment)
 +		buf.WriteByte('#')
 +		buf.WriteString(escape(u.Fragment, encodeFragment))
  	}
 -	return result
 +	return buf.String()
  }
 
 // Values maps a string key to a list of values.

また、src/pkg/net/url/url_test.go には、BenchmarkString という新しいベンチマーク関数が追加されています。

--- a/src/pkg/net/url/url_test.go
+++ b/src/pkg/net/url/url_test.go
@@ -280,6 +280,30 @@ func DoTest(t *testing.T, parse func(string) (*URL, error), name string, tests [\
 	}
 }
 
+func BenchmarkString(b *testing.B) {
+	b.StopTimer()
+	b.ReportAllocs()
+	for _, tt := range urltests {
+		u, err := Parse(tt.in)
+		if err != nil {
+			b.Errorf("Parse(%q) returned error %s", tt.in, err)
+			continue
+		}
+		if tt.roundtrip == "" {
+			continue
+		}
+		b.StartTimer()
+		var g string
+		for i := 0; i < b.N; i++ {
+			g = u.String()
+		}
+		b.StopTimer()
+		if w := tt.roundtrip; g != w {
+			b.Errorf("Parse(%q).String() == %q, want %q", tt.in, g, w)
+		}
+	}
+}
+
 func TestParse(t *testing.T) {
 	DoTest(t, Parse, "Parse", urltests)
 }

コアとなるコードの解説

src/pkg/net/url/url.go の変更

  1. result 変数の削除と bytes.Buffer の導入:

    • - result := "" の行が削除され、代わりに + var buf bytes.Buffer が追加されています。これにより、文字列結合のたびに新しい文字列を生成するのではなく、bytes.Buffer の内部バッファを利用するようになります。
    • コメント - // TODO: Rewrite to use bytes.Buffer が削除されており、この変更が以前からの計画であったことが示唆されます。
  2. 文字列結合の置き換え:

    • result += u.Scheme + ":" のような形式の文字列結合が、buf.WriteString(u.Scheme)buf.WriteByte(':') に置き換えられています。
      • WriteString() は文字列全体をバッファに書き込むために使用されます。
      • WriteByte() は単一の文字(この場合は :@?#)を書き込むために使用されます。単一のバイトを書き込む場合、WriteByteWriteString よりもわずかに効率的である可能性があります。
    • URLの各コンポーネント(Opaque, User, Host, Path, RawQuery, Fragment)の処理において、同様の置き換えが行われています。
  3. 最終的な文字列の取得:

    • - return result の代わりに + return buf.String() が使用されています。bytes.BufferString() メソッドは、バッファに蓄積されたバイト列を最終的な文字列として返します。この操作は一度だけ行われるため、効率的です。

src/pkg/net/url/url_test.go の変更

  1. BenchmarkString 関数の追加:
    • この関数は、(*URL).String() メソッドのパフォーマンスを測定するために追加されました。
    • b.StopTimer()b.StartTimer() を使用して、ベンチマーク対象のコード(u.String() の呼び出し)のみが測定されるようにしています。
    • b.ReportAllocs() は、ベンチマーク実行中に発生したメモリ割り当ての回数とバイト数を報告するように設定します。これにより、最適化の効果を数値で確認できます。
    • urltests という既存のテストデータセットをループし、各URLに対して Parse を行ってから String() メソッドを呼び出すことで、実際の使用シナリオに近い形でベンチマークを行っています。
    • if w := tt.roundtrip; g != w の部分は、String() メソッドが正しくURLを再構築しているか(つまり、機能的な回帰がないか)を確認するためのアサーションです。

このベンチマークの追加は、パフォーマンス改善のコミットにおいて非常に重要です。なぜなら、変更が実際にパフォーマンスを向上させたことを客観的に証明し、将来的な変更がパフォーマンスを劣化させないための回帰テストとしても機能するからです。

関連リンク

参考にした情報源リンク