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

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

このコミットは、Go言語の標準ライブラリである io/ioutil パッケージにおける Discard 関数の実装を改善するものです。具体的には、Discard 関数が内部で使用するバッファの管理に sync.Pool を導入し、それに伴い以前は別のファイル (blackhole.go) に分離されていた関連ロジックを ioutil.go に統合しています。

コミット

commit 568a449bd1992133d8fa444cafefa688dd423d42
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Dec 20 09:38:35 2013 -0800

    io/ioutil: use sync.Pool in Discard
    
    And merge the blackhole.go file back into ioutil,
    where it once was. It was only in a separate file
    because it used to have race-vs-!race versions.
    
    R=golang-codereviews, rsc
    CC=golang-codereviews
    https://golang.org/cl/44060044

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

https://github.com/golang/go/commit/568a449bd1992133d8fa444cafefa688dd423d42

元コミット内容

io/ioutil: use sync.Pool in Discard And merge the blackhole.go file back into ioutil, where it once was. It was only in a separate file because it used to have race-vs-!race versions.

変更の背景

この変更の主な背景は、io/ioutil.Discard 関数がデータを読み捨てる際に使用する一時バッファの効率的な管理です。以前の実装では、blackhole.go という独立したファイルで定義された blackHole() および blackHolePut() 関数を通じて、チャネルを用いたバッファの再利用が行われていました。しかし、この方法はバッファの取得と解放にチャネル操作が伴うため、オーバーヘッドが発生する可能性がありました。

Go 1.1で導入された sync.Pool は、一時的なオブジェクトの再利用を効率的に行うためのメカニズムを提供します。Discard のような頻繁に呼び出され、一時的なバッファを必要とする関数において、sync.Pool を利用することで、ガベージコレクションの負荷を軽減し、パフォーマンスを向上させることが期待できます。

また、blackhole.go が独立したファイルとして存在していたのは、Goのビルドシステムにおける「race-vs-!race」ビルドタグのサポートに関連していました。特定の条件下で異なる実装を提供する必要があったため、ファイルが分割されていました。しかし、このコミットの時点ではその必要性がなくなり、コードの整理と簡潔化のために ioutil.go へ統合されることになりました。これにより、関連するロジックが一箇所にまとまり、コードの可読性と保守性が向上します。

前提知識の解説

io/ioutil.Discard

io/ioutil.Discard は、Go言語の標準ライブラリ io/ioutil パッケージに含まれる io.Writer インターフェースを実装した型です。この型は、書き込まれたデータをすべて破棄(discard)する、いわゆる「ブラックホール」のような振る舞いをします。つまり、Discard にデータを書き込んでも、そのデータはどこにも保存されず、単に捨てられます。

主な用途としては、以下のようなケースが挙げられます。

  • 不要な出力の抑制: 例えば、ある関数が io.Writer を引数にとり、通常はログや結果を出力するが、テスト時や特定の状況下ではその出力が不要な場合に ioutil.Discard を渡すことで、出力を抑制できます。
  • データの読み捨て: io.Copy(ioutil.Discard, reader) のように使用することで、reader からデータを読み込み、そのデータをすべて破棄することができます。これは、ネットワークストリームの残りを読み切って接続をクリーンに閉じたい場合や、プロトコルの仕様上、特定のデータを読み飛ばす必要がある場合などに利用されます。

sync.Pool

sync.Pool は、Go言語の標準ライブラリ sync パッケージに含まれる型で、一時的なオブジェクトの再利用を目的とした同期プリミティブです。sync.Pool は、頻繁に生成・破棄されるオブジェクト(例えば、バッファや構造体など)をプールしておき、必要に応じてプールから取得し、使用後にプールに戻すことで、ガベージコレクタの負担を軽減し、アプリケーションのパフォーマンスを向上させることができます。

sync.Pool の主な特徴は以下の通りです。

  • 一時的なオブジェクトの再利用: sync.Pool は、ガベージコレクタがオブジェクトを回収する前に、それらを再利用可能な状態に保ちます。これにより、オブジェクトの割り当てと解放のコストを削減できます。
  • スレッドセーフ: 複数のゴルーチンから安全にアクセスできるように設計されています。
  • Get() メソッド: プールからオブジェクトを取得します。プールが空の場合、New フィールドに設定された関数が呼び出され、新しいオブジェクトが生成されます。
  • Put() メソッド: 使用済みのオブジェクトをプールに戻します。
  • ガベージコレクションによるクリア: sync.Pool に格納されたオブジェクトは、ガベージコレクションの実行時に自動的にクリアされる可能性があります。これは、sync.Pool があくまで「一時的な」オブジェクトの再利用を目的としているためです。そのため、プールに格納されるオブジェクトは、その状態が失われても問題ない、または簡単に再初期化できるものである必要があります。

Goのビルドタグ (race!race)

Goのビルドタグは、コンパイル時に特定のコードを含めるか除外するかを制御するためのメカニズムです。ファイル名の末尾に _GOOS_GOARCH などのサフィックスを付けることで、特定のオペレーティングシステムやアーキテクチャ向けにコードを限定できます。同様に、_race_!race といったサフィックスは、Goのデータ競合検出器(race detector)が有効になっているかどうかによってコードを切り替えるために使用されます。

  • _race: データ競合検出器が有効な場合にのみコンパイルされるコード。
  • _!race: データ競合検出器が有効でない場合にのみコンパイルされるコード。

以前の blackhole.go は、データ競合検出器の有無によって異なる実装が必要だったため、このようにファイルが分割されていたと考えられます。しかし、このコミットの時点ではその必要性がなくなり、単一の実装で対応できるようになったため、ファイルが統合されました。

技術的詳細

このコミットの技術的な核心は、io/ioutil.Discard の内部実装におけるバッファ管理戦略の変更です。

変更前は、blackhole.go ファイルに定義された blackHole()blackHolePut() 関数が、チャネル (blackHoleBuf) を介して []byte 型のバッファを再利用していました。

// blackhole.go (変更前)
var blackHoleBuf = make(chan []byte, 1) // バッファを1つだけ保持するチャネル

func blackHole() []byte {
	select {
	case b := <-blackHoleBuf: // チャネルからバッファを取得
		return b
	default:
	}
	return make([]byte, 8192) // チャネルが空なら新しいバッファを生成
}

func blackHolePut(p []byte) {
	select {
	case blackHoleBuf <- p: // チャネルにバッファを戻す
	default: // チャネルが満杯ならバッファを捨てる
	}
}

このチャネルベースのバッファプールは、単純なケースでは機能しますが、sync.Pool に比べていくつかの欠点があります。

  1. オーバーヘッド: チャネル操作は、直接的なメモリ割り当てやポインタ操作に比べて、コンテキストスイッチや同期のオーバーヘッドが発生する可能性があります。
  2. 容量の制限: make(chan []byte, 1) のように容量が固定されているため、同時に複数のバッファが必要になった場合に効率が悪くなります。
  3. ガベージコレクションとの連携: チャネル内のオブジェクトはガベージコレクションの対象外となるため、明示的に取り出さない限りメモリに残り続けます。

変更後、blackhole.go は削除され、その機能は ioutil.go に統合されました。そして、バッファの再利用には sync.Pool が使用されるようになりました。

// ioutil.go (変更後)
var blackHolePool = sync.Pool{
	New: func() interface{} {
		b := make([]byte, 8192)
		return &b // ポインタをプールに格納
	},
}

func (devNull) ReadFrom(r io.Reader) (n int64, err error) {
	bufp := blackHolePool.Get().(*[]byte) // プールからバッファのポインタを取得
	// ...
	readSize, err = r.Read(*bufp) // ポインタをデリファレンスしてバッファを使用
	// ...
	if err != nil {
		blackHolePool.Put(bufp) // エラー発生時もバッファをプールに戻す
		// ...
	}
	// ...
}

sync.PoolNew フィールドには、プールが空のときに新しいバッファを生成するための関数が設定されています。ここでは、8192バイトの []byte スライスを生成し、そのポインタ (*[]byte) をプールに格納しています。Get() メソッドで取得したオブジェクトは interface{} 型で返されるため、適切な型アサーション (.(*[]byte)) が必要です。

DiscardReadFrom メソッド内で、blackHolePool.Get() でバッファを取得し、r.Read(*bufp) でそのバッファにデータを読み込みます。処理が完了するか、エラーが発生した場合には、blackHolePool.Put(bufp) を呼び出してバッファをプールに戻します。これにより、同じバッファが将来の Discard 呼び出しで再利用される可能性が高まります。

sync.Pool を使用することで、以下の利点が得られます。

  • ガベージコレクションの削減: 頻繁な make([]byte, 8192) の呼び出しが減り、ガベージコレクタが処理すべきオブジェクトの数が減少します。これにより、GCの一時停止時間が短縮され、全体的なパフォーマンスが向上します。
  • 効率的なバッファ管理: sync.Pool は内部的にCPUコアごとにローカルなプールを持つなど、並行アクセスに最適化された設計になっています。これにより、チャネルベースの実装よりも効率的にバッファの取得と解放が行われます。
  • コードの簡潔化: blackhole.go という独立したファイルが不要になり、関連するロジックが ioutil.go に集約されることで、コードベースがより整理され、理解しやすくなります。

この変更は、Goランタイムの低レベルな部分におけるパフォーマンス最適化の一例であり、特にI/O処理のように頻繁に一時的なバッファが必要とされる場面でその効果を発揮します。

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

このコミットにおけるコアとなるコードの変更は、以下の2つのファイルに集中しています。

  1. src/pkg/io/ioutil/blackhole.go の削除

    • このファイル全体が削除されました。以前は Discard 関数が使用するバッファ管理ロジック (blackHole()blackHolePut()) を含んでいました。
  2. src/pkg/io/ioutil/ioutil.go の変更

    • sync パッケージがインポートに追加されました。

      --- a/src/pkg/io/ioutil/ioutil.go
      +++ b/src/pkg/io/ioutil/ioutil.go
      @@ -10,6 +10,7 @@ import (
       	"io"
       	"os"
       	"sort"
      +	"sync"
       )
      
    • devNull 型(Discard の実体)の ReadFrom メソッド内で使用されるバッファ管理ロジックが、sync.Pool を使用するように変更されました。

      • blackHoleBuf チャネルと blackHole() / blackHolePut() の呼び出しが削除されました。
      • blackHolePool という sync.Pool のインスタンスが新しく定義されました。このプールは、8192バイトの []byte スライスへのポインタを格納します。
      • ReadFrom メソッド内で、blackHolePool.Get() でバッファのポインタを取得し、r.Read(*bufp) でそのバッファを使用します。
      • 処理の終了時(エラー発生時を含む)に、blackHolePool.Put(bufp) でバッファのポインタをプールに戻します。
      --- a/src/pkg/io/ioutil/ioutil.go
      +++ b/src/pkg/io/ioutil/ioutil.go
      @@ -136,14 +137,21 @@ func (devNull) WriteString(s string) (int, error) {
       	return len(s), nil
       }
       
      +var blackHolePool = sync.Pool{
      +	New: func() interface{} {
      +		b := make([]byte, 8192)
      +		return &b
      +	},
      +}
      +
       func (devNull) ReadFrom(r io.Reader) (n int64, err error) {
      -	buf := blackHole()
      -	defer blackHolePut(buf)
      +	bufp := blackHolePool.Get().(*[]byte)
        	readSize := 0
        	for {
      -		readSize, err = r.Read(buf)
      +		readSize, err = r.Read(*bufp)
        		n += int64(readSize)
        		if err != nil {
      +			blackHolePool.Put(bufp)
        			if err == io.EOF {
        				return n, nil
        			}
      

コアとなるコードの解説

src/pkg/io/ioutil/blackhole.go の削除

このファイルは、io/ioutil.Discard が内部的に使用するバッファを管理するためのロジックを含んでいました。具体的には、blackHoleBuf というチャネルを介して、8192バイトのバッファを1つだけ再利用する仕組みでした。このファイルが削除されたのは、sync.Pool の導入により、より効率的で汎用的なバッファ再利用メカニズムが利用可能になったためです。また、以前は「race-vs-!race」ビルドタグのために分離されていたという歴史的経緯も、この時点では解消されていたため、コードの整理の一環として統合されました。

src/pkg/io/ioutil/ioutil.go の変更

  1. import "sync" の追加: sync.Pool を使用するために、sync パッケージがインポートリストに追加されました。これは、Goの標準ライブラリの同期プリミティブを提供するパッケージです。

  2. blackHolePool の定義:

    var blackHolePool = sync.Pool{
    	New: func() interface{} {
    		b := make([]byte, 8192)
    		return &b
    	},
    }
    

    ここで、sync.Pool のインスタンスである blackHolePool がグローバル変数として定義されています。

    • New フィールドには、プールが空のときに新しいオブジェクトを生成するための関数リテラルが設定されています。
    • この関数は、make([]byte, 8192) を呼び出して8192バイトの新しいバイトスライスを作成します。
    • 重要なのは、return &b としている点です。sync.Poolinterface{} を格納するため、値型である []byte を直接格納すると、Get() で取得した際にスライスのヘッダ情報(ポインタ、長さ、容量)がコピーされるだけで、基盤となる配列はコピーされません。しかし、Put() で戻されたスライスが別のゴルーチンで変更されると、予期せぬデータ競合が発生する可能性があります。*[]byte (バイトスライスへのポインタ) を格納することで、Get() で取得したポインタをデリファレンスしてスライスを操作し、Put() で同じポインタを戻すことで、基盤となる配列の再利用を安全に行うことができます。これにより、スライスヘッダのコピーではなく、基盤配列の再利用が促進されます。
  3. func (devNull) ReadFrom(r io.Reader) (n int64, err error) の変更: このメソッドは、io.Reader からデータを読み込み、それを破棄する io.Copy のような動作を内部的に行います。

    • bufp := blackHolePool.Get().(*[]byte): blackHolePool.Get() を呼び出して、プールからバッファのポインタを取得します。Get()interface{} を返すため、(*[]byte) への型アサーションが必要です。これにより、bufp*[]byte 型の変数となり、バッファの実体へのポインタを保持します。
    • readSize, err = r.Read(*bufp): r.Read() メソッドに、bufp が指すバイトスライス (*bufp) を渡してデータを読み込みます。これにより、r から読み込まれたデータがこの再利用されたバッファに格納されます。
    • if err != nil { blackHolePool.Put(bufp) ... }: r.Read() がエラーを返した場合(io.EOF を含む)、取得したバッファのポインタ bufpblackHolePool.Put(bufp) を使ってプールに戻します。これにより、バッファが他の Discard 呼び出しで再利用できるようになります。defer を使わずに条件分岐内で Put を呼び出しているのは、io.EOF 以外のエラーが発生した場合でもバッファを確実にプールに戻すためです。io.EOF の場合は、ループを抜ける直前に return されるため、その前に Put が実行されます。

この変更により、Discard 関数がデータを読み捨てる際に、毎回新しいバッファを割り当てるのではなく、sync.Pool から既存のバッファを再利用できるようになりました。これにより、メモリ割り当ての回数が減少し、ガベージコレクションの負荷が軽減され、特に高頻度で Discard が使用されるシナリオでのパフォーマンスが向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • sync.Pool の利用に関する一般的なGoプログラミングのベストプラクティス
  • Go言語のデータ競合検出器に関する情報