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

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

このコミットは、Go言語の標準ライブラリ bufio パッケージにおける Writer のパフォーマンス改善を目的としています。具体的には、bufio.WriterFlush メソッドが呼び出された際に、内部で使用しているバッファを再利用するメカニズムを導入することで、メモリ割り当て(アロケーション)とガベージコレクションの負荷を軽減し、全体的な処理速度を向上させています。特に、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 されて破棄されるようなシナリオでは、バッファの頻繁な割り当てと解放がガベージコレクションの負荷を増大させ、パフォーマンスのボトルネックとなっていました。

このコミットは、FlushWriter の「通常の寿命の終わり」を示すという認識に基づいています。つまり、Flush が呼び出された時点で、その Writer が今後バッファを必要としない可能性が高いと判断し、その内部バッファを再利用可能なプールに戻すことで、メモリ割り当てのオーバーヘッドを削減し、ガベージコレクションの頻度を減らすことを目的としています。ベンチマーク結果が示すように、特に BenchmarkWriterEmpty のようなシナリオで、ナノ秒あたりの操作数、アロケーション数、および割り当てバイト数において劇的な改善を達成しています。

前提知識の解説

Go言語の bufio パッケージ

bufio パッケージは、Go言語におけるバッファリングされたI/O操作を提供します。これにより、小さなI/O操作が多数発生する際に、システムコールを減らし、パフォーマンスを向上させることができます。

  • bufio.Reader: io.Reader をラップし、バッファリングされた読み取り機能を提供します。
  • bufio.Writer: io.Writer をラップし、バッファリングされた書き込み機能を提供します。データはまず内部バッファに蓄えられ、バッファが満杯になるか、Flush メソッドが呼び出されると、まとめて下層の io.Writer に書き込まれます。

Flush メソッド

bufio.WriterFlush() メソッドは、現在バッファに蓄えられているすべてのデータを、強制的に下層の io.Writer に書き出すために使用されます。これは、データが確実に書き込まれたことを保証したい場合や、Writer の使用を終える際に重要です。

メモリ割り当てとガベージコレクション (GC)

Go言語はガベージコレクタを備えており、開発者が手動でメモリを解放する必要はありません。しかし、頻繁なメモリ割り当て(アロケーション)はGCのトリガーとなり、GCが実行されるとプログラムの実行が一時停止(ストップ・ザ・ワールド)するため、パフォーマンスに影響を与える可能性があります。特に、大きなバッファの頻繁な割り当てと解放は、GCの負荷を増大させる主要な要因の一つです。

バッファプーリング

バッファプーリングは、メモリ割り当てのオーバーヘッドを削減するための一般的な最適化手法です。これは、頻繁に割り当てられるオブジェクト(この場合はバイトスライス)を、使用後にすぐに解放するのではなく、再利用可能なプールの形で保持するものです。新しいオブジェクトが必要になった際には、プールから既存のオブジェクトを取得し、プールが空の場合にのみ新しいオブジェクトを割り当てます。これにより、GCの頻度を減らし、アプリケーションの応答性を向上させることができます。

技術的詳細

このコミットの主要な変更点は、bufio.Writer が使用する内部バッファを再利用するためのメカニズムを導入したことです。これは、Goのチャネルとグローバルなバッファキャッシュ bufCache を利用して実現されています。

  1. bufCache チャネルの導入: var bufCache = make(chan []byte, 16) これは、defaultBufSize (4096バイト) のバッファを最大16個までキャッシュできるチャネルです。bufio.Writer がバッファを使い終わったときに、このチャネルにバッファを送信し、新しい bufio.Writer がバッファを必要とするときに、このチャネルからバッファを受信します。

  2. Writer 構造体の変更: Writer 構造体に bufSize int フィールドが追加されました。これにより、Writer が作成される際に指定されたバッファサイズを保持できるようになります。以前は len(b.buf) でバッファサイズを取得していましたが、バッファが nil になる可能性があるため、明示的な bufSize フィールドが必要になりました。

  3. allocBuf() メソッドの導入: func (b *Writer) allocBuf() { ... } このメソッドは、Writer の内部バッファ b.bufnil の場合に、バッファを割り当てるか、bufCache から取得します。

    • select ステートメントを使用して、bufCache からバッファを取得しようとします。
    • 取得できた場合、そのバッファの長さを b.bufSize に設定します。
    • bufCache が空の場合、make([]byte, b.bufSize, defaultBufSize) を使用して新しいバッファを割り当てます。defaultBufSize を容量として指定することで、バッファが拡張された際に再割り当てを減らす効果も期待できます。
  4. putBuf() メソッドの導入: func (b *Writer) putBuf() { ... } このメソッドは、Writer の内部バッファ b.bufbufCache に戻す役割を担います。

    • バッファが空 (b.n == 0) であり、かつバッファの容量が defaultBufSize と等しい場合にのみ、バッファをキャッシュに戻します。これは、defaultBufSize 以外のサイズのバッファや、まだデータが残っているバッファをキャッシュしないための条件です。
    • select ステートメントを使用して、bufCache にバッファを送信しようとします。bufCache が満杯の場合(つまり、他のバッファでいっぱいの場合)、バッファは単にガベージコレクションの対象となります。
  5. Flush() メソッドの変更と flush() の導入: Flush() メソッドは、実際のフラッシュ処理を行う flush() メソッドを呼び出すように変更されました。そして、flush() の呼び出し後に b.putBuf() を呼び出すことで、明示的な Flush の後にバッファをキャッシュに戻すようにしました。

    • Flush() は外部から呼び出される公開メソッドであり、Writer の寿命の終わりを示すと見なされます。
    • WriteWriteByte などの内部的なバッファリング操作によって自動的にフラッシュされる場合は、flush() が直接呼び出され、putBuf() は呼び出されません。これにより、「通常の、必要に応じた内部フラッシュ」ではバッファを再利用しないというコミットメッセージの意図が反映されています。
  6. 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() メソッドの導入です。

  1. bufCache (チャネル): var bufCache = make(chan []byte, 16) これは、defaultBufSize (4096バイト) のバイトスライスを最大16個まで保持できるバッファ付きチャネルです。bufio.Writer がバッファを使い終わったときに putBuf() を通じてここにバッファを「返却」し、新しい bufio.Writer がバッファを必要とするときに allocBuf() を通じてここからバッファを「取得」します。これにより、頻繁な make([]byte, ...) によるメモリ割り当てと、それに伴うガベージコレクションの発生を抑制します。

  2. allocBuf() メソッド: func (b *Writer) allocBuf() { ... } このメソッドは、Writer の内部バッファ b.bufnil の場合に呼び出されます。

    • まず、select ステートメントを使って bufCache からバッファを取得しようと試みます。select はノンブロッキングで、チャネルからすぐに値が取得できればそれを使用します。
    • もし bufCache が空でバッファが取得できなかった場合、default ケースが実行され、make([]byte, b.bufSize, defaultBufSize) を使って新しいバッファが割り当てられます。
    • 取得または割り当てられたバッファは、b.buf に設定され、b.bufSize の長さにスライスされます。
  3. putBuf() メソッド: func (b *Writer) putBuf() { ... } このメソッドは、Writer の内部バッファを bufCache に戻すために使用されます。

    • バッファが空 (b.n == 0、つまりバッファに書き込まれたデータがない状態) であり、かつバッファの容量が defaultBufSize と等しい場合にのみ、バッファをキャッシュに戻します。この条件は、バッファが完全に使い切られ、かつ標準的なサイズのバッファのみを再利用の対象とすることで、キャッシュの効率を保ちます。
    • select ステートメントを使って bufCache にバッファを送信しようと試みます。bufCache が満杯の場合、default ケースが実行され、バッファはキャッシュに戻されずにガベージコレクションの対象となります。これにより、キャッシュが過剰に大きくなることを防ぎます。
  4. Flush()flush() の分離:

    • 従来の Flush() メソッドは、実際のフラッシュ処理を行う新しいプライベートメソッド flush() を呼び出すように変更されました。
    • そして、Flush() メソッドの最後に b.putBuf() が追加されました。これにより、ユーザーが明示的に Flush() を呼び出した場合にのみ、バッファがキャッシュに戻されるようになりました。
    • Write などの内部的な操作によってバッファが満杯になり、自動的にフラッシュされる場合は、b.flush() が直接呼び出され、b.putBuf() は呼び出されません。これは、これらの内部フラッシュは Writer の寿命の終わりを示すものではないため、バッファをキャッシュに戻すべきではないという設計思想に基づいています。

これらの変更により、bufio.Writer の使用パターン、特に短命な Writer インスタンスが頻繁に生成されるケースにおいて、メモリ効率とパフォーマンスが大幅に向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の bufio パッケージのドキュメント
  • Go言語のガベージコレクションに関する一般的な情報
  • Go言語におけるバッファプーリングの概念