[インデックス 14177] ファイルの概要
このコミットは、Go言語の標準ライブラリ bufio
パッケージにおける Writer.ReadFrom
メソッドの挙動を修正するものです。具体的には、io.Copy
を用いて bufio.Writer
にデータを書き込む際に、バッファが満たされる前に不必要にフラッシュ(書き込み)が行われる問題を解決します。これにより、特にネットワーク通信のようなシナリオにおいて、多数の小さな書き込みが発生することによるパフォーマンスの低下を防ぎ、効率的なバッファリングを維持します。
コミット
commit e55fdff21030e4925086af90fa669b3e378f2dfc
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Oct 19 16:32:00 2012 +1100
bufio: make Writer.ReadFrom not flush prematurely. For example,
many small writes to a network may be less efficient that a few
large writes.
This fixes net/http's TestClientWrites, broken by 6565056 that
introduced Writer.ReadFrom. That test implicitly assumed that
calling io.Copy on a *bufio.Writer wouldn't write to the
underlying network until the buffer was full.
R=dsymonds
CC=bradfitz, golang-dev, mchaten, mikioh.mikioh
https://golang.org/cl/6743044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e55fdff21030e4925086af90fa669b3e378f2dfc
元コミット内容
bufio: make Writer.ReadFrom not flush prematurely. For example,
many small writes to a network may be less efficient that a few
large writes.
This fixes net/http's TestClientWrites, broken by 6565056 that
introduced Writer.ReadFrom. That test implicitly assumed that
calling io.Copy on a *bufio.Writer wouldn't write to the
underlying network until the buffer was full.
R=dsymonds
CC=bradfitz, golang-dev, mchaten, mikioh.mikioh
https://golang.org/cl/6743044
変更の背景
この変更の背景には、bufio.Writer
の ReadFrom
メソッドが、その設計意図に反してバッファリングの利点を損なう形で早期にフラッシュを行っていた問題があります。
具体的には、以前のコミット 6565056
で Writer.ReadFrom
が導入された際、net/http
パッケージ内の TestClientWrites
というテストが壊れました。このテストは、io.Copy
を *bufio.Writer
に対して呼び出した場合、バッファが満杯になるまで基盤となるネットワークへの書き込みが発生しないことを暗黙的に期待していました。しかし、Writer.ReadFrom
の初期実装では、io.Copy
が呼び出されるたびに、バッファの状態にかかわらず強制的にフラッシュが行われていました。
ネットワーク通信において、多数の小さな書き込みを頻繁に行うことは、オーバーヘッドが大きく非効率的です。理想的には、データをバッファに蓄積し、バッファが満杯になったとき、または明示的にフラッシュが要求されたときに、まとめて大きな塊として書き出すことで、システムコールやネットワークパケットの数を減らし、スループットを向上させることができます。このコミットは、このバッファリングの原則を Writer.ReadFrom
にも適用し、不必要な早期フラッシュを排除することで、net/http
のテストを修正し、全体的なI/O効率を改善することを目的としています。
前提知識の解説
Go言語の io
パッケージと io.ReaderFrom
インターフェース
Go言語の io
パッケージは、I/Oプリミティブを提供し、様々なデータソースやシンク(読み書きの対象)に対して統一的なインターフェースを提供します。
io.ReaderFrom
インターフェースは、以下のように定義されています。
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
このインターフェースは、ReadFrom
メソッドを持つ型が、別の io.Reader
からデータを読み込み、それを自身に書き込むことができることを示します。io.Copy
関数は、内部でこの io.ReaderFrom
インターフェースを効率的に利用しようとします。もし dst
が io.ReaderFrom
を実装しており、src
が io.Reader
であれば、dst.ReadFrom(src)
が直接呼び出され、多くの場合、より最適化されたデータ転送パスが利用されます。
Go言語の bufio
パッケージと bufio.Writer
bufio
パッケージは、I/O操作をバッファリングすることで効率を向上させる機能を提供します。
bufio.Writer
は、基盤となる io.Writer
への書き込みをバッファリングする構造体です。データはまず bufio.Writer
の内部バッファに書き込まれ、以下のいずれかの条件が満たされたときに、基盤の io.Writer
にフラッシュ(実際の書き込み)されます。
- バッファが満杯になったとき。
Flush()
メソッドが明示的に呼び出されたとき。Close()
メソッドが呼び出されたとき(通常、Flush()
を内部で呼び出す)。Write
操作が、バッファの残りの容量を超えるデータを書き込もうとしたとき。
バッファリングの主な目的は、システムコール(OSへのI/O要求)の回数を減らすことです。システムコールは比較的コストの高い操作であり、特にネットワークI/Oでは、各パケットの送信にもオーバーヘッドが伴います。小さなデータを頻繁に書き込む代わりに、バッファにデータをまとめてから一度に書き出すことで、これらのオーバーヘッドを削減し、アプリケーションのスループットを向上させることができます。
早期フラッシュ(Premature Flush)の問題
「早期フラッシュ」とは、バッファリングの目的を損なう形で、バッファが満杯になる前に不必要にデータが基盤の io.Writer
に書き出されてしまう状況を指します。これは、特にネットワーク通信において深刻なパフォーマンス問題を引き起こす可能性があります。例えば、1バイトずつデータを書き込むたびにフラッシュが発生すると、1バイトのデータのためにネットワークパケットが送信され、そのたびにTCP/IPスタックのオーバーヘッドやネットワーク遅延が発生します。これは、多数の小さなパケットが飛び交うことになり、帯域幅の利用効率も悪化させます。
このコミットの修正は、bufio.Writer.ReadFrom
が、io.Copy
などの操作を通じてデータを読み込む際に、この早期フラッシュを回避し、bufio.Writer
の本来のバッファリング戦略に従うようにすることを目的としています。
技術的詳細
このコミットの技術的な核心は、bufio.Writer
の ReadFrom
メソッドにおけるフラッシュのタイミングの変更です。
以前の ReadFrom
の実装では、メソッドの冒頭で b.Flush()
が無条件に呼び出されていました。これは、ReadFrom
が開始される前に、bufio.Writer
の内部バッファに既に存在するデータをすべて基盤の io.Writer
に書き出すことを意味します。さらに、ループ内で io.Reader
からデータを読み込むたびに、b.Flush()
が再度呼び出されていました。この挙動は、bufio.Writer
のバッファリングの利点を完全に打ち消し、ReadFrom
が呼び出されるたびに、あるいは内部ループの各イテレーションで、データが強制的にフラッシュされる原因となっていました。
新しい実装では、この無条件なフラッシュが削除され、より賢明なフラッシュ戦略が導入されています。
-
初期フラッシュの削除と
io.ReaderFrom
の最適化:ReadFrom
メソッドの冒頭にあったb.Flush()
が削除されました。 代わりに、b.Buffered() == 0
(バッファが空である) かつ、基盤のb.wr
がio.ReaderFrom
インターフェースを実装している場合にのみ、その基盤のReadFrom
メソッドを直接呼び出すように変更されました。これは、バッファが空であれば、bufio.Writer
が介在する必要がなく、基盤のio.Writer
が提供するより効率的なReadFrom
実装を直接利用できるためです。これにより、不必要なバッファリング層をスキップし、直接的なデータ転送が可能になります。 -
ループ内フラッシュの条件化: データを読み込むループ内でのフラッシュも条件付きになりました。以前は
b.Flush()
が無条件に呼び出されていましたが、新しいコードではb.Available() == 0
(バッファに空きがない、つまりバッファが満杯である) 場合にのみb.Flush()
が呼び出されます。これは、bufio.Writer
の本来のバッファリング戦略に合致しており、バッファが満杯になったときにのみデータをフラッシュすることで、システムコールやネットワーク書き込みの回数を最小限に抑えます。
これらの変更により、bufio.Writer.ReadFrom
は、io.Copy
などの操作を通じて大量のデータを効率的に転送する際に、バッファリングの利点を最大限に活用できるようになりました。テストケース TestWriterReadFromCounts
の追加も、この新しい挙動が正しく機能していることを検証するために重要です。このテストは、bufio.Writer
にデータをコピーする際に、基盤の io.Writer
への書き込みが期待通りにバッファリングされ、不必要なフラッシュが発生していないことを確認します。
コアとなるコードの変更箇所
src/pkg/bufio/bufio.go
の ReadFrom
メソッドが変更されています。
diff --git a/src/pkg/bufio/bufio.go b/src/pkg/bufio/bufio.go
index d1c5a13bca..cd51585f84 100644
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -569,11 +569,10 @@ func (b *Writer) WriteString(s string) (int, error) {
// ReadFrom implements io.ReaderFrom.
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
- if err = b.Flush(); err != nil {
- return 0, err
- }
- if w, ok := b.wr.(io.ReaderFrom); ok {
- return w.ReadFrom(r)
+ if b.Buffered() == 0 {
+ if w, ok := b.wr.(io.ReaderFrom); ok {
+ return w.ReadFrom(r)
+ }
}
var m int
for {
@@ -583,8 +582,10 @@ func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
}
b.n += m
n += int64(m)
- if err1 := b.Flush(); err1 != nil {
- return n, err1
+ if b.Available() == 0 {
+ if err1 := b.Flush(); err1 != nil {
+ return n, err1
+ }
}
if err != nil {
break
また、src/pkg/bufio/bufio_test.go
に TestWriterReadFromCounts
という新しいテストケースが追加されています。
diff --git a/src/pkg/bufio/bufio_test.go b/src/pkg/bufio/bufio_test.go
index 3d07639e2a..763b12326a 100644
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -881,6 +881,64 @@ func TestWriterReadFromErrors(t *testing.T) {
}\n}\n \n+// TestWriterReadFromCounts tests that using io.Copy to copy into a\n+// bufio.Writer does not prematurely flush the buffer. For example, when\n+// buffering writes to a network socket, excessive network writes should be\n+// avoided.\n+func TestWriterReadFromCounts(t *testing.T) {\n+\tvar w0 writeCountingDiscard\n+\tb0 := NewWriterSize(&w0, 1234)\n+\tb0.WriteString(strings.Repeat(\"x\", 1000))\n+\tif w0 != 0 {\n+\t\tt.Fatalf(\"write 1000 'x's: got %d writes, want 0\", w0)\n+\t}\n+\tb0.WriteString(strings.Repeat(\"x\", 200))\n+\tif w0 != 0 {\n+\t\tt.Fatalf(\"write 1200 'x's: got %d writes, want 0\", w0)\n+\t}\n+\tio.Copy(b0, &onlyReader{strings.NewReader(strings.Repeat(\"x\", 30))})\n+\tif w0 != 0 {\n+\t\tt.Fatalf(\"write 1230 'x's: got %d writes, want 0\", w0)\n+\t}\n+\tio.Copy(b0, &onlyReader{strings.NewReader(strings.Repeat(\"x\", 9))})\n+\tif w0 != 1 {\n+\t\tt.Fatalf(\"write 1239 'x's: got %d writes, want 1\", w0)\n+\t}\n+\n+\tvar w1 writeCountingDiscard\n+\tb1 := NewWriterSize(&w1, 1234)\n+\tb1.WriteString(strings.Repeat(\"x\", 1200))\n+\tb1.Flush()\n+\tif w1 != 1 {\n+\t\tt.Fatalf(\"flush 1200 'x's: got %d writes, want 1\", w1)\n+\t}\n+\tb1.WriteString(strings.Repeat(\"x\", 89))\n+\tif w1 != 1 {\n+\t\tt.Fatalf(\"write 1200 + 89 'x's: got %d writes, want 1\", w1)\n+\t}\n+\tio.Copy(b1, &onlyReader{strings.NewReader(strings.Repeat(\"x\", 700))})\n+\tif w1 != 1 {\n+\t\tt.Fatalf(\"write 1200 + 789 'x's: got %d writes, want 1\", w1)\n+\t}\n+\tio.Copy(b1, &onlyReader{strings.NewReader(strings.Repeat(\"x\", 600))})\n+\tif w1 != 2 {\n+\t\tt.Fatalf(\"write 1200 + 1389 'x's: got %d writes, want 2\", w1)\n+\t}\n+\tb1.Flush()\n+\tif w1 != 3 {\n+\t\tt.Fatalf(\"flush 1200 + 1389 'x's: got %d writes, want 3\", w1)\n+\t}\n+}\n+\n+// A writeCountingDiscard is like ioutil.Discard and counts the number of times\n+// Write is called on it.\n+type writeCountingDiscard int\n+\n+func (w *writeCountingDiscard) Write(p []byte) (int, error) {\n+\t*w++\n+\treturn len(p), nil\n+}\n+\n // An onlyReader only implements io.Reader, no matter what other methods the underlying implementation may have.\n type onlyReader struct {\n \tr io.Reader\n```
## コアとなるコードの解説
### `src/pkg/bufio/bufio.go` の変更点
`func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)` メソッドの変更を詳しく見ていきます。
**変更前:**
```go
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
if err = b.Flush(); err != nil { // (A) 無条件の初期フラッシュ
return 0, err
}
if w, ok := b.wr.(io.ReaderFrom); ok { // (B) 基盤のWriterがReaderFromを実装していれば直接呼び出し
return w.ReadFrom(r)
}
var m int
for {
// ... (データ読み込みとバッファへの書き込み) ...
if err1 := b.Flush(); err1 != nil { // (C) ループ内の無条件フラッシュ
return n, err1
}
// ...
}
}
- (A) 無条件の初期フラッシュ:
ReadFrom
が呼び出されると、まずb.Flush()
が無条件に実行されていました。これは、bufio.Writer
のバッファにデータが残っている場合に、ReadFrom
が開始される前にそのデータを強制的に書き出すことを意味します。しかし、これはio.Copy
のような操作でbufio.Writer
を使う場合に、バッファリングの意図に反して早期にデータがフラッシュされる原因となっていました。 - (B) 基盤のWriterがReaderFromを実装していれば直接呼び出し: この部分は、基盤の
io.Writer
がio.ReaderFrom
を実装している場合に、その最適化されたReadFrom
メソッドを直接利用しようとするものです。これは効率的なデータ転送のために良いパターンですが、(A) の無条件フラッシュがその前に実行されるため、バッファリングの利点が損なわれる可能性がありました。 - (C) ループ内の無条件フラッシュ: データを読み込むループの各イテレーションで、
b.Flush()
が再度無条件に呼び出されていました。これは、io.Reader
から読み込んだ小さなチャンクが、バッファが満杯になるのを待たずにすぐに基盤のio.Writer
に書き出されることを意味し、特にネットワークI/Oにおいて非効率的でした。
変更後:
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
if b.Buffered() == 0 { // (A') バッファが空の場合のみ
if w, ok := b.wr.(io.ReaderFrom); ok { // (B') 基盤のWriterがReaderFromを実装していれば直接呼び出し
return w.ReadFrom(r)
}
}
var m int
for {
// ... (データ読み込みとバッファへの書き込み) ...
if b.Available() == 0 { // (C') バッファに空きがない(満杯)の場合のみ
if err1 := b.Flush(); err1 != nil {
return n, err1
}
}
// ...
}
}
- (A') バッファが空の場合のみ:
ReadFrom
の冒頭にあった無条件のb.Flush()
が削除されました。代わりに、b.Buffered() == 0
(バッファが空である) という条件が追加されました。この条件が真の場合にのみ、基盤のio.Writer
がio.ReaderFrom
を実装しているかどうかのチェックが行われ、実装していれば直接そのReadFrom
が呼び出されます。これにより、bufio.Writer
のバッファにデータが残っている場合は、直接io.ReaderFrom
を呼び出す最適化パスはスキップされ、バッファリングが優先されます。 - (B') 基盤のWriterがReaderFromを実装していれば直接呼び出し: この部分は変更前と同じですが、(A') の条件によって、バッファが空の場合にのみこの最適化パスが利用されるようになりました。
- (C') バッファに空きがない(満杯)の場合のみ: ループ内の
b.Flush()
の呼び出しにb.Available() == 0
(バッファに空きがない、つまりバッファが満杯である) という条件が追加されました。これにより、データがバッファに蓄積され、バッファが満杯になったときにのみフラッシュが行われるようになります。これはbufio.Writer
の本来のバッファリング戦略に合致し、不必要な早期フラッシュを防ぎます。
src/pkg/bufio/bufio_test.go
の変更点
TestWriterReadFromCounts
テストは、bufio.Writer
の新しい ReadFrom
の挙動を検証するために追加されました。
writeCountingDiscard
型: これはio.Writer
インターフェースを実装するカスタム型で、Write
メソッドが呼び出された回数をカウントします。これにより、基盤のio.Writer
に実際にデータが書き込まれた回数を追跡できます。- テストロジック:
NewWriterSize
で特定のバッファサイズを持つbufio.Writer
を作成します。WriteString
やio.Copy
を使ってデータを書き込みます。writeCountingDiscard
のカウントをチェックし、バッファが満杯になるまで、または明示的にFlush()
が呼び出されるまで、基盤のio.Writer
への書き込み(フラッシュ)が発生しないことを検証します。- 例えば、バッファサイズ1234バイトの
bufio.Writer
に1230バイト書き込んでもフラッシュが発生せず、さらに9バイト書き込んで合計1239バイトになったときに初めて1回のフラッシュが発生することを確認しています。これは、バッファが満杯になったときにのみフラッシュされるという期待される挙動を示しています。
このテストは、bufio.Writer.ReadFrom
がバッファリングの原則に従い、不必要な早期フラッシュを行わないことを保証するための重要な検証です。
関連リンク
- Go CL 6743044: https://golang.org/cl/6743044
- 関連するコミット 6565056 (Writer.ReadFromの導入): https://github.com/golang/go/commit/6565056
参考にした情報源リンク
- Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
bufio
パッケージドキュメント: https://pkg.go.dev/bufio - Go言語のソースコード (GitHub): https://github.com/golang/go