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

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

このコミットは、Go言語の標準ライブラリ bytes パッケージ内の Buffer 型に Grow メソッドをエクスポート(公開)する変更を導入しています。これにより、Buffer の利用者が事前に必要なバッファ領域を確保できるようになり、動的な再割り当て(reallocation)によるパフォーマンスオーバーヘッドを回避することが可能になります。

コミット

commit 1255a6302d83148d41f78b7c7b49cacad8139bdc
Author: Rob Pike <r@golang.org>
Date:   Thu Jul 12 20:52:19 2012 -0700

    bytes.Buffer: export the Grow method
    Allows a client to pre-allocate buffer space that is known to be necessary,
    avoiding expensive reallocations.
    
    R=gri, gri, adg
    CC=golang-dev
    https://golang.org/cl/6392061

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1255a6302d83148d41f78b7c7b49cacad8139bdc

元コミット内容

bytes.Buffer: export the Grow method Allows a client to pre-allocate buffer space that is known to be necessary, avoiding expensive reallocations.

変更の背景

bytes.Buffer は、可変長のバイトシーケンスを扱うためのGo言語の標準的なデータ構造です。これは、文字列の構築や、ネットワークI/O、ファイルI/Oなど、動的にデータサイズが変化する場面で頻繁に利用されます。

従来の bytes.Buffer は、データが追加されるたびに内部のバッファが不足した場合、自動的にバッファのサイズを拡張していました。この拡張処理は、既存のデータをより大きな新しいメモリ領域にコピーし、古いメモリ領域を解放するという「再割り当て(reallocation)」を伴います。再割り当ては、特に大量のデータが頻繁に追加されるようなシナリオでは、CPU時間とメモリ帯域を消費する高コストな操作となり、アプリケーションのパフォーマンスに悪影響を与える可能性がありました。

このコミットの背景には、このような再割り当てのオーバーヘッドを開発者が明示的に制御し、最適化できるようにするという目的があります。事前に必要なバッファサイズが分かっている場合、Grow メソッドを使って一度に十分なメモリを確保することで、その後のデータ追加における不要な再割り当てを抑制し、パフォーマンスを向上させることができます。

前提知識の解説

Go言語の bytes.Buffer

bytes.Buffer は、io.Reader および io.Writer インターフェースを実装しており、バイト列の読み書きを効率的に行うための型です。内部的には []byte スライスを保持しており、このスライスがデータの格納領域となります。データが追加されると、必要に応じてこの内部スライスの容量が自動的に拡張されます。

メモリの再割り当て(Reallocation)

プログラムが実行中に動的にメモリを確保する際、既に確保されているメモリ領域が不足した場合、より大きなメモリ領域を新たに確保し、既存のデータをその新しい領域にコピーする操作を「再割り当て」と呼びます。この操作は、特に大きなデータ構造や頻繁なデータ追加が行われる場合に、パフォーマンスのボトルネックとなることがあります。再割り当ての頻度を減らすことは、アプリケーションの実行速度を向上させる上で重要な最適化手法の一つです。

エクスポート(Export)と非エクスポート(Unexport)

Go言語では、識別子(変数名、関数名、メソッド名など)の最初の文字が大文字である場合、その識別子はパッケージ外からアクセス可能(エクスポートされている)になります。一方、最初の文字が小文字である場合、その識別子はパッケージ内からのみアクセス可能(非エクスポートされている)です。このコミットでは、元々非エクスポートされていた grow メソッドを Grow という名前でエクスポートすることで、外部から利用できるようにしています。

技術的詳細

このコミットの主要な変更点は、bytes.Buffer 型に Grow(n int) メソッドを追加し、これをエクスポートしたことです。

Grow メソッドのシグネチャは func (b *Buffer) Grow(n int) です。

  • n は、追加で確保したいバイト数を指定します。
  • Grow メソッドは、内部的に非エクスポートの grow(n int) メソッドを呼び出します。この grow メソッドが実際のバッファ拡張ロジックを担当します。
  • Grow メソッドは、n が負の値の場合にパニック(panic)を発生させます。これは、負のバイト数を確保しようとすることが論理的に誤りであるためです。
  • また、バッファが拡張できない場合(例えば、非常に大きなサイズを要求し、システムメモリが不足する場合など)には、ErrTooLarge でパニックを発生させる可能性があります。

Grow メソッドが呼び出されると、bytes.Buffer の内部スライス b.buf の容量が、少なくとも b.off + n バイトを格納できるように調整されます。ここで b.off は、バッファ内の現在の書き込み位置(つまり、既に書き込まれているデータの長さ)を示します。これにより、Grow(n) の呼び出し後には、少なくとも n バイトを再割り当てなしでバッファに書き込むことが保証されます。

テストケース TestGrow では、Grow メソッドの動作が検証されています。特に注目すべきは、runtime.MemStats を使用して、Grow 呼び出し後に Write 操作を行った際にメモリ割り当てが発生しないことを確認している点です。これは、Grow メソッドが意図通りに再割り当てを抑制していることを証明するための重要なテストです。runtime.GOMAXPROCS(-1) == 1 の条件は、シングルスレッド環境でのメモリ割り当てを正確に測定するためのものです。

コアとなるコードの変更箇所

src/pkg/bytes/buffer.go

--- a/src/pkg/bytes/buffer.go
+++ b/src/pkg/bytes/buffer.go
@@ -99,6 +99,19 @@ func (b *Buffer) grow(n int) int {
 	return b.off + m
 }
 
+// Grow grows the buffer's capacity, if necessary, to guarantee space for
+// another n bytes. After Grow(n), at least n bytes can be written to the
+// buffer without another allocation.
+// If n is negative, Grow will panic.
+// If the buffer can't grow it will panic with ErrTooLarge.
+func (b *Buffer) Grow(n int) {
+	if n < 0 {
+		panic("bytes.Buffer.Grow: negative count")
+	}
+	m := b.grow(n)
+	b.buf = b.buf[0:m]
+}
+
 // Write appends the contents of p to the buffer.  The return
 // value n is the length of p; err is always nil.
 // If the buffer becomes too large, Write will panic with

src/pkg/bytes/buffer_test.go

--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -8,6 +8,7 @@ import (
 	. "bytes"
 	"io"
 	"math/rand"
+	"runtime"
 	"testing"
 	"unicode/utf8"
 )
@@ -374,6 +375,37 @@ func TestReadBytes(t *testing.T) {
 	}
 }
 
+func TestGrow(t *testing.T) {
+	x := []byte{'x'}
+	y := []byte{'y'}
+	tmp := make([]byte, 72)
+	for _, startLen := range []int{0, 100, 1000, 10000, 100000} {
+		xBytes := Repeat(x, startLen)
+		for _, growLen := range []int{0, 100, 1000, 10000, 100000} {
+			buf := NewBuffer(xBytes)
+			// If we read, this affects buf.off, which is good to test.
+			readBytes, _ := buf.Read(tmp)
+			buf.Grow(growLen)
+			yBytes := Repeat(y, growLen)
+			// Check no allocation occurs in write, as long as we're single-threaded.
+			var m1, m2 runtime.MemStats
+			runtime.ReadMemStats(&m1)
+			buf.Write(yBytes)
+			runtime.ReadMemStats(&m2)
+			if runtime.GOMAXPROCS(-1) == 1 && m1.Mallocs != m2.Mallocs {
+				t.Errorf("allocation occurred during write")
+			}
+			// Check that buffer has correct data.
+			if !Equal(buf.Bytes()[0:startLen-readBytes], xBytes[readBytes:]) {
+				t.Errorf("bad initial data at %d %d", startLen, growLen)
+			}
+			if !Equal(buf.Bytes()[startLen-readBytes:startLen-readBytes+growLen], yBytes) {
+				t.Errorf("bad written data at %d %d", startLen, growLen)
+			}
+		}
+	}
+}
+
 // Was a bug: used to give EOF reading empty slice at EOF.
 func TestReadEmptyAtEOF(t *testing.T) {
 	b := new(Buffer)

コアとなるコードの解説

src/pkg/bytes/buffer.go の変更

  • func (b *Buffer) Grow(n int) メソッドが追加されました。
  • このメソッドは、引数 n が負の値でないことを確認し、負の値の場合はパニックを発生させます。
  • 実際のバッファ拡張処理は、非エクスポートの b.grow(n) メソッドに委譲されます。grow メソッドは、必要な容量を計算し、内部バッファ b.buf を拡張します。
  • b.buf = b.buf[0:m] の行は、grow メソッドによって返された新しい容量 m に合わせて、スライスの長さを調整しています。これにより、Grow が呼び出された後に、追加の n バイトを再割り当てなしで書き込めるようになります。

src/pkg/bytes/buffer_test.go の変更

  • TestGrow という新しいテスト関数が追加されました。
  • このテストは、bytes.Buffer の初期サイズ (startLen) と Grow で確保する追加サイズ (growLen) を様々な組み合わせでテストしています。
  • buf.Read(tmp) を呼び出すことで、buf.off (現在の書き込み位置) が変化するシナリオも考慮に入れています。これは、Growb.off を考慮して容量を確保する必要があるためです。
  • 最も重要な部分は、runtime.ReadMemStats を使用して、buf.Write(yBytes) の呼び出し前後でメモリ割り当て (m1.Mallocsm2.Mallocs) が増加していないことを確認している点です。これにより、Grow メソッドが期待通りに再割り当てを抑制していることが検証されます。
  • runtime.GOMAXPROCS(-1) == 1 のチェックは、Goのランタイムがシングルスレッドモードで動作している場合にのみメモリ割り当てのチェックを行うためのものです。マルチスレッド環境では、他のゴルーチンによるメモリ割り当てがテスト結果に影響を与える可能性があるため、この条件が設けられています。
  • 最後に、buf.Bytes() を使ってバッファの内容を検証し、初期データと書き込まれたデータが正しいことを確認しています。

この変更により、開発者は bytes.Buffer を使用する際に、パフォーマンスが重要な場面でメモリ割り当てをより細かく制御できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコードリポジトリ
  • Go言語のメモリ管理に関する一般的な情報源
  • runtime.MemStats のドキュメント
  • bytes.Buffer の内部実装に関する議論(GoコミュニティのメーリングリストやIssueトラッカーなど)