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

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

このコミットは、Go言語の標準ライブラリioパッケージにおけるCopy関数の内部的な動作を変更し、io.WriterToインターフェースの実装がio.ReaderFromインターフェースの実装よりも優先されるように修正したものです。これにより、特定の条件下でのio.Copyのパフォーマンスが向上し、メモリ割り当ての効率化が図られます。

コミット

  • コミットハッシュ: fdc4ce6ec790b1a0507c3c2ef20e94aca4876a1b
  • 作者: Daniel Morsing daniel.morsing@gmail.com
  • コミット日時: 2013年5月23日(木) 18:29:19 +0200

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

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

元コミット内容

io: Prioritize WriterTos over ReaderFroms in Copy.

This only affects calls where both ReaderFrom and WriterTo are implemented. WriterTo can issue one large write, while ReaderFrom must Read until EOF, potentially reallocating when out of memory. With one large Write, the Writer only needs to allocate once.

This also helps in ioutil.Discard since we can avoid copying memory when the Reader implements WriterTo.

R=golang-dev, dsymonds, remyoudompheng, bradfitz
CC=golang-dev, minux.ma
https://golang.org/cl/9462044

変更の背景

io.Copy関数は、io.Readerからio.Writerへデータをコピーするための汎用的な関数です。この関数は、効率的なコピーを実現するために、io.ReaderFromio.WriterToという特別なインターフェースの存在をチェックし、それらが実装されている場合には、より最適化されたパスを使用します。

変更前のio.Copyの実装では、dst(コピー先)がio.ReaderFromを実装しているかどうかを最初にチェックし、次にsrc(コピー元)がio.WriterToを実装しているかどうかをチェックしていました。しかし、この順序にはパフォーマンス上の問題がありました。

コミットメッセージによると、io.WriterToは「一度に大きな書き込みを発行できる」のに対し、io.ReaderFromは「EOFまで読み込みを続けなければならず、メモリ不足の場合には再割り当てが発生する可能性がある」と指摘されています。つまり、WriterToを使用する方が、通常はメモリ割り当ての回数を減らし、より大きなチャンクでデータを転送できるため、効率的であると考えられます。

このコミットは、io.ReaderFromio.WriterToの両方が実装されている場合に、より効率的なWriterToのパスを優先することで、io.Copyのパフォーマンスを向上させることを目的としています。特に、ioutil.Discardのようなケースでは、ReaderWriterToを実装している場合、メモリコピーを完全に回避できるという利点があります。

前提知識の解説

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

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

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

    Readメソッドは、データをpに読み込み、読み込んだバイト数nとエラーerrを返します。nが0でerrio.EOFの場合、読み込みは終了です。

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

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

    Writeメソッドは、pのデータを書き込み、書き込んだバイト数nとエラーerrを返します。

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

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

    ReadFromメソッドは、rからEOFになるまでデータを読み込み、そのデータを自身に書き込みます。これは、io.Writerio.Readerから直接データを読み込むための最適化された方法を提供します。例えば、bytes.BufferReadFromを実装しており、他のReaderから効率的にデータを読み込むことができます。

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

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

    WriteToメソッドは、自身のデータをwに書き込みます。これは、io.Readerが自身のデータをio.Writerに直接書き込むための最適化された方法を提供します。例えば、bytes.BufferWriterToを実装しており、自身の内容を他のWriterに効率的に書き込むことができます。

  • io.Copy関数:

    func Copy(dst Writer, src Reader) (written int64, err error)
    

    Copy関数は、srcからdstへデータをコピーします。内部的には、srcdstReaderFromWriterToインターフェースを実装しているかどうかをチェックし、実装している場合はそれらのメソッドを呼び出すことで、より効率的なコピーを行います。どちらも実装していない場合は、内部バッファ(通常は32KB)を使用してReadWriteを繰り返し呼び出すことでコピーを行います。

技術的詳細

このコミットの核心は、io.Copy関数がio.ReaderFromio.WriterToのどちらの最適化パスを優先するかという点にあります。

  • io.ReaderFromの動作: dst.ReadFrom(src)が呼び出される場合、dstsrcからデータを読み取ります。このプロセスは、srcがEOFを返すまでsrc.Read()を繰り返し呼び出すことによって行われます。もしdstが内部バッファを持っていて、それが不足した場合、dstはバッファの再割り当てを行う可能性があります。これにより、複数のメモリ割り当てとコピー操作が発生する可能性があります。

  • io.WriterToの動作: src.WriteTo(dst)が呼び出される場合、srcは自身の内部データをdstに書き込みます。srcWriterToを実装している場合、通常、srcは自身の内部バッファ全体を一度にdstに書き込むことができます。これにより、dst側でのバッファの再割り当てや、小さなチャンクでの複数回の書き込みを避けることができ、効率的なデータ転送が可能になります。

コミットの変更は、この2つのインターフェースが両方とも利用可能な場合に、WriterToのパスを優先するようにio.Copyのロジックを修正しました。これは、WriterToが提供する「一度に大きな書き込み」の能力が、ReaderFromの「EOFまで読み込み、再割り当ての可能性」よりも一般的にパフォーマンス上有利であるという判断に基づいています。

具体的な例として、bytes.Bufferio.Readerio.Writerio.ReaderFromio.WriterToのすべてを実装しています。 変更前: io.Copy(dst *bytes.Buffer, src *bytes.Buffer)の場合、dstReaderFromを実装しているため、dst.ReadFrom(src)が呼び出されていました。 変更後: io.Copy(dst *bytes.Buffer, src *bytes.Buffer)の場合、srcWriterToを実装しているため、src.WriteTo(dst)が呼び出されるようになります。

この変更は、特にioutil.Discardのようなケースで顕著な効果を発揮します。ioutil.Discardio.Writerインターフェースを実装しており、書き込まれたデータをすべて破棄します。もしsrcio.WriterToを実装している場合、src.WriteTo(ioutil.Discard)が呼び出され、srcは自身のデータをDiscardに直接書き込もうとします。Discardはデータを破棄するだけなので、実際のメモリコピーは発生せず、非常に効率的になります。変更前は、ioutil.DiscardReaderFromを実装していないため、汎用的なバッファコピーパスが使用され、不要なメモリコピーが発生する可能性がありました。

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

このコミットでは、主にsrc/pkg/io/io.goCopy関数のロジックと、その動作を検証するためのテストケースがsrc/pkg/io/io_test.goに追加されています。

src/pkg/io/io.goの変更点:

Copy関数の冒頭部分の条件分岐が変更されました。

変更前:

func Copy(dst Writer, src Reader) (written int64, err error) {
	// If the writer has a ReadFrom method, use it to do the copy.
	// Avoids an allocation and a copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	// Similarly, if the reader has a WriteTo method, use it to do the copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	buf := make([]byte, 32*1024)
	// ... (rest of the function)
}

変更後:

func Copy(dst Writer, src Reader) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	buf := make([]byte, 32*1024)
	// ... (rest of the function)
}

src/pkg/io/io_test.goの変更点:

TestCopyPriorityという新しいテスト関数が追加されました。このテストは、WriterToReaderFromよりも優先されることを検証します。

// Version of bytes.Buffer that checks whether WriteTo was called or not
type writeToChecker struct {
	bytes.Buffer
	writeToCalled bool
}

func (wt *writeToChecker) WriteTo(w Writer) (int64, error) {
	wt.writeToCalled = true
	return wt.Buffer.WriteTo(w)
}

// It's preferable to choose WriterTo over ReaderFrom, since a WriterTo can issue one large write,
// while the ReaderFrom must read until EOF, potentially allocating when running out of buffer.
// Make sure that we choose WriterTo when both are implemented.
func TestCopyPriority(t *testing.T) {
	rb := new(writeToChecker)
	wb := new(bytes.Buffer)
	rb.WriteString("hello, world.")
	Copy(wb, rb)
	if wb.String() != "hello, world." {
		t.Errorf("Copy did not work properly")
	} else if !rb.writeToCalled {
		t.Errorf("WriteTo was not prioritized over ReadFrom")
	}
}

コアとなるコードの解説

src/pkg/io/io.goCopy関数では、dstsrcがそれぞれio.ReaderFromio.WriterToインターフェースを実装しているかどうかを型アサーション(if _, ok := ...; ok)でチェックしています。

変更前は、まずdstReaderFromを実装しているかを確認し、次にsrcWriterToを実装しているかを確認していました。 変更後は、この順序が逆転し、まずsrcWriterToを実装しているかを確認し、次にdstReaderFromを実装しているかを確認するようになりました。

この変更により、srcdstの両方がそれぞれの最適化インターフェースを実装している場合(例: bytes.Bufferからbytes.Bufferへのコピー)、src.WriteTo(dst)が呼び出されるようになります。これにより、srcが自身の内部データを効率的にdstに転送できるため、全体的なコピー操作の効率が向上します。

src/pkg/io/io_test.goに追加されたTestCopyPriorityテストは、この新しい優先順位付けが正しく機能することを確認します。 writeToCheckerというカスタム型は、bytes.Bufferを埋め込み、WriteToメソッドが呼び出されたかどうかを追跡するためのwriteToCalledフィールドを追加しています。 テストでは、writeToCheckerのインスタンスrb(Reader Buffer)とbytes.Bufferのインスタンスwb(Writer Buffer)を作成します。 rbに文字列を書き込んだ後、io.Copy(wb, rb)を呼び出します。 wbReaderFromを実装しており、rbWriterToを実装しています。 テストの目的は、io.CopyrbWriteToメソッドを呼び出すことを確認することです。もしrb.writeToCalledfalseのままだった場合、それはWriterToが優先されなかったことを意味し、テストは失敗します。このテストが成功することで、WriterToの優先順位付けが正しく行われていることが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語のioパッケージのソースコード
  • Go言語のbytesパッケージのソースコード
  • コミットメッセージと関連するレビューコメント
  • Go言語のインターフェースに関する一般的な知識