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

[インデックス 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.WriterReadFrom メソッドが、その設計意図に反してバッファリングの利点を損なう形で早期にフラッシュを行っていた問題があります。

具体的には、以前のコミット 6565056Writer.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 インターフェースを効率的に利用しようとします。もし dstio.ReaderFrom を実装しており、srcio.Reader であれば、dst.ReadFrom(src) が直接呼び出され、多くの場合、より最適化されたデータ転送パスが利用されます。

Go言語の bufio パッケージと bufio.Writer

bufio パッケージは、I/O操作をバッファリングすることで効率を向上させる機能を提供します。 bufio.Writer は、基盤となる io.Writer への書き込みをバッファリングする構造体です。データはまず bufio.Writer の内部バッファに書き込まれ、以下のいずれかの条件が満たされたときに、基盤の io.Writer にフラッシュ(実際の書き込み)されます。

  1. バッファが満杯になったとき。
  2. Flush() メソッドが明示的に呼び出されたとき。
  3. Close() メソッドが呼び出されたとき(通常、Flush() を内部で呼び出す)。
  4. Write 操作が、バッファの残りの容量を超えるデータを書き込もうとしたとき。

バッファリングの主な目的は、システムコール(OSへのI/O要求)の回数を減らすことです。システムコールは比較的コストの高い操作であり、特にネットワークI/Oでは、各パケットの送信にもオーバーヘッドが伴います。小さなデータを頻繁に書き込む代わりに、バッファにデータをまとめてから一度に書き出すことで、これらのオーバーヘッドを削減し、アプリケーションのスループットを向上させることができます。

早期フラッシュ(Premature Flush)の問題

「早期フラッシュ」とは、バッファリングの目的を損なう形で、バッファが満杯になる前に不必要にデータが基盤の io.Writer に書き出されてしまう状況を指します。これは、特にネットワーク通信において深刻なパフォーマンス問題を引き起こす可能性があります。例えば、1バイトずつデータを書き込むたびにフラッシュが発生すると、1バイトのデータのためにネットワークパケットが送信され、そのたびにTCP/IPスタックのオーバーヘッドやネットワーク遅延が発生します。これは、多数の小さなパケットが飛び交うことになり、帯域幅の利用効率も悪化させます。

このコミットの修正は、bufio.Writer.ReadFrom が、io.Copy などの操作を通じてデータを読み込む際に、この早期フラッシュを回避し、bufio.Writer の本来のバッファリング戦略に従うようにすることを目的としています。

技術的詳細

このコミットの技術的な核心は、bufio.WriterReadFrom メソッドにおけるフラッシュのタイミングの変更です。

以前の ReadFrom の実装では、メソッドの冒頭で b.Flush() が無条件に呼び出されていました。これは、ReadFrom が開始される前に、bufio.Writer の内部バッファに既に存在するデータをすべて基盤の io.Writer に書き出すことを意味します。さらに、ループ内で io.Reader からデータを読み込むたびに、b.Flush() が再度呼び出されていました。この挙動は、bufio.Writer のバッファリングの利点を完全に打ち消し、ReadFrom が呼び出されるたびに、あるいは内部ループの各イテレーションで、データが強制的にフラッシュされる原因となっていました。

新しい実装では、この無条件なフラッシュが削除され、より賢明なフラッシュ戦略が導入されています。

  1. 初期フラッシュの削除と io.ReaderFrom の最適化: ReadFrom メソッドの冒頭にあった b.Flush() が削除されました。 代わりに、b.Buffered() == 0 (バッファが空である) かつ、基盤の b.wrio.ReaderFrom インターフェースを実装している場合にのみ、その基盤の ReadFrom メソッドを直接呼び出すように変更されました。これは、バッファが空であれば、bufio.Writer が介在する必要がなく、基盤の io.Writer が提供するより効率的な ReadFrom 実装を直接利用できるためです。これにより、不必要なバッファリング層をスキップし、直接的なデータ転送が可能になります。

  2. ループ内フラッシュの条件化: データを読み込むループ内でのフラッシュも条件付きになりました。以前は b.Flush() が無条件に呼び出されていましたが、新しいコードでは b.Available() == 0 (バッファに空きがない、つまりバッファが満杯である) 場合にのみ b.Flush() が呼び出されます。これは、bufio.Writer の本来のバッファリング戦略に合致しており、バッファが満杯になったときにのみデータをフラッシュすることで、システムコールやネットワーク書き込みの回数を最小限に抑えます。

これらの変更により、bufio.Writer.ReadFrom は、io.Copy などの操作を通じて大量のデータを効率的に転送する際に、バッファリングの利点を最大限に活用できるようになりました。テストケース TestWriterReadFromCounts の追加も、この新しい挙動が正しく機能していることを検証するために重要です。このテストは、bufio.Writer にデータをコピーする際に、基盤の io.Writer への書き込みが期待通りにバッファリングされ、不必要なフラッシュが発生していないことを確認します。

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

src/pkg/bufio/bufio.goReadFrom メソッドが変更されています。

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.goTestWriterReadFromCounts という新しいテストケースが追加されています。

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.Writerio.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.Writerio.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 を作成します。
    • WriteStringio.Copy を使ってデータを書き込みます。
    • writeCountingDiscard のカウントをチェックし、バッファが満杯になるまで、または明示的に Flush() が呼び出されるまで、基盤の io.Writer への書き込み(フラッシュ)が発生しないことを検証します。
    • 例えば、バッファサイズ1234バイトの bufio.Writer に1230バイト書き込んでもフラッシュが発生せず、さらに9バイト書き込んで合計1239バイトになったときに初めて1回のフラッシュが発生することを確認しています。これは、バッファが満杯になったときにのみフラッシュされるという期待される挙動を示しています。

このテストは、bufio.Writer.ReadFrom がバッファリングの原則に従い、不必要な早期フラッシュを行わないことを保証するための重要な検証です。

関連リンク

参考にした情報源リンク