[インデックス 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.ReaderFrom
とio.WriterTo
という特別なインターフェースの存在をチェックし、それらが実装されている場合には、より最適化されたパスを使用します。
変更前のio.Copy
の実装では、dst
(コピー先)がio.ReaderFrom
を実装しているかどうかを最初にチェックし、次にsrc
(コピー元)がio.WriterTo
を実装しているかどうかをチェックしていました。しかし、この順序にはパフォーマンス上の問題がありました。
コミットメッセージによると、io.WriterTo
は「一度に大きな書き込みを発行できる」のに対し、io.ReaderFrom
は「EOFまで読み込みを続けなければならず、メモリ不足の場合には再割り当てが発生する可能性がある」と指摘されています。つまり、WriterTo
を使用する方が、通常はメモリ割り当ての回数を減らし、より大きなチャンクでデータを転送できるため、効率的であると考えられます。
このコミットは、io.ReaderFrom
とio.WriterTo
の両方が実装されている場合に、より効率的なWriterTo
のパスを優先することで、io.Copy
のパフォーマンスを向上させることを目的としています。特に、ioutil.Discard
のようなケースでは、Reader
がWriterTo
を実装している場合、メモリコピーを完全に回避できるという利点があります。
前提知識の解説
このコミットを理解するためには、Go言語の以下の基本的なI/Oインターフェースと概念を理解しておく必要があります。
-
io.Reader
インターフェース:type Reader interface { Read(p []byte) (n int, err error) }
Read
メソッドは、データをp
に読み込み、読み込んだバイト数n
とエラーerr
を返します。n
が0でerr
がio.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.Writer
がio.Reader
から直接データを読み込むための最適化された方法を提供します。例えば、bytes.Buffer
はReadFrom
を実装しており、他のReader
から効率的にデータを読み込むことができます。 -
io.WriterTo
インターフェース:type WriterTo interface { WriteTo(w Writer) (n int64, err error) }
WriteTo
メソッドは、自身のデータをw
に書き込みます。これは、io.Reader
が自身のデータをio.Writer
に直接書き込むための最適化された方法を提供します。例えば、bytes.Buffer
はWriterTo
を実装しており、自身の内容を他のWriter
に効率的に書き込むことができます。 -
io.Copy
関数:func Copy(dst Writer, src Reader) (written int64, err error)
Copy
関数は、src
からdst
へデータをコピーします。内部的には、src
とdst
がReaderFrom
やWriterTo
インターフェースを実装しているかどうかをチェックし、実装している場合はそれらのメソッドを呼び出すことで、より効率的なコピーを行います。どちらも実装していない場合は、内部バッファ(通常は32KB)を使用してRead
とWrite
を繰り返し呼び出すことでコピーを行います。
技術的詳細
このコミットの核心は、io.Copy
関数がio.ReaderFrom
とio.WriterTo
のどちらの最適化パスを優先するかという点にあります。
-
io.ReaderFrom
の動作:dst.ReadFrom(src)
が呼び出される場合、dst
はsrc
からデータを読み取ります。このプロセスは、src
がEOFを返すまでsrc.Read()
を繰り返し呼び出すことによって行われます。もしdst
が内部バッファを持っていて、それが不足した場合、dst
はバッファの再割り当てを行う可能性があります。これにより、複数のメモリ割り当てとコピー操作が発生する可能性があります。 -
io.WriterTo
の動作:src.WriteTo(dst)
が呼び出される場合、src
は自身の内部データをdst
に書き込みます。src
がWriterTo
を実装している場合、通常、src
は自身の内部バッファ全体を一度にdst
に書き込むことができます。これにより、dst
側でのバッファの再割り当てや、小さなチャンクでの複数回の書き込みを避けることができ、効率的なデータ転送が可能になります。
コミットの変更は、この2つのインターフェースが両方とも利用可能な場合に、WriterTo
のパスを優先するようにio.Copy
のロジックを修正しました。これは、WriterTo
が提供する「一度に大きな書き込み」の能力が、ReaderFrom
の「EOFまで読み込み、再割り当ての可能性」よりも一般的にパフォーマンス上有利であるという判断に基づいています。
具体的な例として、bytes.Buffer
はio.Reader
、io.Writer
、io.ReaderFrom
、io.WriterTo
のすべてを実装しています。
変更前: io.Copy(dst *bytes.Buffer, src *bytes.Buffer)
の場合、dst
がReaderFrom
を実装しているため、dst.ReadFrom(src)
が呼び出されていました。
変更後: io.Copy(dst *bytes.Buffer, src *bytes.Buffer)
の場合、src
がWriterTo
を実装しているため、src.WriteTo(dst)
が呼び出されるようになります。
この変更は、特にioutil.Discard
のようなケースで顕著な効果を発揮します。ioutil.Discard
はio.Writer
インターフェースを実装しており、書き込まれたデータをすべて破棄します。もしsrc
がio.WriterTo
を実装している場合、src.WriteTo(ioutil.Discard)
が呼び出され、src
は自身のデータをDiscard
に直接書き込もうとします。Discard
はデータを破棄するだけなので、実際のメモリコピーは発生せず、非常に効率的になります。変更前は、ioutil.Discard
がReaderFrom
を実装していないため、汎用的なバッファコピーパスが使用され、不要なメモリコピーが発生する可能性がありました。
コアとなるコードの変更箇所
このコミットでは、主にsrc/pkg/io/io.go
のCopy
関数のロジックと、その動作を検証するためのテストケースが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
という新しいテスト関数が追加されました。このテストは、WriterTo
がReaderFrom
よりも優先されることを検証します。
// 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.go
のCopy
関数では、dst
とsrc
がそれぞれio.ReaderFrom
とio.WriterTo
インターフェースを実装しているかどうかを型アサーション(if _, ok := ...; ok
)でチェックしています。
変更前は、まずdst
がReaderFrom
を実装しているかを確認し、次にsrc
がWriterTo
を実装しているかを確認していました。
変更後は、この順序が逆転し、まずsrc
がWriterTo
を実装しているかを確認し、次にdst
がReaderFrom
を実装しているかを確認するようになりました。
この変更により、src
とdst
の両方がそれぞれの最適化インターフェースを実装している場合(例: 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)
を呼び出します。
wb
はReaderFrom
を実装しており、rb
はWriterTo
を実装しています。
テストの目的は、io.Copy
がrb
のWriteTo
メソッドを呼び出すことを確認することです。もしrb.writeToCalled
がfalse
のままだった場合、それはWriterTo
が優先されなかったことを意味し、テストは失敗します。このテストが成功することで、WriterTo
の優先順位付けが正しく行われていることが保証されます。
関連リンク
- Go言語の公式ドキュメント: https://pkg.go.dev/io
- このコミットのGo Gerritレビューページ: https://golang.org/cl/9462044
参考にした情報源リンク
- Go言語の
io
パッケージのソースコード - Go言語の
bytes
パッケージのソースコード - コミットメッセージと関連するレビューコメント
- Go言語のインターフェースに関する一般的な知識