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

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

このコミットは、Go言語の標準ライブラリである bufio パッケージにおける Reader.WriteTo メソッドのパフォーマンス改善を目的としています。具体的には、io.Writer インターフェースを実装するターゲットが io.ReaderFrom インターフェースも実装している場合に、その ReaderFrom メソッドを優先的に利用することで、データのコピー効率を大幅に向上させています。これにより、特に大きなデータを扱う際のI/O処理のオーバーヘッドが削減され、全体的なスループットが改善されます。

コミット

commit b00e4770d962289ef1a542f153408d55e3307167
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Feb 27 10:48:36 2014 -0800

    bufio: in Reader.WriteTo, try to use target's ReaderFrom
    
    This is the simple half of https://golang.org/cl/53560043/ with
    a new benchmark. pongad is in the C+A files already.
    
    benchmark                         old ns/op     new ns/op     delta
    BenchmarkReaderWriteToOptimal     2054          825           -59.83%
    
    Update #6373
    
    LGTM=iant, gri
    R=golang-codereviews, iant, gri
    CC=golang-codereviews
    https://golang.org/cl/69220046

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

https://github.com/golang/go/commit/b00e4770d962289ef1a542f153408d55e3307167

元コミット内容

bufio: in Reader.WriteTo, try to use target's ReaderFrom

このコミットは、bufio パッケージの Reader.WriteTo メソッドにおいて、書き込み先 (io.Writer) が io.ReaderFrom インターフェースを実装している場合に、その ReaderFrom メソッドを利用するように変更するものです。これにより、データのコピー処理が最適化され、パフォーマンスが向上します。コミットメッセージには、新しいベンチマーク BenchmarkReaderWriteToOptimal の結果が示されており、約60%の性能改善が達成されたことが報告されています。また、関連する変更リスト (CL) へのリンクと、Issue #6373 の更新であることが明記されています。

変更の背景

この変更の背景には、Go言語のI/O操作における効率性の追求があります。io.Copyio.WriterToio.ReaderFrom といったインターフェースは、データのコピー処理を最適化するためのメカニズムを提供します。

従来の Reader.WriteTo メソッドは、内部バッファリングされたデータを io.Writer に書き出す際に、writeBuf メソッドを繰り返し呼び出すことで処理を行っていました。これは一般的なケースでは問題ありませんが、書き込み先が io.ReaderFrom インターフェースを実装している場合、より効率的なデータ転送方法が存在します。

io.ReaderFrom インターフェースは、自身が io.Reader からデータを直接読み込む能力を持つことを示します。例えば、bytes.Buffernet.Conn など、多くの標準ライブラリの型がこのインターフェースを実装しています。これらの型は、内部的に最適化された方法(例えば、システムコールレベルでのゼロコピー転送など)でデータを読み込むことができるため、Reader.WriteToio.ReaderFrom を利用することで、中間バッファリングや余分なデータコピーを削減し、パフォーマンスを大幅に向上させることが可能になります。

このコミットは、Issue #6373 で議論されていたパフォーマンス改善の一環であり、特に大きなデータをストリーム処理するようなシナリオにおいて、bufio.Reader からのデータ転送効率を高めることを目的としています。

前提知識の解説

このコミットを理解するためには、Go言語の以下のI/O関連インターフェースと概念を理解しておく必要があります。

  1. io.Reader インターフェース:

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    

    Read メソッドは、データを p に読み込み、読み込んだバイト数 n とエラーを返します。これは、データソースからの読み込み操作を抽象化する最も基本的なインターフェースです。

  2. io.Writer インターフェース:

    type Writer interface {
        Write(p []byte) (n int, err error)
    }
    

    Write メソッドは、p のデータを書き込み、書き込んだバイト数 n とエラーを返します。これは、データシンクへの書き込み操作を抽象化する最も基本的なインターフェースです。

  3. io.WriterTo インターフェース:

    type WriterTo interface {
        WriteTo(w Writer) (n int64, err error)
    }
    

    WriteTo メソッドは、自身のデータを w に書き込みます。このインターフェースを実装する型は、自身のデータを効率的に他の io.Writer に転送する方法を提供します。bufio.Reader はこのインターフェースを実装しています。

  4. io.ReaderFrom インターフェース:

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

    ReadFrom メソッドは、r からデータを読み込み、自身に書き込みます。このインターフェースを実装する型は、他の io.Reader からデータを効率的に受け取る方法を提供します。例えば、bytes.Buffernet.Conn などがこれを実装しており、内部的に最適化されたコピーメカニズムを利用できる場合があります。

  5. bufio パッケージ: bufio パッケージは、I/O操作をバッファリングすることで効率化するための機能を提供します。bufio.Reader は、基となる io.Reader からデータを読み込む際に内部バッファを使用し、読み込み回数を減らすことでパフォーマンスを向上させます。同様に、bufio.Writer は書き込み操作をバッファリングします。

このコミットの核心は、bufio.ReaderWriteTo メソッドが、書き込み先の io.Writerio.ReaderFrom も実装している場合に、その io.ReaderFrom の能力を「検出」し、利用するという点です。これにより、bufio.Reader がバッファリングしたデータを io.Writer に一つずつ書き出すのではなく、io.Writerbufio.Reader から直接、かつ効率的にデータを読み込むという、より最適なデータフローが実現されます。

技術的詳細

このコミットの技術的詳細は、bufio.ReaderWriteTo メソッドにおける型アサーションとインターフェースの活用にあります。

bufio.ReaderWriteTo メソッドは、io.Writer インターフェースを受け取ります。このメソッドの目的は、bufio.Reader がバッファリングしているデータ、および基となる io.Reader から読み込んだ残りのデータを、引数として渡された io.Writer にすべて書き出すことです。

変更前の実装では、WriteTo はまず b.fill() を呼び出して内部バッファを埋め、その後 b.r < b.w (読み込みポインタが書き込みポインタより小さい、つまりバッファにデータがある) の間、b.writeBuf(w) を繰り返し呼び出してバッファの内容を w に書き出していました。バッファが空になったら再度 b.fill() を呼び出し、基となるリーダーからデータを読み込んでバッファを埋める、というサイクルを繰り返していました。

このコミットによる変更は、このループに入る前に、渡された io.Writer (w) が io.ReaderFrom インターフェースも実装しているかどうかをチェックするロジックを追加しています。

if w, ok := w.(io.ReaderFrom); ok {
    m, err := w.ReadFrom(b.rd)
    n += m
    return n, err
}

このコードスニペットは以下のことを行っています。

  1. w, ok := w.(io.ReaderFrom): これは型アサーションです。wio.ReaderFrom インターフェースを実装している場合、oktrue になり、wio.ReaderFrom 型として変数 w (シャドーイングされている) に代入されます。
  2. if w, ok := w.(io.ReaderFrom); ok: もし wio.ReaderFrom を実装していれば、この if ブロックが実行されます。
  3. m, err := w.ReadFrom(b.rd): ここが最適化の核心です。io.ReaderFrom を実装している wReadFrom メソッドを呼び出し、bufio.Reader の基となる io.Reader (b.rd) を引数として渡します。これにより、wb.rd から直接、かつ自身が最も効率的だと考える方法でデータを読み込むことができます。例えば、net.ConnReadFrom は、OSの sendfile のようなゼロコピーメカニズムを利用できる場合があります。
  4. n += m: 読み込んだバイト数を合計に加算します。
  5. return n, err: ReadFrom が完了したら、すぐにメソッドを終了します。これにより、従来のバイト単位のコピーループがスキップされ、大幅なパフォーマンス向上が期待できます。

この変更は、Go言語のインターフェースの強力な特徴である「ダックタイピング」を効果的に利用しています。特定のインターフェースを実装しているかどうかをランタイムでチェックし、それに応じて異なる(より最適化された)コードパスを選択することで、柔軟性とパフォーマンスの両立を実現しています。

ベンチマーク BenchmarkReaderWriteToOptimal は、この最適化の効果を測定するために追加されました。このベンチマークでは、bufio.Reader から ioutil.Discard (これは io.ReaderFrom を実装している) へデータを書き込むシナリオをシミュレートしています。結果として、old ns/op (ナノ秒/操作) が2054だったのに対し、new ns/op が825となり、約60%の性能改善が確認されています。これは、io.ReaderFrom を利用した直接的なデータ転送が、従来のバッファリングとコピーの繰り返しよりもはるかに効率的であることを明確に示しています。

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

このコミットにおけるコアとなるコードの変更箇所は、以下の2つのファイルに集中しています。

  1. src/pkg/bufio/bufio.go: Reader.WriteTo メソッドに io.ReaderFrom の検出と利用ロジックが追加されました。
  2. src/pkg/bufio/bufio_test.go: 新しいベンチマーク BenchmarkReaderWriteToOptimal が追加され、既存のヘルパー型 onlyReaderonlyWriter の定義が簡略化されました。

src/pkg/bufio/bufio.go の変更点

--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -410,6 +410,12 @@ func (b *Reader) WriteTo(w io.Writer) (n int64, err error) {
 		return n, err
 	}
 
+	if w, ok := w.(io.ReaderFrom); ok {
+		m, err := w.ReadFrom(b.rd)
+		n += m
+		return n, err
+	}
+
 	for b.fill(); b.r < b.w; b.fill() {
 		m, err := b.writeBuf(w)
 		n += m

src/pkg/bufio/bufio_test.go の変更点

--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -1094,20 +1094,12 @@ func TestWriterReset(t *testing.T) {
 
 // An onlyReader only implements io.Reader, no matter what other methods the underlying implementation may have.
 type onlyReader struct {
-	r io.Reader
-}
-
-func (r onlyReader) Read(b []byte) (int, error) {
-	return r.r.Read(b)
+	io.Reader
 }
 
 // An onlyWriter only implements io.Writer, no matter what other methods the underlying implementation may have.
 type onlyWriter struct {
-	w io.Writer
-}
-
-func (w onlyWriter) Write(b []byte) (int, error) {
-	return w.w.Write(b)
+	io.Writer
 }
 
 func BenchmarkReaderCopyOptimal(b *testing.B) {
@@ -1152,6 +1144,27 @@ func BenchmarkReaderCopyNoWriteTo(b *testing.B) {
 	}
 }
 
+func BenchmarkReaderWriteToOptimal(b *testing.B) {
+	const bufSize = 16 << 10
+	buf := make([]byte, bufSize)
+	r := bytes.NewReader(buf)
+	srcReader := NewReaderSize(onlyReader{r}, 1<<10)
+	if _, ok := ioutil.Discard.(io.ReaderFrom); !ok {
+		b.Fatal("ioutil.Discard doesn't support ReaderFrom")
+	}
+	for i := 0; i < b.N; i++ {
+		r.Seek(0, 0)
+		srcReader.Reset(onlyReader{r})
+		n, err := srcReader.WriteTo(ioutil.Discard)
+		if err != nil {
+			b.Fatal(err)
+		}
+		if n != bufSize {
+			b.Fatalf("n = %d; want %d", n, bufSize)
+		}
+	}
+}
+
 func BenchmarkWriterCopyOptimal(b *testing.B) {
 	// Optimal case is where the underlying writer implements io.ReaderFrom
 	srcBuf := bytes.NewBuffer(make([]byte, 8192))

コアとなるコードの解説

src/pkg/bufio/bufio.go の変更解説

Reader.WriteTo メソッドの冒頭に、以下のコードが追加されました。

	if w, ok := w.(io.ReaderFrom); ok {
		m, err := w.ReadFrom(b.rd)
		n += m
		return n, err
	}

これは、WriteTo メソッドに渡された io.Writer インターフェース w が、実際には io.ReaderFrom インターフェースも実装しているかどうかをチェックしています。

  • w.(io.ReaderFrom): これは型アサーションです。もし wio.ReaderFrom を実装していれば、その具体的な値が io.ReaderFrom 型として返され、oktrue になります。
  • if w, ok := w.(io.ReaderFrom); ok: oktrue の場合、つまり書き込み先が io.ReaderFrom をサポートしている場合、このブロック内のコードが実行されます。
  • m, err := w.ReadFrom(b.rd): ここで、書き込み先 (w) の ReadFrom メソッドが呼び出されます。引数には bufio.Reader の基となる io.Reader (b.rd) が渡されます。これにより、書き込み先は b.rd から直接データを読み込むことができ、bufio.Reader が持つバッファリングの恩恵を受けつつ、さらに書き込み先が提供する最適化されたデータ転送メカニズム(例: sendfile システムコールなど)を利用することが可能になります。
  • n += m: ReadFrom が返した読み込みバイト数 m を、これまでに書き込んだバイト数の合計 n に加算します。
  • return n, err: ReadFrom が完了した時点で、WriteTo メソッドは処理を終了します。これにより、その後の従来のバイト単位のコピー処理(for b.fill(); b.r < b.w; b.fill() { ... } ループ)は実行されず、大幅な効率化が図られます。

この変更により、bufio.Reader は、書き込み先がより効率的なデータ転送方法を提供している場合に、それを自動的に検出し、利用するようになりました。これは、Go言語のインターフェースの柔軟性と、それを利用したパフォーマンス最適化の典型的な例です。

src/pkg/bufio/bufio_test.go の変更解説

  1. onlyReader および onlyWriter 型の簡略化: 変更前は、これらのヘルパー型は内部に io.Reader または io.Writer をフィールドとして持ち、そのフィールドのメソッドを呼び出す形式でした。

    type onlyReader struct {
    	r io.Reader
    }
    func (r onlyReader) Read(b []byte) (int, error) {
    	return r.r.Read(b)
    }
    

    変更後は、匿名フィールドとして io.Reader または io.Writer を埋め込む形になりました。

    type onlyReader struct {
    	io.Reader
    }
    

    これにより、onlyReaderio.Reader のメソッドを自動的に「昇格」させ、より簡潔な記述が可能になります。機能的には同じですが、コードの可読性と保守性が向上します。

  2. BenchmarkReaderWriteToOptimal の追加: この新しいベンチマークは、Reader.WriteTo メソッドが io.ReaderFrom を実装するターゲットに書き込む際のパフォーマンスを測定するために追加されました。

    • bufSize = 16 << 10 (16KB) のバッファを作成し、bytes.NewReader(buf)io.Reader を作成します。
    • srcReader := NewReaderSize(onlyReader{r}, 1<<10): bufio.Reader を作成します。onlyReader を使用することで、基となるリーダーが io.Reader 以外のインターフェース(例: io.Seeker など)を実装していても、bufio.Reader がそれらのメソッドを直接利用しないように制限しています。
    • if _, ok := ioutil.Discard.(io.ReaderFrom); !ok { b.Fatal(...) }: ioutil.Discardio.ReaderFrom を実装していることを確認しています。ioutil.Discard は書き込まれたデータをすべて破棄する特殊な io.Writer であり、通常は非常に高速な書き込みが可能です。また、io.ReaderFrom を実装しているため、このベンチマークの目的(ReaderFrom を利用した最適化の測定)に非常に適しています。
    • ループ内で、r.Seek(0, 0) でリーダーをリセットし、srcReader.Reset(onlyReader{r})bufio.Reader の状態をリセットします。
    • n, err := srcReader.WriteTo(ioutil.Discard): 実際に bufio.Reader から ioutil.Discard へデータを書き込み、そのパフォーマンスを測定します。
    • 結果の n が期待される bufSize と一致するかどうかを確認し、エラーがないことを検証します。

このベンチマークの追加により、Reader.WriteToio.ReaderFrom を利用した最適化が実際にどれだけの効果をもたらすかを定量的に評価できるようになりました。コミットメッセージに示されたベンチマーク結果は、この最適化が非常に効果的であることを裏付けています。

関連リンク

  • Go Issue #6373: io: add ReaderFrom to io.Writer このコミットが解決しようとしている問題の議論が行われたIssueです。io.Writerio.ReaderFrom を追加するという提案がなされていますが、最終的には io.WriterToio.ReaderFrom の組み合わせで同様の最適化を実現する方向になりました。 https://github.com/golang/go/issues/6373

  • Go Change-list 53560043: io: add ReaderFrom to io.Writer (関連する初期の提案) このコミットメッセージで言及されている「simple half of https://golang.org/cl/53560043/」です。これは、io.Writerio.ReaderFrom を追加するという初期の提案に関連する変更リストですが、最終的には現在の io.ReaderFrom インターフェースの形に落ち着きました。 https://golang.org/cl/53560043/

  • Go Change-list 69220046: bufio: in Reader.WriteTo, try to use target's ReaderFrom (このコミットの変更リスト) このコミット自体の変更リストです。 https://golang.org/cl/69220046

参考にした情報源リンク