[インデックス 19377] ファイルの概要
このコミットは、Go言語の cmd/pack
パッケージ内の TestLargeDefs
テストにおいて、ファイル書き込みのパフォーマンスを改善することを目的としています。具体的には、bufio.Writer
を導入することで、大量の小さな書き込み操作をバッファリングし、特にPlan 9のようなディスクI/Oが遅い環境でのテスト実行時間を大幅に短縮しています。
コミット
commit c6aa2e5ac8097f9491a407c3bb2385159d9aed32
Author: Anthony Martin <ality@pbrane.org>
Date: Thu May 15 20:12:06 2014 -0700
cmd/pack: buffer writes in TestLargeDefs
TestLargeDefs was issuing over one million small writes to
create a 7MB file (large.go). This is quite slow on Plan 9
since our disk file systems aren't very fast and they're
usually accessed over the network.
Buffering the writes makes the test about six times faster.
Even on Linux, it's about 1.5 times faster.
Here are the results on a slow Plan 9 machine:
Before:
% ./pack.test -test.v -test.run TestLargeDefs
=== RUN TestLargeDefs
--- PASS: TestLargeDefs (125.11 seconds)
PASS
After:
% ./pack.test -test.v -test.run TestLargeDefs
=== RUN TestLargeDefs
--- PASS: TestLargeDefs (20.835 seconds)
PASS
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/95040044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c6aa2e5ac8097f9491a407c3bb2385159d9aed32
元コミット内容
cmd/pack
パッケージの TestLargeDefs
テストにおいて、書き込み処理をバッファリングする変更です。
TestLargeDefs
は、約7MBの large.go
ファイルを作成するために100万回以上の小さな書き込みを行っていました。これは、特にディスクファイルシステムが高速ではなく、通常ネットワーク経由でアクセスされるPlan 9環境では非常に遅い処理でした。
書き込みをバッファリングすることで、テストの実行速度が約6倍に向上しました。Linux環境でも約1.5倍の高速化が見られました。
Plan 9環境でのテスト結果は以下の通りです:
変更前: TestLargeDefs
が125.11秒
変更後: TestLargeDefs
が20.835秒
変更の背景
この変更の主な背景は、cmd/pack
パッケージの TestLargeDefs
テストの実行速度が、特にPlan 9オペレーティングシステム上で非常に遅かったことです。
TestLargeDefs
は、large.go
という約7MBのファイルを生成する際に、100万回を超える非常に小さな書き込み操作を繰り返していました。ファイルシステムへの書き込みは、通常、システムコールを伴うI/O操作であり、これらのシステムコールはオーバーヘッドを伴います。特に、Plan 9のような分散システムでは、ファイルシステムがネットワーク経由でアクセスされることが多く、個々の小さな書き込みがネットワークレイテンシとシステムコールオーバーヘッドの累積によって、極めて非効率的になっていました。
この非効率性がテストの実行時間を著しく長くし、開発サイクルを遅らせる原因となっていました。テストの実行時間を短縮することは、開発者の生産性向上に直結するため、このパフォーマンスボトルネックを解消することが喫緊の課題でした。
前提知識の解説
Go言語の io.Writer
インターフェース
Go言語では、データの書き込み操作は io.Writer
インターフェースによって抽象化されています。このインターフェースは、単一の Write
メソッドを定義しています。
type Writer interface {
Write(p []byte) (n int, err error)
}
Write
メソッドは、バイトスライス p
のデータを書き込み、書き込まれたバイト数 n
とエラー err
を返します。ファイル、ネットワーク接続、標準出力など、様々な出力先がこの io.Writer
インターフェースを実装しています。
fmt.Fprintf
関数
fmt.Fprintf
は、io.Writer
インターフェースを実装する任意の出力先に対して、フォーマットされた文字列を書き込むための関数です。
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
この関数は、w
で指定された io.Writer
に、format
文字列とそれに続く引数 a
を使ってフォーマットされたデータを書き込みます。内部的には、w.Write
メソッドを呼び出してデータを書き込みます。
bufio.Writer
bufio.Writer
は、io.Writer
の実装をラップし、書き込み操作をバッファリングすることでI/Oパフォーマンスを向上させるための型です。
bufio.Writer
は内部にバッファ(メモリ領域)を持ち、Write
メソッドが呼び出されても、すぐに基になる io.Writer
にデータを書き込むのではなく、まずそのバッファにデータを蓄積します。バッファが満杯になったとき、または Flush
メソッドが明示的に呼び出されたときに、バッファ内のすべてのデータが一度に基になる io.Writer
に書き込まれます。
これにより、多数の小さな書き込み操作が、より少ない回数の大きな書き込み操作にまとめられ、システムコールやディスクI/Oの回数を劇的に減らすことができます。結果として、特にI/Oコストが高い環境(ネットワークファイルシステム、HDDなど)でのパフォーマンスが大幅に向上します。
cmd/pack
パッケージ
cmd/pack
は、Go言語のツールチェインの一部であり、Goのパッケージアーカイブ(.a
ファイル)を操作するためのコマンドです。これは、Goのビルドプロセスにおいて、コンパイルされたオブジェクトファイルをまとめる役割を担っていました。Go 1.5以降、このツールは go tool compile
や go tool link
に統合され、直接使用されることは少なくなりましたが、当時のGoのビルドシステムにおいて重要な役割を果たしていました。
Plan 9 オペレーティングシステム
Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの概念をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルとして表現し、それらをファイルシステムを通じてアクセスするという思想を徹底しています。
Plan 9のファイルシステムは、ネットワーク透過性が高く、リモートのファイルサーバー上のファイルもローカルファイルと同じように扱えます。しかし、このネットワーク透過性は、個々のI/O操作においてネットワークレイテンシが加わることを意味し、特に多数の小さなI/O操作を行う場合には、パフォーマンス上のボトルネックとなることがありました。このコミットの背景にある「ディスクファイルシステムが高速ではなく、通常ネットワーク経由でアクセスされる」という記述は、Plan 9のこのような特性を指しています。
技術的詳細
このコミットの技術的詳細な変更点は、TestLargeDefs
関数内でファイルへの書き込みを行う際に、直接 os.File
オブジェクト(f
)に対して fmt.Fprintf
を呼び出すのではなく、bufio.NewWriter(f)
を使って作成したバッファ付きライター(b
)を介して書き込みを行うようにしたことです。
元のコードでは、printf
関数が呼び出されるたびに、内部で fmt.Fprintf(f, ...)
が実行され、これは最終的に f
(*os.File
)の Write
メソッドを呼び出していました。os.File
の Write
メソッドは、通常、書き込みごとにシステムコールを発行します。TestLargeDefs
は、約7MBの large.go
ファイルを生成するために、100万回以上の小さな文字列の書き込みを行っていたため、これが100万回以上のシステムコール発行につながり、非常に大きなオーバーヘッドとなっていました。
変更後、bufio.NewWriter(f)
によって bufio.Writer
のインスタンス b
が作成されます。この b
は f
をラップし、io.Writer
インターフェースを実装しています。printf
関数内での fmt.Fprintf(b, ...)
の呼び出しは、今度は b
の Write
メソッドを呼び出します。bufio.Writer
の Write
メソッドは、受け取ったデータをまず内部バッファに格納します。バッファが満杯になるか、明示的に b.Flush()
が呼び出されるまで、実際のディスクへの書き込み(基になる f.Write
の呼び出し)は行われません。
これにより、100万回以上の小さな書き込み操作が、バッファのサイズに応じたより少ない回数の大きな書き込み操作に集約されます。結果として、システムコールの発行回数が劇的に減少し、I/Oの効率が向上します。テストの最後に b.Flush()
を呼び出すことで、バッファに残っているすべてのデータが確実にファイルに書き込まれるようにしています。
この最適化は、特にI/Oがボトルネックとなる環境(例: ネットワークファイルシステム、低速なストレージ)で顕著な効果を発揮します。コミットメッセージにあるように、Plan 9環境では約6倍、Linux環境でも約1.5倍の高速化が実現されました。これは、I/Oバッファリングが、システムコールオーバーヘッドとディスクI/Oのレイテンシを効果的に隠蔽できることを示しています。
コアとなるコードの変更箇所
変更は src/cmd/pack/pack_test.go
ファイルに対して行われました。
--- a/src/cmd/pack/pack_test.go
+++ b/src/cmd/pack/pack_test.go
@@ -5,6 +5,7 @@
package main
import (
+ "bufio" // 追加
"bytes"
"fmt"
"io"
@@ -223,9 +224,10 @@ func TestLargeDefs(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ b := bufio.NewWriter(f) // 追加
printf := func(format string, args ...interface{}) {
- _, err := fmt.Fprintf(f, format, args...) // 変更前
+ _, err := fmt.Fprintf(b, format, args...) // 変更後
if err != nil {
t.Fatalf("Writing to %s: %v", large, err)
}
@@ -240,6 +242,9 @@ func TestLargeDefs(t *testing.T) {
printf("`\\n\"")
}
printf("}\\n")
+ if err = b.Flush(); err != nil { // 追加
+ t.Fatal(err)
+ }
if err = f.Close(); err != nil {
t.Fatal(err)
}
コアとなるコードの解説
-
import ("bufio")
の追加:bufio
パッケージは、バッファリングされたI/O操作を提供します。bufio.Writer
を使用するために、このパッケージのインポートが必要になります。 -
b := bufio.NewWriter(f)
の追加:TestLargeDefs
関数内で、ファイルf
(*os.File
型)をラップする新しいbufio.Writer
インスタンスb
が作成されます。これ以降、b
への書き込みは、直接ファイルf
に書き込むのではなく、まずb
の内部バッファに蓄積されます。 -
fmt.Fprintf(f, ...)
からfmt.Fprintf(b, ...)
への変更:printf
関数は、フォーマットされた文字列をファイルに書き込むためのヘルパー関数です。変更前は、直接f
(*os.File
)に対して書き込みを行っていました。この変更により、書き込み先がb
(*bufio.Writer
)に変更されました。これにより、printf
が呼び出されるたびに、データはbufio.Writer
のバッファに書き込まれるようになり、実際のシステムコールによるファイルI/Oの回数が削減されます。 -
if err = b.Flush(); err != nil { t.Fatal(err) }
の追加: すべてのデータがprintf
関数を通じてb
に書き込まれた後、b.Flush()
が呼び出されます。このFlush
メソッドは、bufio.Writer
の内部バッファに残っているすべてのデータを、基になるファイルf
に強制的に書き出します。これにより、テストが終了する前にすべてのデータがディスクに永続化されることが保証されます。Flush
がエラーを返した場合、テストは失敗します。
これらの変更により、TestLargeDefs
は、多数の小さな書き込みを単一または少数の大きな書き込みに集約することで、I/O操作のオーバーヘッドを劇的に削減し、テストの実行速度を大幅に向上させました。
関連リンク
- Go CL 95040044: https://golang.org/cl/95040044
参考にした情報源リンク
- Go言語
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語
fmt
パッケージドキュメント: https://pkg.go.dev/fmt - Go言語
bufio
パッケージドキュメント: https://pkg.go.dev/bufio - Plan 9 from Bell Labs (Wikipedia): https://ja.wikipedia.org/wiki/Plan_9_from_Bell_Labs
- Go
cmd/pack
(Go 1.4 documentation): https://pkg.go.dev/cmd/pack@go1.4 (当時のpack
コマンドの役割を理解するため)I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. I have included explanations for the background, prerequisite knowledge, technical details, core code changes, and their explanations. I also used the provided GitHub URL and thegolang.org/cl
link from the commit message. I did not save any files and outputted the content to standard output.