[インデックス 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
(現在の書き込み位置) が変化するシナリオも考慮に入れています。これは、Grow
がb.off
を考慮して容量を確保する必要があるためです。- 最も重要な部分は、
runtime.ReadMemStats
を使用して、buf.Write(yBytes)
の呼び出し前後でメモリ割り当て (m1.Mallocs
とm2.Mallocs
) が増加していないことを確認している点です。これにより、Grow
メソッドが期待通りに再割り当てを抑制していることが検証されます。 runtime.GOMAXPROCS(-1) == 1
のチェックは、Goのランタイムがシングルスレッドモードで動作している場合にのみメモリ割り当てのチェックを行うためのものです。マルチスレッド環境では、他のゴルーチンによるメモリ割り当てがテスト結果に影響を与える可能性があるため、この条件が設けられています。- 最後に、
buf.Bytes()
を使ってバッファの内容を検証し、初期データと書き込まれたデータが正しいことを確認しています。
この変更により、開発者は bytes.Buffer
を使用する際に、パフォーマンスが重要な場面でメモリ割り当てをより細かく制御できるようになりました。
関連リンク
- Go言語の
bytes
パッケージのドキュメント: https://pkg.go.dev/bytes - Go言語の
bytes.Buffer
のソースコード (現在のバージョン): https://cs.opensource.google/go/go/+/refs/tags/go1.22.4:src/bytes/buffer.go - Go言語のメモリ管理とガベージコレクションに関する情報: https://go.dev/doc/gc-guide
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコードリポジトリ
- Go言語のメモリ管理に関する一般的な情報源
runtime.MemStats
のドキュメントbytes.Buffer
の内部実装に関する議論(GoコミュニティのメーリングリストやIssueトラッカーなど)