[インデックス 16334] ファイルの概要
このコミットは、Go言語の標準ライブラリ bufio
パッケージにおける Reader
のバッファ管理を改善し、一時的なバッファを共有することでメモリ使用量とパフォーマンスを最適化するものです。具体的には、bufio.Reader
がバッファリングされたデータを使い切った際に、そのバッファをプールに戻すメカニズムを導入しています。
コミット
commit b25a53acd71a254df54869ecbe76e44c35580ada
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri May 17 15:16:06 2013 -0700
bufio: make Reader buffer transient
Share garbage between different bufio Readers. When a Reader
has zero buffered data, put its buffer into a pool.
This acknowledges that most bufio.Readers eventually get
read to completion, and their buffers are then no longer
needed.
benchmark old ns/op new ns/op delta
BenchmarkReaderEmpty 2993 1058 -64.65%
benchmark old allocs new allocs delta
BenchmarkReaderEmpty 3 2 -33.33%
benchmark old bytes new bytes delta
BenchmarkReaderEmpty 4278 133 -96.89%
Update #5100
R=r
CC=adg, dvyukov, gobot, golang-dev, rogpeppe
https://golang.org/cl/8819049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b25a53acd71a254df54869ecbe76e44c35580ada
元コミット内容
bufio: make Reader buffer transient
異なる bufio.Reader
間でガベージ(不要になったバッファ)を共有します。Reader
がバッファリングされたデータを使い切った場合、そのバッファをプールに戻します。
これは、ほとんどの bufio.Reader
が最終的に読み取りを完了し、そのバッファが不要になるという事実を認識したものです。
ベンチマーク結果:
BenchmarkReaderEmpty
- 実行時間: 2993 ns/op -> 1058 ns/op (-64.65%)
- アロケーション数: 3 allocs -> 2 allocs (-33.33%)
- 割り当てバイト数: 4278 bytes -> 133 bytes (-96.89%)
Issue #5100 を更新。
変更の背景
この変更の背景には、bufio.Reader
が内部的に使用するバッファの効率的な管理という課題がありました。従来の bufio.Reader
は、一度バッファを確保すると、その Reader
インスタンスがガベージコレクションされるまでバッファを保持し続けました。しかし、多くのケースでは、Reader
は一度データを読み込み終えると、そのバッファはすぐに不要になります。
このような状況では、不要になったバッファがメモリ上に残り続け、ガベージコレクタの負担を増やしたり、メモリフットプリントを不必要に大きくしたりする可能性がありました。特に、短命な bufio.Reader
が多数生成されるようなシナリオでは、この問題が顕著になります。
このコミットは、このような問題を解決するために、使用済みバッファを再利用可能なプールに返すメカニズムを導入しました。これにより、メモリの再利用が促進され、ガベージコレクションの頻度と負荷が軽減され、結果としてアプリケーション全体のパフォーマンスが向上します。ベンチマーク結果が示すように、特にバッファが空になるケース(BenchmarkReaderEmpty
)において、劇的な改善が見られます。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
bufio
パッケージ: Go言語の標準ライブラリの一部で、I/O操作をバッファリングすることでパフォーマンスを向上させるための機能を提供します。bufio.Reader
はio.Reader
インターフェースをラップし、内部バッファを使用して効率的な読み取りを可能にします。- バッファリング: データを一度にまとめて読み書きすることで、システムコール(ディスクI/OやネットワークI/Oなど)の回数を減らし、I/O効率を高める手法です。
bufio.Reader
は、指定されたサイズのバイトスライスを内部バッファとして持ち、そこからデータを読み出します。 io.Reader
インターフェース: Go言語における基本的な読み取りインターフェースで、Read(p []byte) (n int, err error)
メソッドを持ちます。
- バッファリング: データを一度にまとめて読み書きすることで、システムコール(ディスクI/OやネットワークI/Oなど)の回数を減らし、I/O効率を高める手法です。
-
ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはやどの変数からも参照されなくなった領域を自動的に解放する仕組みです。Go言語は自動ガベージコレクションを採用しており、開発者が手動でメモリを解放する必要はありません。しかし、GCの実行にはコストがかかり、頻繁なメモリ確保・解放はGCの負荷を増大させ、アプリケーションのレイテンシやスループットに影響を与える可能性があります。
-
バッファプール (Buffer Pool): 頻繁に確保・解放される可能性のあるオブジェクト(この場合はバイトスライス)を再利用するために、それらを一時的に保持しておく仕組みです。オブジェクトが必要になったときにプールから取得し、不要になったときにプールに戻すことで、メモリの確保・解放のオーバーヘッドを削減し、ガベージコレクションの負担を軽減します。Go言語では、このコミットの時点では
sync.Pool
はまだ存在していませんでしたが、概念としては同様の目的で利用されます。このコミットでは、chan []byte
を使用して簡易的なバッファプールを実装しています。 -
ベンチマーク: ソフトウェアの性能を測定するためのテストです。Go言語では、
testing
パッケージにベンチマーク機能が組み込まれており、go test -bench .
コマンドで実行できます。ns/op
: 1操作あたりのナノ秒。値が小さいほど高速。allocs
: 1操作あたりのメモリ確保回数。値が小さいほどGCの負担が少ない。bytes
: 1操作あたりのメモリ割り当てバイト数。値が小さいほどメモリ効率が良い。
技術的詳細
このコミットの主要な技術的変更点は、bufio.Reader
が使用する内部バッファを、必要に応じてプールから取得し、不要になったらプールに戻す「一時的 (transient)」なものにしたことです。
-
bufCache
の導入:var bufCache = make(chan []byte, arbitrarySize)
というチャネルが導入されました。これは、defaultBufSize
の容量を持つバイトスライスを保持するための簡易的なバッファプールとして機能します。arbitrarySize
は、プールに保持するバッファの最大数を制限します。チャネルを使用することで、複数のゴルーチンから安全にバッファの出し入れができます。- 補足: このコミットの時点では
sync.Pool
はGo言語に導入されていませんでした。sync.Pool
はGo 1.3で導入され、より効率的なオブジェクトプールを提供します。このbufCache
は、sync.Pool
の前身となるような、手動でのバッファプーリングの実装例と言えます。
-
Reader
構造体の変更:buf []byte
フィールドは、nil
またはbufSize
サイズのバイトスライスを保持するようになりました。bufSize int
フィールドが追加され、Reader
が使用するバッファの推奨サイズを保持します。これにより、buf
がnil
の状態でもバッファサイズを記憶できます。
-
NewReaderSize
およびNewReader
の変更:- これらのコンストラクタは、
defaultBufSize
を超えるサイズのバッファを要求された場合にのみ、初期バッファをmake
するようになりました。defaultBufSize
以下の場合は、r.buf
をnil
のままにしておき、必要になったときにallocBuf
でバッファを確保するように変更されました。これは、Reader
が実際にデータを読み込むまでバッファの確保を遅延させる「遅延アロケーション」の考え方に基づいています。
- これらのコンストラクタは、
-
allocBuf()
メソッドの追加:- このメソッドは、
b.buf
がnil
の場合にバッファを確保します。 - まず
bufCache
からバッファを取得しようと試みます (<-bufCache
)。 - プールが空の場合、新しいバッファを
make
します。 - これにより、新しいバッファの確保回数を減らし、既存のバッファを再利用できるようになります。
- このメソッドは、
-
putBuf()
メソッドの追加:- このメソッドは、
Reader
の内部バッファが空になり、かつエラーが発生していない(つまり、読み取りが完了した)場合に、そのバッファをbufCache
に戻します。 b.r == b.w
はバッファが空であることを示し、b.err == io.EOF
は読み取りが完了したことを示します。cap(b.buf) == defaultBufSize
は、プールに戻すバッファがデフォルトサイズであることを確認します。これにより、異なるサイズのバッファがプールに混在するのを防ぎます。select { case bufCache <- b.buf: ... default: }
を使用することで、プールが満杯の場合でもブロックせずに処理を進めます。- バッファをプールに戻した後、
b.buf = nil
とすることで、Reader
インスタンスがバッファへの参照を解放し、バッファがガベージコレクタの対象となるか、プールで再利用されるようにします。
- このメソッドは、
-
既存メソッドへの
allocBuf()
とputBuf()
の組み込み:fill()
: バッファにデータを読み込む前にb.allocBuf()
を呼び出し、バッファが確実に存在するようにします。Read()
,ReadString()
,WriteTo()
: これらのメソッドは、読み取りが完了し、バッファが空になった場合にb.putBuf()
を呼び出すように変更されました。これにより、不要になったバッファがプールに戻されます。UnreadByte()
:b.allocBuf()
を呼び出し、バッファがnil
の場合に確保するようにします。
これらの変更により、bufio.Reader
はバッファを動的に管理し、不要になったバッファを再利用することで、メモリ効率とパフォーマンスを大幅に向上させています。
コアとなるコードの変更箇所
src/pkg/bufio/bufio.go
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -29,7 +29,8 @@ var (
// Reader implements buffering for an io.Reader object.
type Reader struct {
- buf []byte
+ buf []byte // either nil or []byte of size bufSize
+ bufSize int
rd io.Reader
r, w int
err error
@@ -45,18 +46,23 @@ const minReadBufferSize = 16
func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
- if ok && len(b.buf) >= size {
+ if ok && b.bufSize >= size {
return b
}
if size < minReadBufferSize {
size = minReadBufferSize
}
- return &Reader{
- buf: make([]byte, size),\n
+ r := &Reader{
+ bufSize: size,
rd: rd,
lastByte: -1,
lastRuneSize: -1,
}\n
+ if size > defaultBufSize {
+ // TODO(bradfitz): make all buffer sizes recycle
+ r.buf = make([]byte, r.bufSize)
+ }
+ return r
}
// NewReader returns a new Reader whose buffer has the default size.
@@ -66,8 +72,42 @@ func NewReader(rd io.Reader) *Reader {
var errNegativeRead = errors.New("bufio: reader returned negative count from Read")
+// TODO: use a sync.Cache instead of this:
+const arbitrarySize = 8
+
+// bufCache holds only byte slices with capacity defaultBufSize.
+var bufCache = make(chan []byte, arbitrarySize)
+
+// allocBuf makes b.buf non-nil.
+func (b *Reader) allocBuf() {
+ if b.buf != nil {
+ return
+ }
+ select {
+ case b.buf = <-bufCache:
+ b.buf = b.buf[:b.bufSize]
+ default:
+ b.buf = make([]byte, b.bufSize, defaultBufSize)
+ }
+}
+
+// putBuf returns b.buf if it's unused.
+func (b *Reader) putBuf() {
+ if b.r == b.w && b.err == io.EOF && cap(b.buf) == defaultBufSize {
+ select {
+ case bufCache <- b.buf:
+ b.buf = nil
+ b.r = 0
+ b.w = 0
+ default:
+ }
+ }
+}
+
// fill reads a new chunk into the buffer.
func (b *Reader) fill() {
+\tb.allocBuf()\n
+
// Slide existing data to beginning.\n
if b.r > 0 {
copy(b.buf, b.buf[b.r:b.w])
@@ -100,7 +140,7 @@ func (b *Reader) Peek(n int) ([]byte, error) {
if n < 0 {
return nil, ErrNegativeCount
}
- if n > len(b.buf) {
+ if n > b.bufSize {
return nil, ErrBufferFull
}
for b.w-b.r < n && b.err == nil {
@@ -134,7 +174,7 @@ func (b *Reader) Read(p []byte) (n int, err error) {
if b.err != nil {
return 0, b.readErr()
}\n
- if len(p) >= len(b.buf) {
+ if len(p) >= b.bufSize {
// Large read, empty buffer.
// Read directly into p to avoid copy.
n, b.err = b.rd.Read(p)
@@ -157,6 +197,7 @@ func (b *Reader) Read(p []byte) (n int, err error) {
b.r += n
b.lastByte = int(b.buf[b.r-1])
b.lastRuneSize = -1
+\tb.putBuf()\n
return n, nil
}
@@ -173,6 +214,9 @@ func (b *Reader) ReadByte() (c byte, err error) {
c = b.buf[b.r]
b.r++
b.lastByte = int(c)
+\tif b.err != nil { // avoid putBuf call in the common case
+\t\tb.putBuf()\n
+\t}\n
return c, nil
}
@@ -180,6 +224,7 @@ func (b *Reader) UnreadByte() error {
func (b *Reader) UnreadByte() error {
b.lastRuneSize = -1
if b.r == b.w && b.lastByte >= 0 {
+\t\tb.allocBuf()\n
\tb.w = 1
\tb.r = 0
\tb.buf[0] = byte(b.lastByte)
@@ -381,7 +426,9 @@ func (b *Reader) ReadBytes(delim byte) (line []byte, err error) {
// For simple uses, a Scanner may be more convenient.\n
func (b *Reader) ReadString(delim byte) (line string, err error) {
bytes, err := b.ReadBytes(delim)\n
-\treturn string(bytes), err\n
+\tline = string(bytes)\n
+\tb.putBuf()\n
+\treturn line, err
}
// WriteTo implements io.WriterTo.
@@ -416,6 +463,7 @@ func (b *Reader) WriteTo(w io.Writer) (n int64, err error) {
func (b *Reader) writeBuf(w io.Writer) (int64, error) {
n, err := w.Write(b.buf[b.r:b.w])
b.r += n
+\tb.putBuf()\n
return int64(n), err
}
@@ -444,6 +492,7 @@ func NewWriterSize(wr io.Writer, size int) *Writer {
\tsize = defaultBufSize
}\n
b = new(Writer)\n
+\t// TODO(bradfitz): make Writer buffers lazy too, like Reader's\n
b.buf = make([]byte, size)\n
b.wr = wr
return b
src/pkg/bufio/bufio_test.go
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -1083,3 +1083,18 @@ func BenchmarkWriterCopyNoReadFrom(b *testing.B) {
\tio.Copy(dst, src)
}\n
}\n
+\n
+func BenchmarkReaderEmpty(b *testing.B) {
+\tb.ReportAllocs()\n
+\tstr := strings.Repeat("x", 16<<10)\n
+\tfor i := 0; i < b.N; i++ {\n
+\t\tbr := NewReader(strings.NewReader(str))\n
+\t\tn, err := io.Copy(ioutil.Discard, br)\n
+\t\tif err != nil {\n
+\t\t\tb.Fatal(err)\n
+\t\t}\n
+\t\tif n != int64(len(str)) {\n
+\t\t\tb.Fatal("wrong length")\n
+\t\t}\n
+\t}\n
+}\n
コアとなるコードの解説
bufio.go
の変更点
-
Reader
構造体の変更:buf []byte // either nil or []byte of size bufSize
buf
フィールドのコメントが更新され、バッファがnil
になる可能性があることが明示されました。これは、バッファがプールに戻されたり、遅延アロケーションされたりするためです。
bufSize int
- 新しく追加されたフィールドで、
Reader
が使用するバッファの論理的なサイズを保持します。実際のbuf
スライスがnil
であっても、このサイズは常に保持されます。
- 新しく追加されたフィールドで、
-
NewReaderSize
関数の変更:- 以前は
make([]byte, size)
で常にバッファを初期化していましたが、変更後はr.bufSize = size
を設定し、size > defaultBufSize
の場合にのみr.buf = make(...)
でバッファを確保するようになりました。 defaultBufSize
以下のバッファサイズの場合、バッファは最初はnil
のままとなり、実際に読み取りが必要になったときにallocBuf()
によって確保されます。これにより、Reader
が作成されたものの、実際にはほとんど読み取られないようなケースでのメモリ確保を削減します。
- 以前は
-
bufCache
変数:var bufCache = make(chan []byte, arbitrarySize)
defaultBufSize
のバイトスライスを再利用するためのチャネルベースのプールです。arbitrarySize
はプールに保持できるバッファの最大数(この場合は8)を定義します。
-
allocBuf()
メソッド:func (b *Reader) allocBuf() { ... }
b.buf
がnil
の場合に呼び出され、バッファを確保します。select { case b.buf = <-bufCache: ... default: ... }
- まず
bufCache
からバッファを取得しようと試みます。成功すれば、そのバッファをb.buf
に設定し、b.bufSize
に合わせてスライスを調整します。 bufCache
が空の場合(default
ブロック)、新しいdefaultBufSize
のバッファをmake
で作成します。
- まず
- このメカニズムにより、新しいバッファの確保を最小限に抑え、既存のバッファを再利用します。
-
putBuf()
メソッド:func (b *Reader) putBuf() { ... }
Reader
のバッファが空になり (b.r == b.w
)、かつ読み取りが完了した (b.err == io.EOF
) 場合に呼び出されます。cap(b.buf) == defaultBufSize
の条件は、プールに戻すバッファがデフォルトサイズであることを保証します。これにより、プールが異なるサイズのバッファで汚染されるのを防ぎます。select { case bufCache <- b.buf: ... default: }
b.buf
をbufCache
に戻そうと試みます。プールが満杯の場合、バッファはプールに戻されず、ガベージコレクタによって回収されます。- プールに戻された後、
b.buf = nil
、b.r = 0
、b.w = 0
とすることで、Reader
インスタンスがバッファへの参照を解放し、バッファが再利用可能であることを示します。
-
既存メソッドへの
allocBuf()
とputBuf()
の組み込み:fill()
: データをバッファに読み込む前にb.allocBuf()
を呼び出し、バッファが確実に存在するようにします。Read()
,ReadByte()
,ReadString()
,WriteTo()
,writeBuf()
: これらのメソッドは、読み取り操作が完了し、バッファが空になった場合にb.putBuf()
を呼び出すように変更されました。これにより、バッファが不要になった時点で速やかにプールに戻されます。UnreadByte()
:b.allocBuf()
を呼び出し、バッファがnil
の場合に確保するようにします。これは、UnreadByte
がバッファにバイトを書き戻す必要があるためです。
bufio_test.go
の変更点
BenchmarkReaderEmpty
の追加:- このベンチマークは、
bufio.Reader
が空になるまでデータを読み取るシナリオをシミュレートします。 strings.NewReader(str)
からNewReader
を作成し、io.Copy(ioutil.Discard, br)
でデータを読み捨てます。b.ReportAllocs()
を呼び出すことで、メモリ確保の回数とバイト数をレポートに含めるようにしています。- このベンチマークは、バッファプーリングの効果を測定するために特に設計されており、コミットメッセージに記載されている劇的なパフォーマンス改善の根拠となっています。
- このベンチマークは、
これらの変更により、bufio.Reader
はバッファをより動的に、かつ効率的に管理できるようになり、特に短命な Reader
インスタンスが多数生成されるような状況でのメモリフットプリントとガベージコレクションの負荷を大幅に削減します。
関連リンク
- Go言語
bufio
パッケージのドキュメント: https://pkg.go.dev/bufio - Go言語
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語
sync
パッケージのドキュメント (特にsync.Pool
): https://pkg.go.dev/sync#Pool (このコミット時点ではsync.Pool
は存在しませんが、関連する概念として) - Go言語のベンチマークに関する公式ドキュメント: https://go.dev/doc/articles/go_benchmarking.html
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のIssueトラッカー (Issue #5100): https://go.dev/issue/5100
- Gerrit Code Review (Change-Id 8819049): https://golang.org/cl/8819049 (コミットメッセージに記載されているGerritのリンク)
- Go言語の
sync.Pool
に関する記事やドキュメント (このコミットの後のバージョンで導入された概念ですが、バッファプーリングの文脈で参考になります):- Go 1.3 Release Notes (sync.Poolの導入について): https://go.dev/doc/go1.3#sync_pool
- "Go's sync.Pool" by Dave Cheney: https://dave.cheney.net/2013/09/02/gos-sync-pool