[インデックス 13480] ファイルの概要
このコミットは、Go言語の標準ライブラリであるsrc/pkg/testing/testing.go
ファイルに対する変更です。testing
パッケージは、Goプログラムの自動テストをサポートするための機能を提供します。具体的には、テスト関数やベンチマーク関数の定義、テストの実行、結果のレポートなど、テストフレームワークの基盤となる機能が含まれています。このファイルは、テストのログ出力やエラー報告の際に使用される内部的なユーティリティ関数decorate
を含んでおり、その効率性を改善することがこのコミットの目的です。
コミット
testing
パッケージにおいて、多数の行を整形する際のメモリ肥大化を修正しました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dd78f745c44f426578fbb8cddcc05c8227fb0ad5
元コミット内容
commit dd78f745c44f426578fbb8cddcc05c8227fb0ad5
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Tue Jul 17 07:56:25 2012 +0200
testing: fix memory blowup when formatting many lines.
Fixes #3830.
R=golang-dev, r
CC=golang-dev, remy
https://golang.org/cl/6373047
変更の背景
この変更の背景には、Goのtesting
パッケージがテスト結果をログに出力する際に、特に多数の行を整形して出力する場合に発生していたメモリ使用量の肥大化("memory blowup")問題があります。これは、Go言語における文字列の扱いに起因する一般的なパフォーマンス問題の一つです。
元のdecorate
関数は、文字列の連結や部分文字列の抽出を繰り返すことで、ログメッセージの整形(ファイル名と行番号の付加、インデント、改行の調整)を行っていました。Go言語において文字列は不変(immutable)であるため、文字列を連結したり変更したりするたびに、新しい文字列オブジェクトがメモリ上に生成されます。短い文字列や少ない回数の操作であれば問題ありませんが、テストのログ出力のように大量のテキストを頻繁に整形するシナリオでは、この不必要な文字列オブジェクトの生成が繰り返され、結果として大量のメモリが一時的に消費され、ガベージコレクションの負荷が増大し、パフォーマンスが低下する原因となっていました。
この問題は、GoのIssue #3830として報告されており、このコミットはその解決を目的としています。
前提知識の解説
Go言語における文字列の不変性 (Immutability of strings in Go)
Go言語において、文字列(string
型)は不変な値のシーケンスです。これは、一度作成された文字列の内容は変更できないことを意味します。例えば、s := "hello"
という文字列があり、これにs += " world"
のように別の文字列を連結しようとすると、既存の"hello"
という文字列が変更されるのではなく、"hello world"
という新しい文字列がメモリ上に作成され、s
はその新しい文字列を参照するようになります。
この不変性は、文字列の安全な共有やハッシュ計算の効率化など、多くの利点をもたらしますが、頻繁な文字列連結操作を行う場合には、新しい文字列オブジェクトが繰り返し生成されるため、メモリの割り当てと解放(ガベージコレクション)のオーバーヘッドが大きくなるという欠点があります。
bytes.Buffer
の役割と利点
bytes.Buffer
は、Goの標準ライブラリbytes
パッケージで提供される型で、可変長のバイトシーケンス(バイトスライス)を効率的に操作するためのバッファです。これは、特に文字列を効率的に構築する際に非常に有用です。
bytes.Buffer
は内部的にバイトスライスを保持し、データを追加する際に必要に応じてその容量を拡張します。この際、毎回新しいメモリを割り当てるのではなく、ある程度の余裕を持ったバッファを確保し、その範囲内でデータを追加していくため、頻繁なメモリ再割り当てを避けることができます。これにより、不変な文字列の連結を繰り返すよりもはるかに少ないメモリ割り当てとガベージコレクションのオーバーヘッドで、大量の文字列データを効率的に構築することが可能になります。
Goのtesting
パッケージの基本的な役割
testing
パッケージは、Go言語でユニットテスト、ベンチマークテスト、および例(Example)を記述するためのフレームワークを提供します。開発者は、_test.go
というサフィックスを持つファイルにテストコードを記述し、go test
コマンドで実行します。このパッケージは、テストの成功/失敗の報告、ログ出力、並行テストの実行などの機能を提供し、Goアプリケーションの品質保証に不可欠な役割を果たします。
runtime.Caller
の用途
runtime.Caller
関数は、Goの標準ライブラリruntime
パッケージで提供され、現在のゴルーチン(Goの軽量スレッド)のコールスタックに関する情報を取得するために使用されます。具体的には、呼び出し元のファイル名、行番号、関数名、およびその情報が有効であるかどうかを示すブール値を返します。
testing
パッケージでは、テストのログ出力やエラー報告の際に、どのファイルと行でログが記録されたか、またはエラーが発生したかを示すためにruntime.Caller
が利用されます。これにより、開発者はテストの出力から問題の発生源を素早く特定できます。
技術的詳細
このコミットの主要な変更点は、testing.go
内のdecorate
関数の実装を、非効率な文字列連結からbytes.Buffer
を使用した効率的なバイト操作へと全面的に書き換えたことです。
旧decorate
関数の非効率性
変更前のdecorate
関数は、以下のような非効率な処理を行っていました。
addFileLine
パラメータの存在: ファイルと行番号の付加を制御するブール値パラメータaddFileLine
を持っていました。fmt.Sprintf
と文字列連結: ファイルと行番号の情報を文字列に整形するためにfmt.Sprintf
を使用し、その結果を元の文字列s
に連結していました。- 再帰的な呼び出しと文字列スライス: 複数行にわたる文字列のインデント処理において、改行文字(
\n
)を見つけるたびに、文字列をスライスし、その部分文字列に対してdecorate
関数を再帰的に呼び出していました。この再帰的な呼び出しと文字列スライスは、大量の新しい文字列オブジェクトを生成し、メモリ使用量を増大させる主要な原因でした。 len(s)
の繰り返し計算: ループ内でlen(s)
を繰り返し計算しており、これも小さなオーバーヘッドとなっていました。
これらの操作は、特に長いログメッセージや多数のログ行を処理する際に、大量のメモリ割り当てとコピーを引き起こし、パフォーマンスのボトルネックとなっていました。
新decorate
関数でのbytes.Buffer
の導入とその効果
新しいdecorate
関数は、以下の点で大幅に改善されています。
bytes.Buffer
の利用:bytes.Buffer
を導入し、すべての文字列構築操作をこのバッファ上で行うように変更されました。これにより、文字列の連結や整形に伴う不必要なメモリ割り当てが劇的に削減されます。addFileLine
パラメータの削除:decorate
関数からaddFileLine
パラメータが削除され、ファイル名と行番号の付加が常に実行されるようになりました。これにより、関数のインターフェースが簡素化され、呼び出し元(log
関数)での引数も不要になりました。- 効率的な行処理:
- 入力文字列
s
をstrings.Split(s, "\n")
で一度にすべての行に分割します。これにより、元の実装のように改行文字を一つずつ探して再帰的に処理する必要がなくなりました。 - 各行に対してループを回し、
buf.WriteByte('\n')
で改行、buf.WriteByte('\t')
でインデントを追加し、buf.WriteString(line)
で実際の行の内容をバッファに書き込みます。 - 2行目以降の行には追加のタブインデント(
buf.WriteByte('\t')
)が適用されるロジックも、bytes.Buffer
上で効率的に実装されています。
- 入力文字列
- 最終的な改行の追加: 入力文字列が改行で終わっていない場合、最後に
buf.WriteByte('\n')
で改行を追加するロジックも、効率的に処理されます。
これらの変更により、decorate
関数はメモリ効率が大幅に向上し、特に大量のログ出力を行うテストにおいて、メモリ使用量の肥大化とパフォーマンスの低下が解消されました。
コアとなるコードの変更箇所
src/pkg/testing/testing.go
ファイルのdecorate
関数が全面的に書き換えられました。
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -79,6 +79,7 @@
package testing
import (
+ "bytes"
"flag"
"fmt"
"os"
@@ -128,37 +129,42 @@ func Short() bool {
return *short
}
-// decorate inserts the final newline if needed and indentation tabs for formatting.
-// If addFileLine is true, it also prefixes the string with the file and line of the call site.
-func decorate(s string, addFileLine bool) string {
- if addFileLine {
- _, file, line, ok := runtime.Caller(3) // decorate + log + public function.
- if ok {
- // Truncate file name at last file name separator.
- if index := strings.LastIndex(file, "/"); index >= 0 {
- file = file[index+1:]
- } else if index = strings.LastIndex(file, "\\"); index >= 0 {
- file = file[index+1:]
- }
- } else {
- file = "???"
- line = 1
- }
- s = fmt.Sprintf("%s:%d: %s", file, line, s)
- }
- s = "\t" + s // Every line is indented at least one tab.
- n := len(s)
- if n > 0 && s[n-1] != '\n' {
- s += "\n"
- n++
- }
- for i := 0; i < n-1; i++ { // -1 to avoid final newline
- if s[i] == '\n' {
- // Second and subsequent lines are indented an extra tab.
- return s[0:i+1] + "\t" + decorate(s[i+1:n], false)
- }
- }
- return s
+// decorate prefixes the string with the file and line of the call site
+// and inserts the final newline if needed and indentation tabs for formatting.
+func decorate(s string) string {
+ _, file, line, ok := runtime.Caller(3) // decorate + log + public function.
+ if ok {
+ // Truncate file name at last file name separator.
+ if index := strings.LastIndex(file, "/"); index >= 0 {
+ file = file[index+1:]
+ } else if index = strings.LastIndex(file, "\\"); index >= 0 {
+ file = file[index+1:]
+ }
+ } else {
+ file = "???"
+ line = 1
+ }
+ buf := new(bytes.Buffer)
+ fmt.Fprintf(buf, "%s:%d: ", file, line)
+
+ lines := strings.Split(s, "\n")
+ for i, line := range lines {
+ if i > 0 {
+ buf.WriteByte('\n')
+ }
+ // Every line is indented at least one tab.
+ buf.WriteByte('\t')
+ if i > 0 {
+ // Second and subsequent lines are indented an extra tab.
+ buf.WriteByte('\t')
+ }
+ buf.WriteString(line)
+ }
+ if l := len(s); l > 0 && s[len(s)-1] != '\n' {
+ // Add final new line if needed.
+ buf.WriteByte('\n')
+ }
+ return buf.String()
}
// T is a type passed to Test functions to manage test state and support formatted test logs.
@@ -204,7 +210,7 @@ func (c *common) FailNow() {
// log generates the output. It's always at the same stack depth.
func (c *common) log(s string) {
- c.output = append(c.output, decorate(s, true)...)
+ c.output = append(c.output, decorate(s)...)
}
// Log formats its arguments using default formatting, analogous to Println(),
コアとなるコードの解説
import "bytes"
の追加
bytes.Buffer
を使用するために、bytes
パッケージがインポートリストに追加されました。
decorate
関数のシグネチャ変更
// 旧: func decorate(s string, addFileLine bool) string
// 新: func decorate(s string) string
addFileLine
というブール値パラメータが削除されました。これは、ファイル名と行番号のプレフィックスが常に付加されるようになったためです。これにより、関数の呼び出し元(log
関数)も簡素化されます。
ファイルと行番号の取得と整形
_, file, line, ok := runtime.Caller(3) // decorate + log + public function.
if ok {
// Truncate file name at last file name separator.
if index := strings.LastIndex(file, "/"); index >= 0 {
file = file[index+1:]
} else if index = strings.LastIndex(file, "\\"); index >= 0 {
file = file[index+1:]
}
} else {
file = "???"
line = 1
}
この部分は変更前とほぼ同じです。runtime.Caller(3)
を使って、decorate
関数を呼び出した元のコード(テスト関数など)のファイル名と行番号を取得します。パスからファイル名のみを抽出する処理も同様です。
bytes.Buffer
の初期化とプレフィックスの書き込み
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "%s:%d: ", file, line)
ここで新しいbytes.Buffer
が作成されます。new(bytes.Buffer)
は&bytes.Buffer{}
と同じ意味で、bytes.Buffer
のゼロ値へのポインタを返します。
次に、fmt.Fprintf
を使って、取得したファイル名と行番号、そしてコロンとスペース(例: main_test.go:42:
)をバッファに書き込みます。これにより、ログメッセージの冒頭部分が効率的に構築されます。
入力文字列の行分割とループ処理
lines := strings.Split(s, "\n")
for i, line := range lines {
if i > 0 {
buf.WriteByte('\n')
}
// Every line is indented at least one tab.
buf.WriteByte('\t')
if i > 0 {
// Second and subsequent lines are indented an extra tab.
buf.WriteByte('\t')
}
buf.WriteString(line)
}
lines := strings.Split(s, "\n")
: 入力文字列s
を改行文字\n
で分割し、各行を要素とする文字列スライスlines
を作成します。これにより、元の再帰的な処理が不要になります。for i, line := range lines
: 分割された各行に対してループ処理を行います。if i > 0 { buf.WriteByte('\n') }
: 2行目以降の行の前に改行文字をバッファに書き込みます。これにより、各行が新しい行に表示されます。buf.WriteByte('\t')
: すべての行の先頭にタブ文字(インデント)を書き込みます。if i > 0 { buf.WriteByte('\t') }
: 2行目以降の行には、さらに追加のタブ文字を書き込み、より深いインデントを適用します。これは、元のdecorate
関数が再帰的に行っていた「2行目以降は追加のタブ」というロジックを再現しています。buf.WriteString(line)
: 現在の行の内容をバッファに書き込みます。
これらの操作はすべてbytes.Buffer
上で行われるため、新しい文字列オブジェクトの生成が最小限に抑えられ、非常に効率的です。
最終的な改行の追加
if l := len(s); l > 0 && s[len(s)-1] != '\n' {
// Add final new line if needed.
buf.WriteByte('\n')
}
元の入力文字列s
が改行で終わっていない場合、整形された出力の最後に改行を追加します。これは、ログ出力の整形規則に合わせたものです。
結果の返却
return buf.String()
最終的に、bytes.Buffer
の内容をstring
型に変換して返します。Buffer.String()
メソッドは、バッファの内容を新しい文字列として返しますが、これは一度の操作であり、ループ内での繰り返し生成とは異なります。
log
関数の変更
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -204,7 +210,7 @@ func (c *common) FailNow() {
// log generates the output. It's always at the same stack depth.
func (c *common) log(s string) {
- c.output = append(c.output, decorate(s, true)...)
+ c.output = append(c.output, decorate(s)...)
}
decorate
関数のシグネチャ変更に伴い、log
関数での呼び出しもdecorate(s, true)
からdecorate(s)
へと簡素化されました。
これらの変更により、decorate
関数はメモリ効率とパフォーマンスが大幅に向上し、Goのテストフレームワークの堅牢性が高まりました。
関連リンク
- Go Issue #3830: https://github.com/golang/go/issues/3830
- Go CL 6373047: https://golang.org/cl/6373047
参考にした情報源リンク
- Go言語の公式ドキュメント:
bytes
パッケージ: https://pkg.go.dev/bytesstrings
パッケージ: https://pkg.go.dev/stringsruntime
パッケージ: https://pkg.go.dev/runtimetesting
パッケージ: https://pkg.go.dev/testing
- Go言語における文字列の不変性に関する一般的な情報源 (例: Goの文字列操作のベストプラクティスに関するブログ記事など)