[インデックス 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言語のインターフェースに関する一般的な知識