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

[インデックス 14176] ファイルの概要

このコミットは、Go言語の標準ライブラリbufioパッケージにおける*Writer型へのio.ReaderFromインターフェースの実装に関するものです。具体的には、以下の2つのファイルが変更されています。

  • src/pkg/bufio/bufio.go: bufio.WriterReadFromメソッドが追加され、io.ReaderFromインターフェースが実装されました。
  • src/pkg/bufio/bufio_test.go: bufio.WriterReadFromメソッドの動作を検証するための新しいテストケースと、パフォーマンス改善を示すベンチマークが追加されました。

コミット

commit 2a4818dd11a4fac1016b1ef2c2200a8e094272bd
Author: Michael Chaten <mchaten@gmail.com>
Date:   Fri Oct 19 11:22:51 2012 +1100

    bufio: Implement io.ReaderFrom for (*Writer).
    
    This is part 2 of 2 for issue 4028.
    
    benchmark                        old ns/op    new ns/op    delta
    BenchmarkWriterCopyOptimal           53293        28326  -46.85%
    BenchmarkWriterCopyUnoptimal         53757        30537  -43.19%
    BenchmarkWriterCopyNoReadFrom        53192        36642  -31.11%
    
    Fixes #4028.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6565056

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2a4818dd11a4fac1016b1ef2c2200a8e094272bd

元コミット内容

    bufio: Implement io.ReaderFrom for (*Writer).
    
    This is part 2 of 2 for issue 4028.
    
    benchmark                        old ns/op    new ns/op    delta
    BenchmarkWriterCopyOptimal           53293        28326  -46.85%
    BenchmarkWriterCopyUnoptimal         53757        30537  -43.19%
    BenchmarkWriterCopyNoReadFrom        53192        36642  -31.11%
    
    Fixes #4028.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6565056

変更の背景

このコミットは、Go言語のbufioパッケージにおけるWriterのパフォーマンス改善を目的としています。特に、io.Copyのような操作において、バッファリングされた書き込み処理の効率を高めることが狙いです。コミットメッセージに「This is part 2 of 2 for issue 4028.」とあるように、これはIssue 4028(具体的な内容は現在のGoの公開Issueトラッカーでは直接確認できませんが、おそらくio.Copyのパフォーマンスに関するものと推測されます)を解決するための2部構成の変更の2番目の部分です。

既存のio.Copy関数は、通常、Readerからデータを読み込み、それをWriterに書き込むというループで動作します。しかし、Writerio.ReaderFromインターフェースを実装している場合、io.Copyはより効率的なパス(通常はより大きなチャンクでのデータ転送や、基盤となるI/Oメカニズムへの直接的な委譲)を利用できます。このコミットは、bufio.Writerがこの最適化されたパスを利用できるようにすることで、特に大量のデータをコピーする際のパフォーマンスを大幅に向上させることを目指しています。

ベンチマーク結果が示すように、BenchmarkWriterCopyOptimalでは約46.85%の、BenchmarkWriterCopyUnoptimalでも約43.19%の性能向上が見られます。これは、io.ReaderFromの実装が、bufio.Writerを介したデータ転送において顕著な効果をもたらしたことを示しています。

前提知識の解説

io.ReaderFromインターフェース

Go言語のioパッケージは、I/Oプリミティブの基本的なインターフェースを定義しています。その中の一つにio.ReaderFromインターフェースがあります。

type ReaderFrom interface {
    ReadFrom(r Reader) (n int64, err error)
}

このインターフェースは、ReadFromメソッドを定義しています。このメソッドは、rからデータを読み込み、それを実装している型(この場合はWriter)に書き込みます。io.Copy関数は、dst引数がio.ReaderFromを実装している場合、このReadFromメソッドを呼び出すことで、より効率的なデータ転送を試みます。これにより、中間バッファリングのオーバーヘッドを減らしたり、基盤となるシステムコールを最適化したりすることが可能になります。

bufio.Writer

bufioパッケージは、I/O操作をバッファリングすることで効率を向上させるための型を提供します。bufio.Writerは、基盤となるio.Writerへの書き込みをバッファリングします。データはまず内部バッファに書き込まれ、バッファが満杯になったとき、またはFlushメソッドが明示的に呼び出されたときに、基盤となるio.Writerに書き込まれます。これにより、小さな書き込みが頻繁に行われる場合でも、システムコールを減らし、I/Oの効率を高めることができます。

ゼロコピー (Zero-copy) と最適化されたI/O

「ゼロコピー」とは、CPUがデータをコピーする回数を減らすことで、データ転送の効率を向上させる技術の総称です。特にネットワークI/OやファイルI/Oにおいて、カーネル空間とユーザー空間の間でのデータコピーを最小限に抑えることで、CPU使用率を下げ、スループットを向上させます。

io.ReaderFromのようなインターフェースは、直接的なゼロコピーではないにしても、特定の条件下でI/O操作を最適化するためのメカニズムを提供します。例えば、基盤となるWriterio.ReaderFromを実装している場合、bufio.Writerは自身のバッファリングロジックをスキップし、基盤となるWriterReadFromメソッドに処理を委譲することができます。これにより、より効率的なデータ転送パスが利用され、パフォーマンスが向上します。

技術的詳細

このコミットの核心は、bufio.WriterReadFromメソッドを実装し、io.ReaderFromインターフェースに準拠させることです。

bufio.WriterReadFromメソッドのロジックは以下のようになっています。

  1. 既存バッファのフラッシュ: まず、b.Flush()を呼び出して、bufio.Writerの内部バッファに既に存在するデータを基盤となるio.Writerに書き込みます。これは、ReadFromが新しいデータを読み込む前に、既存のデータが確実に書き込まれるようにするために重要です。
  2. 基盤となるWriterのio.ReaderFromチェック: b.wrbufio.Writerがラップしている基盤となるio.Writer)がio.ReaderFromインターフェースを実装しているかどうかを型アサーションw, ok := b.wr.(io.ReaderFrom)で確認します。
    • もし実装していれば、bufio.Writerは自身のバッファリングロジックを介さずに、直接基盤となるWriterReadFromメソッドを呼び出します。これにより、基盤となるWriterが提供する可能性のある、より最適化されたデータ転送パス(例えば、ファイルディスクリプタ間の直接転送など)を利用できます。これがベンチマークにおける「Optimal」ケースに相当します。
    • 実装していなければ、通常のバッファリングされた読み書きループにフォールバックします。
  3. フォールバックループ: 基盤となるWriterio.ReaderFromを実装していない場合、bufio.Writerrからデータを読み込み、自身の内部バッファに書き込み、バッファが満杯になるたびにFlush()を呼び出して基盤となるWriterに書き込みます。
    • r.Read(b.buf[b.n:])rからデータを読み込み、bufio.Writerの内部バッファの空きスペースに格納します。
    • 読み込んだバイト数mb.n(バッファ内のデータ量)とn(総転送バイト数)に加算します。
    • b.Flush()を呼び出して、バッファの内容を基盤となるWriterに書き込みます。これにより、bufio.Writerのバッファがクリアされ、次の読み込みのためのスペースが確保されます。
    • io.EOFエラーが発生した場合、それは正常なストリームの終了を示すため、errnilに設定して返します。その他のエラーはそのまま返されます。

この実装により、io.Copybufio.Writerを宛先として使用する場合、基盤となるWriterio.ReaderFromをサポートしていればその最適化されたパスを利用し、そうでなければbufio.Writer自身の効率的なバッファリングメカニズムを利用するという、柔軟かつ高性能なデータ転送が可能になります。

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

src/pkg/bufio/bufio.go

bufio.WriterReadFromメソッドが追加されました。

--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -567,6 +567,35 @@ func (b *Writer) WriteString(s string) (int, error) {
 	return nn, nil
 }
 
+// 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)
+	}
+	var m int
+	for {
+		m, err = r.Read(b.buf[b.n:])
+		if m == 0 {
+			break
+		}
+		b.n += m
+		n += int64(m)
+		if err1 := b.Flush(); err1 != nil {
+			return n, err1
+		}
+		if err != nil {
+			break
+		}
+	}
+	if err == io.EOF {
+		err = nil
+	}
+	return n, err
+}
+
 // buffered input and output
 
 // ReadWriter stores pointers to a Reader and a Writer.

src/pkg/bufio/bufio_test.go

TestWriterReadFrom関数が追加され、Writer.ReadFromの機能テストが行われています。また、createTestInputヘルパー関数が追加され、TestReaderWriteToonlyReaderを使用するように変更されています。 さらに、errorReaderFromTest構造体とTestWriterReadFromErrors関数が追加され、エラーハンドリングのテストが行われています。 最後に、BenchmarkWriterCopyOptimal, BenchmarkWriterCopyUnoptimal, BenchmarkWriterCopyNoReadFromという3つの新しいベンチマークが追加され、Writer.ReadFromのパフォーマンスが測定されています。

--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -763,8 +763,8 @@ func testReadLineNewlines(t *testing.T, input string, expect []readLineResult) {
 	}\n"
 }
 
-func TestReaderWriteTo(t *testing.T) {
-	input := make([]byte, 8192)
+func createTestInput(n int) []byte {
+	input := make([]byte, n)
 	for i := range input {
 		// 101 and 251 are arbitrary prime numbers.
 		// The idea is to create an input sequence
@@ -774,7 +774,12 @@ func TestReaderWriteTo(t *testing.T) {
 			input[i] ^= byte(i / 101)
 		}
 	}
-	r := NewReader(bytes.NewBuffer(input))
+	return input
+}
+
+func TestReaderWriteTo(t *testing.T) {
+	input := createTestInput(8192)
+	r := NewReader(&onlyReader{bytes.NewBuffer(input)})
 	w := new(bytes.Buffer)
 	if n, err := r.WriteTo(w); err != nil || n != int64(len(input)) {
 		t.Fatalf("r.WriteTo(w) = %d, %v, want %d, nil", n, err, len(input))
@@ -817,6 +822,65 @@ func TestReaderWriteToErrors(t *testing.T) {
 	}
 }
 
+func TestWriterReadFrom(t *testing.T) {
+	ws := []func(io.Writer) io.Writer{
+		func(w io.Writer) io.Writer { return &onlyWriter{w} },
+		func(w io.Writer) io.Writer { return w },
+	}
+
+	rs := []func(io.Reader) io.Reader{
+		iotest.DataErrReader,
+		func(r io.Reader) io.Reader { return r },
+	}
+
+	for ri, rfunc := range rs {
+		for wi, wfunc := range ws {
+			input := createTestInput(8192)
+			b := new(bytes.Buffer)
+			w := NewWriter(wfunc(b))
+			r := rfunc(bytes.NewBuffer(input))
+			if n, err := w.ReadFrom(r); err != nil || n != int64(len(input)) {
+				t.Errorf("ws[%d],rs[%d]: w.ReadFrom(r) = %d, %v, want %d, nil", wi, ri, n, err, len(input))
+				continue
+			}
+			if got, want := b.String(), string(input); got != want {
+				t.Errorf("ws[%d], rs[%d]:\ngot  %q\nwant %q\n", wi, ri, got, want)
+			}
+		}
+	}
+}
+
+type errorReaderFromTest struct {
+	rn, wn     int
+	rerr, werr error
+	expected   error
+}
+
+func (r errorReaderFromTest) Read(p []byte) (int, error) {
+	return len(p) * r.rn, r.rerr
+}
+
+func (w errorReaderFromTest) Write(p []byte) (int, error) {
+	return len(p) * w.wn, w.werr
+}
+
+var errorReaderFromTests = []errorReaderFromTest{
+	{0, 1, io.EOF, nil, nil},
+	{1, 1, io.EOF, nil, nil},
+	{0, 1, io.ErrClosedPipe, nil, io.ErrClosedPipe},
+	{0, 0, io.ErrClosedPipe, io.ErrShortWrite, io.ErrClosedPipe},
+	{1, 0, nil, io.ErrShortWrite, io.ErrShortWrite},
+}
+
+func TestWriterReadFromErrors(t *testing.T) {
+	for i, rw := range errorReaderFromTests {
+		w := NewWriter(rw)
+		if _, err := w.ReadFrom(rw); err != rw.expected {
+			t.Errorf("w.ReadFrom(errorReaderFromTests[%d]) = _, %v, want _,%v", i, err, rw.expected)
+		}
+	}
+}
+
 // 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@@ -866,3 +930,34 @@ func BenchmarkReaderCopyNoWriteTo(b *testing.B) {\n \t\tio.Copy(dst, src)\n \t}\n }\n+\n+func BenchmarkWriterCopyOptimal(b *testing.B) {\n+\t// Optimal case is where the underlying writer implements io.ReaderFrom\n+\tfor i := 0; i < b.N; i++ {\n+\t\tb.StopTimer()\n+\t\tsrc := &onlyReader{bytes.NewBuffer(make([]byte, 8192))}\n+\t\tdst := NewWriter(new(bytes.Buffer))\n+\t\tb.StartTimer()\n+\t\tio.Copy(dst, src)\n+\t}\n+}\n+\n+func BenchmarkWriterCopyUnoptimal(b *testing.B) {\n+\tfor i := 0; i < b.N; i++ {\n+\t\tb.StopTimer()\n+\t\tsrc := &onlyReader{bytes.NewBuffer(make([]byte, 8192))}\n+\t\tdst := NewWriter(&onlyWriter{new(bytes.Buffer)})\n+\t\tb.StartTimer()\n+\t\tio.Copy(dst, src)\n+\t}\n+}\n+\n+func BenchmarkWriterCopyNoReadFrom(b *testing.B) {\n+\tfor i := 0; i < b.N; i++ {\n+\t\tb.StopTimer()\n+\t\tsrc := &onlyReader{bytes.NewBuffer(make([]byte, 8192))}\n+\t\tdst := &onlyWriter{NewWriter(new(bytes.Buffer))}\n+\t\tb.StartTimer()\n+\t\tio.Copy(dst, src)\n+\t}\n+}\n```

## コアとなるコードの解説

### `bufio.go` の `ReadFrom` メソッド

```go
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)
	}
	var m int
	for {
		m, err = r.Read(b.buf[b.n:])
		if m == 0 {
			break
		}
		b.n += m
		n += int64(m)
		if err1 := b.Flush(); err1 != nil {
			return n, err1
		}
		if err != nil {
			break
		}
	}
	if err == io.EOF {
		err = nil
	}
	return n, err
}
  • if err = b.Flush(); err != nil: ReadFromが開始される前に、bufio.Writerの内部バッファに蓄積されているデータを強制的に基盤となるWriterに書き出します。これにより、ReadFromが読み込む新しいデータと既存のデータが混ざることを防ぎます。
  • if w, ok := b.wr.(io.ReaderFrom); ok: bufio.Writerがラップしている基盤となるio.Writerb.wr)が、自身もio.ReaderFromインターフェースを実装しているかどうかをチェックします。
    • もし実装していれば、return w.ReadFrom(r)によって、bufio.Writerは自身のバッファリングロジックを介さずに、直接基盤となるWriterReadFromメソッドに処理を委譲します。これは、基盤となるWriterがより効率的なデータ転送メカニズム(例えば、OSレベルの最適化されたコピー操作など)を提供できる場合に特に有効です。
  • for { ... }: 基盤となるWriterio.ReaderFromを実装していない場合のフォールバックロジックです。
    • m, err = r.Read(b.buf[b.n:]): r(ソースReader)からbufio.Writerの内部バッファの空いている部分にデータを読み込みます。
    • if m == 0 { break }: 読み込むデータがなくなったらループを終了します。
    • b.n += m: 読み込んだバイト数mを内部バッファの現在データ量b.nに加算します。
    • n += int64(m): 総転送バイト数nを更新します。
    • if err1 := b.Flush(); err1 != nil: データを読み込んだ後、バッファの内容を基盤となるWriterにフラッシュします。これにより、バッファが満杯になるたびにデータが書き込まれ、メモリ使用量を抑えつつ効率的な書き込みが行われます。
    • if err != nil { break }: 読み込み中にエラーが発生した場合、ループを終了します。
  • if err == io.EOF { err = nil }: io.EOFは正常なストリームの終了を示すため、エラーをnilに変換して返します。これにより、呼び出し元はデータがすべて転送されたことを正常な終了として扱えます。

bufio_test.go のテストとベンチマーク

  • createTestInput: テスト用のランダムなバイトスライスを生成するヘルパー関数です。
  • TestWriterReadFrom: bufio.WriterReadFromメソッドが正しく動作するかを検証する機能テストです。様々なio.Writerio.Readerの組み合わせ(例えば、onlyWriteriotest.DataErrReaderなど)でテストを行い、データが正しく転送されることを確認します。
  • errorReaderFromTestTestWriterReadFromErrors: ReadFromメソッドのエラーハンドリングが適切に行われるかを検証するテストです。ReadWrite操作で特定のエラーを返すカスタムのReaderFrom実装を使用し、期待されるエラーが返されることを確認します。
  • ベンチマーク:
    • BenchmarkWriterCopyOptimal: 基盤となるWriterio.ReaderFromを実装している場合のio.Copyのパフォーマンスを測定します。このシナリオでは、bufio.Writerは基盤となるWriterReadFromメソッドに処理を委譲するため、最も効率的なパスが利用されます。
    • BenchmarkWriterCopyUnoptimal: 基盤となるWriterio.ReaderFromを実装していないが、bufio.Writerがバッファリングを行う場合のio.Copyのパフォーマンスを測定します。この場合、bufio.Writerは自身のフォールバックロジックを使用してデータを転送します。
    • BenchmarkWriterCopyNoReadFrom: io.Copyio.ReaderFromの最適化パスを全く利用できないように、bufio.WriteronlyWriterでラップした場合のパフォーマンスを測定します。これは、このコミットによる最適化がない場合のベースライン性能を示します。

これらのベンチマークは、io.ReaderFromの実装がio.Copyのパフォーマンスに与える影響を明確に示しており、特に「Optimal」ケースでの大幅な改善が確認できます。

関連リンク

参考にした情報源リンク