[インデックス 16366] ファイルの概要
このコミットは、Go言語の標準ライブラリ bufio
パッケージにおける Writer
のパフォーマンス改善を目的としています。具体的には、bufio.Writer
の Flush
メソッドが呼び出された際に、内部で使用しているバッファを再利用するメカニズムを導入することで、メモリ割り当て(アロケーション)とガベージコレクションの負荷を軽減し、全体的な処理速度を向上させています。特に、Writer
のライフサイクルの終わりに明示的に Flush
が行われるケースにおいて、顕著な改善が見られます。
コミット
Author: Brad Fitzpatrick bradfitz@golang.org Date: Tue May 21 15:51:49 2013 -0700 Commit Hash: 99f67228608db9c9a587586186ec612feb425e48
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/99f67228608db9c9a587586186ec612feb425e48
元コミット内容
bufio: reuse Writer buffers after Flush
A bufio.Writer.Flush marks the usual end of a Writer's
life. Recycle its internal buffer on those explicit flushes,
but not on normal, as-needed internal flushes.
benchmark old ns/op new ns/op delta
BenchmarkWriterEmpty 1959 727 -62.89%
benchmark old allocs new allocs delta
BenchmarkWriterEmpty 2 1 -50.00%
benchmark old bytes new bytes delta
BenchmarkWriterEmpty 4215 83 -98.03%
R=gri, iant
CC=gobot, golang-dev, voidlogic7
https://golang.org/cl/9459044
変更の背景
bufio.Writer
は、I/O操作の効率化のために内部バッファを使用します。データはまずこのバッファに書き込まれ、バッファが満杯になるか、明示的に Flush
メソッドが呼び出されると、まとめて下層の io.Writer
に書き出されます。
従来の bufio.Writer
の実装では、Flush
が呼び出された後も内部バッファは解放されず、Writer
オブジェクトがガベージコレクションの対象となるまでメモリを占有し続けていました。特に、短期間で多数の bufio.Writer
インスタンスが生成され、それぞれが一度だけ Flush
されて破棄されるようなシナリオでは、バッファの頻繁な割り当てと解放がガベージコレクションの負荷を増大させ、パフォーマンスのボトルネックとなっていました。
このコミットは、Flush
が Writer
の「通常の寿命の終わり」を示すという認識に基づいています。つまり、Flush
が呼び出された時点で、その Writer
が今後バッファを必要としない可能性が高いと判断し、その内部バッファを再利用可能なプールに戻すことで、メモリ割り当てのオーバーヘッドを削減し、ガベージコレクションの頻度を減らすことを目的としています。ベンチマーク結果が示すように、特に BenchmarkWriterEmpty
のようなシナリオで、ナノ秒あたりの操作数、アロケーション数、および割り当てバイト数において劇的な改善を達成しています。
前提知識の解説
Go言語の bufio
パッケージ
bufio
パッケージは、Go言語におけるバッファリングされたI/O操作を提供します。これにより、小さなI/O操作が多数発生する際に、システムコールを減らし、パフォーマンスを向上させることができます。
bufio.Reader
:io.Reader
をラップし、バッファリングされた読み取り機能を提供します。bufio.Writer
:io.Writer
をラップし、バッファリングされた書き込み機能を提供します。データはまず内部バッファに蓄えられ、バッファが満杯になるか、Flush
メソッドが呼び出されると、まとめて下層のio.Writer
に書き込まれます。
Flush
メソッド
bufio.Writer
の Flush()
メソッドは、現在バッファに蓄えられているすべてのデータを、強制的に下層の io.Writer
に書き出すために使用されます。これは、データが確実に書き込まれたことを保証したい場合や、Writer
の使用を終える際に重要です。
メモリ割り当てとガベージコレクション (GC)
Go言語はガベージコレクタを備えており、開発者が手動でメモリを解放する必要はありません。しかし、頻繁なメモリ割り当て(アロケーション)はGCのトリガーとなり、GCが実行されるとプログラムの実行が一時停止(ストップ・ザ・ワールド)するため、パフォーマンスに影響を与える可能性があります。特に、大きなバッファの頻繁な割り当てと解放は、GCの負荷を増大させる主要な要因の一つです。
バッファプーリング
バッファプーリングは、メモリ割り当てのオーバーヘッドを削減するための一般的な最適化手法です。これは、頻繁に割り当てられるオブジェクト(この場合はバイトスライス)を、使用後にすぐに解放するのではなく、再利用可能なプールの形で保持するものです。新しいオブジェクトが必要になった際には、プールから既存のオブジェクトを取得し、プールが空の場合にのみ新しいオブジェクトを割り当てます。これにより、GCの頻度を減らし、アプリケーションの応答性を向上させることができます。
技術的詳細
このコミットの主要な変更点は、bufio.Writer
が使用する内部バッファを再利用するためのメカニズムを導入したことです。これは、Goのチャネルとグローバルなバッファキャッシュ bufCache
を利用して実現されています。
-
bufCache
チャネルの導入:var bufCache = make(chan []byte, 16)
これは、defaultBufSize
(4096バイト) のバッファを最大16個までキャッシュできるチャネルです。bufio.Writer
がバッファを使い終わったときに、このチャネルにバッファを送信し、新しいbufio.Writer
がバッファを必要とするときに、このチャネルからバッファを受信します。 -
Writer
構造体の変更:Writer
構造体にbufSize int
フィールドが追加されました。これにより、Writer
が作成される際に指定されたバッファサイズを保持できるようになります。以前はlen(b.buf)
でバッファサイズを取得していましたが、バッファがnil
になる可能性があるため、明示的なbufSize
フィールドが必要になりました。 -
allocBuf()
メソッドの導入:func (b *Writer) allocBuf() { ... }
このメソッドは、Writer
の内部バッファb.buf
がnil
の場合に、バッファを割り当てるか、bufCache
から取得します。select
ステートメントを使用して、bufCache
からバッファを取得しようとします。- 取得できた場合、そのバッファの長さを
b.bufSize
に設定します。 bufCache
が空の場合、make([]byte, b.bufSize, defaultBufSize)
を使用して新しいバッファを割り当てます。defaultBufSize
を容量として指定することで、バッファが拡張された際に再割り当てを減らす効果も期待できます。
-
putBuf()
メソッドの導入:func (b *Writer) putBuf() { ... }
このメソッドは、Writer
の内部バッファb.buf
をbufCache
に戻す役割を担います。- バッファが空 (
b.n == 0
) であり、かつバッファの容量がdefaultBufSize
と等しい場合にのみ、バッファをキャッシュに戻します。これは、defaultBufSize
以外のサイズのバッファや、まだデータが残っているバッファをキャッシュしないための条件です。 select
ステートメントを使用して、bufCache
にバッファを送信しようとします。bufCache
が満杯の場合(つまり、他のバッファでいっぱいの場合)、バッファは単にガベージコレクションの対象となります。
- バッファが空 (
-
Flush()
メソッドの変更とflush()
の導入:Flush()
メソッドは、実際のフラッシュ処理を行うflush()
メソッドを呼び出すように変更されました。そして、flush()
の呼び出し後にb.putBuf()
を呼び出すことで、明示的なFlush
の後にバッファをキャッシュに戻すようにしました。Flush()
は外部から呼び出される公開メソッドであり、Writer
の寿命の終わりを示すと見なされます。Write
やWriteByte
などの内部的なバッファリング操作によって自動的にフラッシュされる場合は、flush()
が直接呼び出され、putBuf()
は呼び出されません。これにより、「通常の、必要に応じた内部フラッシュ」ではバッファを再利用しないというコミットメッセージの意図が反映されています。
-
Write
,WriteByte
,WriteRune
,WriteString
,ReadFrom
メソッドの変更: これらの書き込み関連のメソッドは、データの書き込み前にb.allocBuf()
を呼び出すように変更されました。これにより、バッファがまだ割り当てられていない場合に、必要に応じてバッファが取得または割り当てられるようになります。また、内部的なフラッシュの呼び出しがb.Flush()
からb.flush()
に変更され、バッファの再利用ロジックが適切に適用されるようになりました。
これらの変更により、bufio.Writer
が明示的に Flush
された際に、その内部バッファが bufCache
を介して再利用されるようになり、特に短命な Writer
インスタンスが多数生成されるシナリオでのメモリ割り当てとGCのオーバーヘッドが大幅に削減されました。
コアとなるコードの変更箇所
src/pkg/bufio/bufio.go
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -29,7 +29,7 @@ var (
// Reader implements buffering for an io.Reader object.
type Reader struct {
- buf []byte // either nil or []byte of size bufSize
+ buf []byte // either nil or []byte of length bufSize
bufSize int
rd io.Reader
r, w int
@@ -314,7 +314,7 @@ func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
// Buffer is full?
- if b.Buffered() >= len(b.buf) {
+ if b.Buffered() >= b.bufSize {
b.r = b.w
return b.buf, ErrBufferFull
}
@@ -473,10 +473,11 @@ func (b *Reader) writeBuf(w io.Writer) (int64, error) {
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes will return the error.
type Writer struct {
- err error
- buf []byte
- n int
- wr io.Writer
+ err error
+ buf []byte // either nil or []byte of length bufSize
+ bufSize int
+ n int
+ wr io.Writer
}
// NewWriterSize returns a new Writer whose buffer has at least the specified
@@ -485,16 +486,20 @@ func NewWriterSize(wr io.Writer, size int) *Writer {
func NewWriterSize(wr io.Writer, size int) *Writer {
// Is it already a Writer?
b, ok := wr.(*Writer)
- if ok && len(b.buf) >= size {
+ if ok && b.bufSize >= size {
return b
}
if size <= 0 {
size = defaultBufSize
}
- b = new(Writer)
- // TODO(bradfitz): make Writer buffers lazy too, like Reader's
- b.buf = make([]byte, size)
- b.wr = wr
+ b = &Writer{
+ wr: wr,
+ bufSize: size,
+ }
+ if size > defaultBufSize {
+ // TODO(bradfitz): make all buffer sizes recycle
+ b.buf = make([]byte, b.bufSize)
+ }
return b
}
@@ -503,8 +508,38 @@ func NewWriter(wr io.Writer) *Writer {
return NewWriterSize(wr, defaultBufSize)
}
+// allocBuf makes b.buf non-nil.
+func (b *Writer) 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 *Writer) putBuf() {
+ if b.n == 0 && cap(b.buf) == defaultBufSize {
+ select {
+ case bufCache <- b.buf:
+ b.buf = nil
+ default:
+ }
+ }
+}
+
// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {
+ err := b.flush()
+ b.putBuf()
+ return err
+}
+
+func (b *Writer) flush() error {
if b.err != nil {
return b.err
}
@@ -528,7 +563,7 @@ func (b *Writer) Flush() error {
}
// Available returns how many bytes are unused in the buffer.
-func (b *Writer) Available() int { return len(b.buf) - b.n }
+func (b *Writer) Available() int { return b.bufSize - b.n }
// Buffered returns the number of bytes that have been written into the current buffer.
func (b *Writer) Buffered() int { return b.n }
@@ -538,6 +573,7 @@ func (b *Writer) Buffered() int { return b.n }
// If nn < len(p), it also returns an error explaining
// why the write is short.
func (b *Writer) Write(p []byte) (nn int, err error) {
+ b.allocBuf()
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
@@ -547,7 +583,7 @@ func (b *Writer) Write(p []byte) (nn int, err error) {
} else {
n = copy(b.buf[b.n:], p)
b.n += n
- b.Flush()
+ b.flush()
}
nn += n
p = p[n:]
@@ -566,9 +602,12 @@ func (b *Writer) WriteByte(c byte) error {
if b.err != nil {
return b.err
}
- if b.Available() <= 0 && b.Flush() != nil {
+ if b.Available() <= 0 && b.flush() != nil {
return b.err
}
+ if b.buf == nil {
+ b.allocBuf()
+ }
b.buf[b.n] = c
b.n++
return nil
@@ -577,6 +616,9 @@ func (b *Writer) WriteByte(c byte) error {
// WriteRune writes a single Unicode code point, returning
// the number of bytes written and any error.
func (b *Writer) WriteRune(r rune) (size int, err error) {
+ if b.buf == nil {
+ b.allocBuf()
+ }
if r < utf8.RuneSelf {
err = b.WriteByte(byte(r))
if err != nil {
@@ -589,7 +631,7 @@ func (b *Writer) WriteRune(r rune) (size int, err error) {
}
n := b.Available()
if n < utf8.UTFMax {
- if b.Flush(); b.err != nil {
+ if b.flush(); b.err != nil {
return 0, b.err
}
n = b.Available()
@@ -608,13 +650,14 @@ func (b *Writer) WriteRune(r rune) (size int, err error) {
// If the count is less than len(s), it also returns an error explaining
// why the write is short.
func (b *Writer) WriteString(s string) (int, error) {
+ b.allocBuf()
nn := 0
for len(s) > b.Available() && b.err == nil {
n := copy(b.buf[b.n:], s)
b.n += n
nn += n
s = s[n:]
- b.Flush()
+ b.flush()
}
if b.err != nil {
return nn, b.err
@@ -627,6 +670,7 @@ func (b *Writer) WriteString(s string) (int, error) {
// ReadFrom implements io.ReaderFrom.
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
+ b.allocBuf()
if b.Buffered() == 0 {
if w, ok := b.wr.(io.ReaderFrom); ok {
return w.ReadFrom(r)
@@ -641,7 +685,7 @@ func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
b.n += m
n += int64(m)
if b.Available() == 0 {
- if err1 := b.Flush(); err1 != nil {
+ if err1 := b.flush(); err1 != nil {
return n, err1
}
}
src/pkg/bufio/bufio_test.go
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -1098,3 +1098,31 @@ func BenchmarkReaderEmpty(b *testing.B) {
}
}
}
+
+func BenchmarkWriterEmpty(b *testing.B) {
+ b.ReportAllocs()
+ str := strings.Repeat("x", 1<<10)
+ bs := []byte(str)
+ for i := 0; i < b.N; i++ {
+ bw := NewWriter(ioutil.Discard)
+ bw.Flush()
+ bw.WriteByte('a')
+ bw.Flush()
+ bw.WriteRune('B')
+ bw.Flush()
+ bw.Write(bs)
+ bw.Flush()
+ bw.WriteString(str)
+ bw.Flush()
+ }
+}
+
+func BenchmarkWriterFlush(b *testing.B) {
+ b.ReportAllocs()
+ bw := NewWriter(ioutil.Discard)
+ str := strings.Repeat("x", 50)
+ for i := 0; i < b.N; i++ {
+ bw.WriteString(str)
+ bw.Flush()
+ }
+}
コアとなるコードの解説
このコミットの核心は、bufio.Writer
の内部バッファを効率的に再利用するための bufCache
チャネルと、それに関連する allocBuf()
および putBuf()
メソッドの導入です。
-
bufCache
(チャネル):var bufCache = make(chan []byte, 16)
これは、defaultBufSize
(4096バイト) のバイトスライスを最大16個まで保持できるバッファ付きチャネルです。bufio.Writer
がバッファを使い終わったときにputBuf()
を通じてここにバッファを「返却」し、新しいbufio.Writer
がバッファを必要とするときにallocBuf()
を通じてここからバッファを「取得」します。これにより、頻繁なmake([]byte, ...)
によるメモリ割り当てと、それに伴うガベージコレクションの発生を抑制します。 -
allocBuf()
メソッド:func (b *Writer) allocBuf() { ... }
このメソッドは、Writer
の内部バッファb.buf
がnil
の場合に呼び出されます。- まず、
select
ステートメントを使ってbufCache
からバッファを取得しようと試みます。select
はノンブロッキングで、チャネルからすぐに値が取得できればそれを使用します。 - もし
bufCache
が空でバッファが取得できなかった場合、default
ケースが実行され、make([]byte, b.bufSize, defaultBufSize)
を使って新しいバッファが割り当てられます。 - 取得または割り当てられたバッファは、
b.buf
に設定され、b.bufSize
の長さにスライスされます。
- まず、
-
putBuf()
メソッド:func (b *Writer) putBuf() { ... }
このメソッドは、Writer
の内部バッファをbufCache
に戻すために使用されます。- バッファが空 (
b.n == 0
、つまりバッファに書き込まれたデータがない状態) であり、かつバッファの容量がdefaultBufSize
と等しい場合にのみ、バッファをキャッシュに戻します。この条件は、バッファが完全に使い切られ、かつ標準的なサイズのバッファのみを再利用の対象とすることで、キャッシュの効率を保ちます。 select
ステートメントを使ってbufCache
にバッファを送信しようと試みます。bufCache
が満杯の場合、default
ケースが実行され、バッファはキャッシュに戻されずにガベージコレクションの対象となります。これにより、キャッシュが過剰に大きくなることを防ぎます。
- バッファが空 (
-
Flush()
とflush()
の分離:- 従来の
Flush()
メソッドは、実際のフラッシュ処理を行う新しいプライベートメソッドflush()
を呼び出すように変更されました。 - そして、
Flush()
メソッドの最後にb.putBuf()
が追加されました。これにより、ユーザーが明示的にFlush()
を呼び出した場合にのみ、バッファがキャッシュに戻されるようになりました。 Write
などの内部的な操作によってバッファが満杯になり、自動的にフラッシュされる場合は、b.flush()
が直接呼び出され、b.putBuf()
は呼び出されません。これは、これらの内部フラッシュはWriter
の寿命の終わりを示すものではないため、バッファをキャッシュに戻すべきではないという設計思想に基づいています。
- 従来の
これらの変更により、bufio.Writer
の使用パターン、特に短命な Writer
インスタンスが頻繁に生成されるケースにおいて、メモリ効率とパフォーマンスが大幅に向上しました。
関連リンク
- Go Code Review: https://golang.org/cl/9459044
参考にした情報源リンク
- Go言語の
bufio
パッケージのドキュメント - Go言語のガベージコレクションに関する一般的な情報
- Go言語におけるバッファプーリングの概念