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

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

このコミットは、Go言語のbufioパッケージにおけるバッファの再利用メカニズムを廃止し、代わりにReaderおよびWriter型にResetメソッドを追加する変更を導入しています。これにより、バッファの管理が簡素化され、より明示的なリソース管理が可能になります。

コミット

commit ede9aa9e028e9bded416309981d8944d88366ffe
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sat Aug 10 19:22:19 2013 -0700

    bufio: drop buffer recycling, add Reader.Reset and Writer.Reset
    
    Fixes #6086
    
    R=golang-dev, pieter, r, rsc
    CC=golang-dev
    https://golang.org/cl/12603049

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

https://github.com/golang/go/commit/ede9aa9e028e9bded416309981d8944d88366ffe

元コミット内容

bufio: drop buffer recycling, add Reader.Reset and Writer.Reset

このコミットは、bufioパッケージにおけるバッファの再利用(リサイクル)機能を削除し、その代わりにReaderWriterの各型にResetメソッドを追加することを目的としています。これにより、バッファの管理方法が変更され、特定のユースケースでのパフォーマンスとリソース効率が改善される可能性があります。

変更の背景

Goのbufioパッケージは、I/O操作の効率を向上させるためにバッファリングを提供します。以前のバージョンでは、bufio.Readerbufio.Writerが使用する内部バッファを再利用するメカニズムが存在しました。これは、特に多数のReaderWriterが短期間に生成・破棄されるようなシナリオにおいて、ガベージコレクションの負荷を軽減し、メモリ割り当てを最適化することを意図していました。

しかし、バッファの再利用は、その実装が複雑になりがちであり、特定の条件下では予期せぬ挙動やデバッグの困難さを引き起こす可能性がありました。また、バッファの再利用が常に最適なパフォーマンスをもたらすとは限らず、むしろオーバーヘッドとなる場合もありました。

このコミットの背景には、おそらく以下の点が挙げられます。

  1. 複雑性の軽減: バッファの再利用ロジック(bufCacheチャネルを用いた実装)は、コードベースに一定の複雑性をもたらしていました。これを削除することで、コードの可読性と保守性が向上します。
  2. 明示的なリソース管理: Resetメソッドの導入により、ユーザーは既存のReaderWriterインスタンスを再利用し、新しいI/Oソースに切り替えることがより明示的に、かつ効率的に行えるようになります。これにより、不要なオブジェクトの再生成を避け、リソースの再利用を開発者が制御できるようになります。
  3. パフォーマンスの最適化: バッファの再利用が常にパフォーマンス上のメリットをもたらすとは限らないため、よりシンプルで予測可能なResetメカニズムに移行することで、全体的なパフォーマンスが改善される可能性があります。特に、バッファサイズがdefaultBufSizeと異なる場合の挙動が簡素化されます。
  4. Issue #6086の解決: コミットメッセージに「Fixes #6086」とあることから、この変更が特定のバグや問題の解決に関連していることが示唆されます。ただし、Goの公式Issueトラッカーで#6086を検索しても直接的な情報が見つからないため、これは内部的な追跡番号であるか、あるいは非常に古い、または特定のコンテキストでのみ参照される問題である可能性があります。一般的には、バッファの再利用に関する潜在的な問題や、Reader/Writerの再初期化に関する要望が背景にあると考えられます。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とbufioパッケージの基本的な知識が必要です。

  • io.Readerio.Writerインターフェース: Go言語における基本的なI/Oインターフェースです。Readerはデータを読み込むためのReadメソッドを、Writerはデータを書き込むためのWriteメソッドを定義します。
  • bufioパッケージ: io.Readerio.Writerをラップしてバッファリング機能を提供するパッケージです。これにより、ディスクI/OやネットワークI/Oなどの低レベルなI/O操作の回数を減らし、パフォーマンスを向上させます。
    • bufio.Reader: io.Readerからの読み込みをバッファリングします。
    • bufio.Writer: io.Writerへの書き込みをバッファリングします。
  • バッファリング: データを一度にまとめて読み書きすることで、システムコール(OSへのI/O要求)の回数を減らし、I/O効率を高める技術です。
  • ガベージコレクション (GC): Go言語のランタイムが自動的に不要になったメモリを解放するプロセスです。GCの頻度や負荷が高いと、アプリケーションのパフォーマンスに影響を与えることがあります。バッファの再利用は、GCの負荷を軽減する目的で導入されることがあります。
  • make([]byte, size, capacity): Goでスライスを作成する際の構文です。sizeはスライスの初期長を、capacityは基底配列の容量を指定します。capacityを指定することで、再割り当てなしでスライスを拡張できる最大サイズを制御できます。
  • sync.Pool: Go 1.3で導入された、一時的なオブジェクトを再利用するためのメカニズムです。このコミットが行われた時点(Go 1.1リリース後)ではsync.Poolはまだ存在していませんでしたが、バッファの再利用の文脈で言及されることがあります(コミット内のコメント// TODO: use a sync.Cache instead of this:は、将来的にsync.Poolのようなメカニズムを検討していたことを示唆しています)。
  • chan []byte: バイトスライスをやり取りするためのチャネルです。このコミット以前のバッファ再利用メカニズムでは、bufCacheというチャネルを使ってバッファをプールしていました。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. バッファ再利用メカニズムの削除:

    • bufio.Readerおよびbufio.Writerから、bufCacheというグローバルなチャネルを用いたバッファの再利用ロジックが削除されました。
    • 具体的には、ReaderallocBufputBufメソッド、およびWriterallocBufputBufメソッドが削除されています。
    • これにより、バッファは必要に応じてmake([]byte, size)で新しく割り当てられるようになり、不要になったバッファはGCの対象となります。
    • Reader構造体からbufSizeフィールドが削除され、バッファのサイズはlen(b.buf)で直接取得されるようになりました。
  2. Reader.Reset(r io.Reader)メソッドの追加:

    • Reader型にResetメソッドが追加されました。このメソッドは、既存のReaderインスタンスの内部状態(バッファ内のデータ、エラー状態、読み込み/書き込みポインタなど)をリセットし、新しいio.Readerソースから読み込みを開始できるようにします。
    • Resetは、既存のバッファ(b.buf)を再利用し、新しいio.Readerrdフィールドに設定します。これにより、Readerオブジェクト自体を再利用できるため、オブジェクトの再割り当てとGCの負荷を軽減できます。
  3. Writer.Reset(w io.Writer)メソッドの追加:

    • Writer型にResetメソッドが追加されました。このメソッドは、既存のWriterインスタンスの内部状態をリセットし、新しいio.Writerシンクに書き込みを開始できるようにします。
    • Resetは、バッファ内の未フラッシュデータを破棄し、エラー状態をクリアし、新しいio.Writerwrフィールドに設定します。Reader.Resetと同様に、Writerオブジェクトの再利用を可能にします。
  4. NewReaderSizeおよびNewWriterSizeの変更:

    • これらのコンストラクタ関数は、既存のReader/Writerが適切なサイズを持っている場合にそれを返すロジックを維持しつつ、バッファの割り当て方法が変更されました。
    • 特にNewReaderSizeでは、new(Reader)で新しいReaderオブジェクトを作成し、そのresetヘルパーメソッドを呼び出して初期化するようになりました。
  5. バッファサイズチェックの変更:

    • ReaderPeekReadメソッドにおけるバッファサイズチェックが、b.bufSizeからlen(b.buf)に変更されました。これは、bufSizeフィールドが削除されたことによる必然的な変更です。

これらの変更により、bufioパッケージは、バッファの明示的な再利用ではなく、ReaderWriterオブジェクト自体の再利用を推奨する設計になりました。これにより、開発者は必要に応じてオブジェクトをプールし、Resetメソッドを使って効率的に再利用することが可能になります。

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

このコミットにおける主要なコード変更は、src/pkg/bufio/bufio.gosrc/pkg/bufio/bufio_test.goに集中しています。

src/pkg/bufio/bufio.go

  • Reader構造体:
    • - bufSize int の削除。
  • NewReaderSize関数:
    • b.bufSize >= size から len(b.buf) >= size への変更。
    • r.buf = make([]byte, r.bufSize) の代わりに r := new(Reader); r.reset(make([]byte, size), rd) を使用。
  • バッファ再利用関連の関数/変数削除:
    • - const arbitrarySize = 8
    • - var bufCache = make(chan []byte, arbitrarySize)
    • - func (b *Reader) allocBuf() { ... } の削除。
    • - func (b *Reader) putBuf() { ... } の削除。
  • Reader.Resetメソッドの追加:
    • + func (b *Reader) Reset(r io.Reader) { ... }
    • + func (b *Reader) reset(buf []byte, r io.Reader) { ... } (内部ヘルパー)
  • Readerの各メソッドからのallocBufputBufの呼び出し削除:
    • fill, Read, ReadByte, UnreadByte, ReadString, writeBuf などから関連する行が削除。
  • Readerのバッファサイズ参照の変更:
    • b.bufSize から len(b.buf) への変更 (Peek, Read, ReadSlice 内)。
  • Writer構造体:
    • - bufSize int の削除。
  • NewWriterSize関数:
    • b.bufSize >= size から len(b.buf) >= size への変更。
    • b = &Writer{...} の代わりに return &Writer{buf: make([]byte, size), wr: w} を使用。
  • バッファ再利用関連の関数削除:
    • - func (b *Writer) allocBuf() { ... } の削除。
    • - func (b *Writer) putBuf() { ... } の削除。
  • Writer.Resetメソッドの追加:
    • + func (b *Writer) Reset(w io.Writer) { ... }
  • Writerの各メソッドからのallocBufputBufの呼び出し削除:
    • Flush, Write, WriteByte, WriteRune, WriteString, ReadFrom などから関連する行が削除。
  • Writerのバッファサイズ参照の変更:
    • b.bufSize - b.n から len(b.buf) - b.n への変更 (Available 内)。

src/pkg/bufio/bufio_test.go

  • TestReaderReset関数の追加:
    • Reader.Resetの動作を検証する新しいテストケース。
  • TestWriterReset関数の追加:
    • Writer.Resetの動作を検証する新しいテストケース。

コアとなるコードの解説

Readerの変更

以前のReaderは、bufSizeというフィールドでバッファの推奨サイズを保持し、allocBufputBufというメソッドを通じてbufCacheチャネルからバッファを「借りて」再利用しようとしていました。

// 削除されたコード (Reader.allocBuf)
var bufCache = make(chan []byte, arbitrarySize) // グローバルなバッファプール

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) // 新規作成
	}
}

// 削除されたコード (Reader.putBuf)
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:
		}
	}
}

このコミットでは、上記のallocBufputBuf、およびbufCacheが完全に削除されました。代わりに、Readerの初期化とリセットはより直接的になりました。

// 新しい Reader.Reset メソッド
func (b *Reader) Reset(r io.Reader) {
	b.reset(b.buf, r) // 既存のバッファを再利用してリセット
}

// 内部ヘルパー
func (b *Reader) reset(buf []byte, r io.Reader) {
	*b = Reader{ // 構造体全体を新しい値で上書き
		buf:          buf,
		rd:           r,
		lastByte:     -1,
		lastRuneSize: -1,
	}
}

NewReaderSize関数も変更され、Readerオブジェクトの作成と初期化にresetヘルパーを使用するようになりました。

func NewReaderSize(rd io.Reader, size int) *Reader {
	// ... (既存のReaderのチェック)
	r := new(Reader) // 新しいReaderオブジェクトを割り当て
	r.reset(make([]byte, size), rd) // 新しいバッファを割り当ててリセット
	return r
}

この変更により、Readerは常に自身の内部バッファを保持し、Resetが呼ばれたときにそのバッファを再利用して新しいI/Oソースに切り替えることができます。これにより、Readerオブジェクト自体の再利用が促進され、ガベージコレクションの圧力が軽減されます。

Writerの変更

WriterReaderと同様に、allocBufputBufを通じてバッファを再利用するロジックを持っていましたが、これも削除されました。

// 削除されたコード (Writer.allocBuf)
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)
	}
}

// 削除されたコード (Writer.putBuf)
func (b *Writer) putBuf() {
	if b.n == 0 && cap(b.buf) == defaultBufSize {
		select {
		case bufCache <- b.buf:
			b.buf = nil
		default:
		}
	}
}

WriterにもResetメソッドが追加され、既存のWriterインスタンスを新しいio.Writerシンクに切り替えることができるようになりました。

// 新しい Writer.Reset メソッド
func (b *Writer) Reset(w io.Writer) {
	b.err = nil // エラー状態をクリア
	b.n = 0     // バッファ内の書き込み済みバイト数をリセット
	b.wr = w    // 新しい io.Writer を設定
}

NewWriterSize関数も簡素化され、直接新しいWriterオブジェクトとバッファを割り当てるようになりました。

func NewWriterSize(w io.Writer, size int) *Writer {
	// ... (既存のWriterのチェック)
	return &Writer{ // 新しいWriterオブジェクトとバッファを直接作成
		buf: make([]byte, size),
		wr:  w,
	}
}

これらの変更により、bufioパッケージは、バッファの再利用を内部的な複雑なメカニズムに頼るのではなく、ReaderWriterオブジェクト自体を再利用するための明示的なAPI(Resetメソッド)を提供するようになりました。これにより、開発者は必要に応じてこれらのオブジェクトをプールし、効率的に再利用することが可能になります。

関連リンク

参考にした情報源リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • コミットの変更内容 (Go Gerrit): https://golang.org/cl/12603049 (これはコミットメッセージに記載されているリンクであり、GitHubのコミットページと同じ内容を指しています。)
  • Go言語のIssueトラッカー: https://github.com/golang/go/issues (Issue #6086に関する直接的な情報は確認できませんでした。)
  • Go言語のsync.Poolに関する情報 (Go 1.3で導入): https://pkg.go.dev/sync#Pool (このコミット時点では存在しませんが、バッファ再利用の文脈で関連する概念です。)