[インデックス 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ランタイムは以下の処理を行います。
result
の現在の内容と追加される文字列を格納するのに十分な大きさの新しいメモリ領域を割り当てる。result
の現在の内容を新しいメモリ領域にコピーする。- 追加される文字列を新しいメモリ領域にコピーする。
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
の変更
-
result
変数の削除とbytes.Buffer
の導入:- result := ""
の行が削除され、代わりに+ var buf bytes.Buffer
が追加されています。これにより、文字列結合のたびに新しい文字列を生成するのではなく、bytes.Buffer
の内部バッファを利用するようになります。- コメント
- // TODO: Rewrite to use bytes.Buffer
が削除されており、この変更が以前からの計画であったことが示唆されます。
-
文字列結合の置き換え:
result += u.Scheme + ":"
のような形式の文字列結合が、buf.WriteString(u.Scheme)
とbuf.WriteByte(':')
に置き換えられています。WriteString()
は文字列全体をバッファに書き込むために使用されます。WriteByte()
は単一の文字(この場合は:
や@
、?
、#
)を書き込むために使用されます。単一のバイトを書き込む場合、WriteByte
はWriteString
よりもわずかに効率的である可能性があります。
- URLの各コンポーネント(
Opaque
,User
,Host
,Path
,RawQuery
,Fragment
)の処理において、同様の置き換えが行われています。
-
最終的な文字列の取得:
- return result
の代わりに+ return buf.String()
が使用されています。bytes.Buffer
のString()
メソッドは、バッファに蓄積されたバイト列を最終的な文字列として返します。この操作は一度だけ行われるため、効率的です。
src/pkg/net/url/url_test.go
の変更
BenchmarkString
関数の追加:- この関数は、
(*URL).String()
メソッドのパフォーマンスを測定するために追加されました。 b.StopTimer()
とb.StartTimer()
を使用して、ベンチマーク対象のコード(u.String()
の呼び出し)のみが測定されるようにしています。b.ReportAllocs()
は、ベンチマーク実行中に発生したメモリ割り当ての回数とバイト数を報告するように設定します。これにより、最適化の効果を数値で確認できます。urltests
という既存のテストデータセットをループし、各URLに対してParse
を行ってからString()
メソッドを呼び出すことで、実際の使用シナリオに近い形でベンチマークを行っています。if w := tt.roundtrip; g != w
の部分は、String()
メソッドが正しくURLを再構築しているか(つまり、機能的な回帰がないか)を確認するためのアサーションです。
- この関数は、
このベンチマークの追加は、パフォーマンス改善のコミットにおいて非常に重要です。なぜなら、変更が実際にパフォーマンスを向上させたことを客観的に証明し、将来的な変更がパフォーマンスを劣化させないための回帰テストとしても機能するからです。
関連リンク
- Go CL 7182050: https://golang.org/cl/7182050
参考にした情報源リンク
- Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語の
bytes.Buffer
の使用例とパフォーマンスに関する議論:- https://go.dev/blog/strings (Go Blog: Strings, bytes, runes and characters in Go)
- https://yourbasic.org/golang/string-builder-performance/ (Go: String builder performance)
- RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax: https://datatracker.ietf.org/doc/html/rfc3986
- Go言語のベンチマークに関するドキュメント: https://go.dev/doc/articles/go_benchmarking.html```markdown
[インデックス 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ランタイムは以下の処理を行います。
result
の現在の内容と追加される文字列を格納するのに十分な大きさの新しいメモリ領域を割り当てる。result
の現在の内容を新しいメモリ領域にコピーする。- 追加される文字列を新しいメモリ領域にコピーする。
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
の変更
-
result
変数の削除とbytes.Buffer
の導入:- result := ""
の行が削除され、代わりに+ var buf bytes.Buffer
が追加されています。これにより、文字列結合のたびに新しい文字列を生成するのではなく、bytes.Buffer
の内部バッファを利用するようになります。- コメント
- // TODO: Rewrite to use bytes.Buffer
が削除されており、この変更が以前からの計画であったことが示唆されます。
-
文字列結合の置き換え:
result += u.Scheme + ":"
のような形式の文字列結合が、buf.WriteString(u.Scheme)
とbuf.WriteByte(':')
に置き換えられています。WriteString()
は文字列全体をバッファに書き込むために使用されます。WriteByte()
は単一の文字(この場合は:
や@
、?
、#
)を書き込むために使用されます。単一のバイトを書き込む場合、WriteByte
はWriteString
よりもわずかに効率的である可能性があります。
- URLの各コンポーネント(
Opaque
,User
,Host
,Path
,RawQuery
,Fragment
)の処理において、同様の置き換えが行われています。
-
最終的な文字列の取得:
- return result
の代わりに+ return buf.String()
が使用されています。bytes.Buffer
のString()
メソッドは、バッファに蓄積されたバイト列を最終的な文字列として返します。この操作は一度だけ行われるため、効率的です。
src/pkg/net/url/url_test.go
の変更
BenchmarkString
関数の追加:- この関数は、
(*URL).String()
メソッドのパフォーマンスを測定するために追加されました。 b.StopTimer()
とb.StartTimer()
を使用して、ベンチマーク対象のコード(u.String()
の呼び出し)のみが測定されるようにしています。b.ReportAllocs()
は、ベンチマーク実行中に発生したメモリ割り当ての回数とバイト数を報告するように設定します。これにより、最適化の効果を数値で確認できます。urltests
という既存のテストデータセットをループし、各URLに対してParse
を行ってからString()
メソッドを呼び出すことで、実際の使用シナリオに近い形でベンチマークを行っています。if w := tt.roundtrip; g != w
の部分は、String()
メソッドが正しくURLを再構築しているか(つまり、機能的な回帰がないか)を確認するためのアサーションです。
- この関数は、
このベンチマークの追加は、パフォーマンス改善のコミットにおいて非常に重要です。なぜなら、変更が実際にパフォーマンスを向上させたことを客観的に証明し、将来的な変更がパフォーマンスを劣化させないための回帰テストとしても機能するからです。
関連リンク
- Go CL 7182050: https://golang.org/cl/7182050
参考にした情報源リンク
- Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語の
bytes.Buffer
の使用例とパフォーマンスに関する議論:- https://go.dev/blog/strings (Go Blog: Strings, bytes, runes and characters in Go)
- https://yourbasic.org/golang/string-builder-performance/ (Go: String builder performance)
- RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax: https://datatracker.ietf.org/doc/html/rfc3986
- Go言語のベンチマークに関するドキュメント: https://go.dev/doc/articles/go_benchmarking.html