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

[インデックス 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)において、劇的な改善が見られます。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. bufio パッケージ: Go言語の標準ライブラリの一部で、I/O操作をバッファリングすることでパフォーマンスを向上させるための機能を提供します。bufio.Readerio.Reader インターフェースをラップし、内部バッファを使用して効率的な読み取りを可能にします。

    • バッファリング: データを一度にまとめて読み書きすることで、システムコール(ディスクI/OやネットワークI/Oなど)の回数を減らし、I/O効率を高める手法です。bufio.Reader は、指定されたサイズのバイトスライスを内部バッファとして持ち、そこからデータを読み出します。
    • io.Reader インターフェース: Go言語における基本的な読み取りインターフェースで、Read(p []byte) (n int, err error) メソッドを持ちます。
  2. ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはやどの変数からも参照されなくなった領域を自動的に解放する仕組みです。Go言語は自動ガベージコレクションを採用しており、開発者が手動でメモリを解放する必要はありません。しかし、GCの実行にはコストがかかり、頻繁なメモリ確保・解放はGCの負荷を増大させ、アプリケーションのレイテンシやスループットに影響を与える可能性があります。

  3. バッファプール (Buffer Pool): 頻繁に確保・解放される可能性のあるオブジェクト(この場合はバイトスライス)を再利用するために、それらを一時的に保持しておく仕組みです。オブジェクトが必要になったときにプールから取得し、不要になったときにプールに戻すことで、メモリの確保・解放のオーバーヘッドを削減し、ガベージコレクションの負担を軽減します。Go言語では、このコミットの時点では sync.Pool はまだ存在していませんでしたが、概念としては同様の目的で利用されます。このコミットでは、chan []byte を使用して簡易的なバッファプールを実装しています。

  4. ベンチマーク: ソフトウェアの性能を測定するためのテストです。Go言語では、testing パッケージにベンチマーク機能が組み込まれており、go test -bench . コマンドで実行できます。

    • ns/op: 1操作あたりのナノ秒。値が小さいほど高速。
    • allocs: 1操作あたりのメモリ確保回数。値が小さいほどGCの負担が少ない。
    • bytes: 1操作あたりのメモリ割り当てバイト数。値が小さいほどメモリ効率が良い。

技術的詳細

このコミットの主要な技術的変更点は、bufio.Reader が使用する内部バッファを、必要に応じてプールから取得し、不要になったらプールに戻す「一時的 (transient)」なものにしたことです。

  1. bufCache の導入:

    • var bufCache = make(chan []byte, arbitrarySize) というチャネルが導入されました。これは、defaultBufSize の容量を持つバイトスライスを保持するための簡易的なバッファプールとして機能します。arbitrarySize は、プールに保持するバッファの最大数を制限します。チャネルを使用することで、複数のゴルーチンから安全にバッファの出し入れができます。
    • 補足: このコミットの時点では sync.Pool はGo言語に導入されていませんでした。sync.Pool はGo 1.3で導入され、より効率的なオブジェクトプールを提供します。この bufCache は、sync.Pool の前身となるような、手動でのバッファプーリングの実装例と言えます。
  2. Reader 構造体の変更:

    • buf []byte フィールドは、nil または bufSize サイズのバイトスライスを保持するようになりました。
    • bufSize int フィールドが追加され、Reader が使用するバッファの推奨サイズを保持します。これにより、bufnil の状態でもバッファサイズを記憶できます。
  3. NewReaderSize および NewReader の変更:

    • これらのコンストラクタは、defaultBufSize を超えるサイズのバッファを要求された場合にのみ、初期バッファを make するようになりました。defaultBufSize 以下の場合は、r.bufnil のままにしておき、必要になったときに allocBuf でバッファを確保するように変更されました。これは、Reader が実際にデータを読み込むまでバッファの確保を遅延させる「遅延アロケーション」の考え方に基づいています。
  4. allocBuf() メソッドの追加:

    • このメソッドは、b.bufnil の場合にバッファを確保します。
    • まず bufCache からバッファを取得しようと試みます (<-bufCache)。
    • プールが空の場合、新しいバッファを make します。
    • これにより、新しいバッファの確保回数を減らし、既存のバッファを再利用できるようになります。
  5. 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 インスタンスがバッファへの参照を解放し、バッファがガベージコレクタの対象となるか、プールで再利用されるようにします。
  6. 既存メソッドへの 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 の変更点

  1. Reader 構造体の変更:

    • buf []byte // either nil or []byte of size bufSize
      • buf フィールドのコメントが更新され、バッファが nil になる可能性があることが明示されました。これは、バッファがプールに戻されたり、遅延アロケーションされたりするためです。
    • bufSize int
      • 新しく追加されたフィールドで、Reader が使用するバッファの論理的なサイズを保持します。実際の buf スライスが nil であっても、このサイズは常に保持されます。
  2. NewReaderSize 関数の変更:

    • 以前は make([]byte, size) で常にバッファを初期化していましたが、変更後は r.bufSize = size を設定し、size > defaultBufSize の場合にのみ r.buf = make(...) でバッファを確保するようになりました。
    • defaultBufSize 以下のバッファサイズの場合、バッファは最初は nil のままとなり、実際に読み取りが必要になったときに allocBuf() によって確保されます。これにより、Reader が作成されたものの、実際にはほとんど読み取られないようなケースでのメモリ確保を削減します。
  3. bufCache 変数:

    • var bufCache = make(chan []byte, arbitrarySize)
      • defaultBufSize のバイトスライスを再利用するためのチャネルベースのプールです。arbitrarySize はプールに保持できるバッファの最大数(この場合は8)を定義します。
  4. allocBuf() メソッド:

    • func (b *Reader) allocBuf() { ... }
      • b.bufnil の場合に呼び出され、バッファを確保します。
      • select { case b.buf = <-bufCache: ... default: ... }
        • まず bufCache からバッファを取得しようと試みます。成功すれば、そのバッファを b.buf に設定し、b.bufSize に合わせてスライスを調整します。
        • bufCache が空の場合(default ブロック)、新しい defaultBufSize のバッファを make で作成します。
      • このメカニズムにより、新しいバッファの確保を最小限に抑え、既存のバッファを再利用します。
  5. 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.bufbufCache に戻そうと試みます。プールが満杯の場合、バッファはプールに戻されず、ガベージコレクタによって回収されます。
        • プールに戻された後、b.buf = nilb.r = 0b.w = 0 とすることで、Reader インスタンスがバッファへの参照を解放し、バッファが再利用可能であることを示します。
  6. 既存メソッドへの allocBuf()putBuf() の組み込み:

    • fill(): データをバッファに読み込む前に b.allocBuf() を呼び出し、バッファが確実に存在するようにします。
    • Read(), ReadByte(), ReadString(), WriteTo(), writeBuf(): これらのメソッドは、読み取り操作が完了し、バッファが空になった場合に b.putBuf() を呼び出すように変更されました。これにより、バッファが不要になった時点で速やかにプールに戻されます。
    • UnreadByte(): b.allocBuf() を呼び出し、バッファが nil の場合に確保するようにします。これは、UnreadByte がバッファにバイトを書き戻す必要があるためです。

bufio_test.go の変更点

  1. BenchmarkReaderEmpty の追加:
    • このベンチマークは、bufio.Reader が空になるまでデータを読み取るシナリオをシミュレートします。
    • strings.NewReader(str) から NewReader を作成し、io.Copy(ioutil.Discard, br) でデータを読み捨てます。
    • b.ReportAllocs() を呼び出すことで、メモリ確保の回数とバイト数をレポートに含めるようにしています。
    • このベンチマークは、バッファプーリングの効果を測定するために特に設計されており、コミットメッセージに記載されている劇的なパフォーマンス改善の根拠となっています。

これらの変更により、bufio.Reader はバッファをより動的に、かつ効率的に管理できるようになり、特に短命な Reader インスタンスが多数生成されるような状況でのメモリフットプリントとガベージコレクションの負荷を大幅に削減します。

関連リンク

参考にした情報源リンク