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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおけるパフォーマンス改善を目的としています。具体的には、fmtパッケージ内で使用されるpp(printer)とss(scanner)という内部構造体の再利用メカニズムを、カスタム実装のキャッシュからGo標準ライブラリのsync.Poolへと移行しています。これにより、オブジェクトの割り当てとガベージコレクションのオーバーヘッドを削減し、fmtパッケージの処理速度向上に貢献します。

コミット

commit 0f9311811c037891876e4b151c55351299cb588f
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Dec 18 11:09:07 2013 -0800

    fmt: use sync.Pool
    
    Update #4720
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/43990043

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

https://github.com/golang/go/commit/0f9311811c037891876e4b151c55351299cb588f

元コミット内容

fmt: use sync.Pool

Update #4720

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/43990043

変更の背景

この変更の背景には、Go言語のfmtパッケージが頻繁に利用されることによるパフォーマンス上の課題がありました。fmtパッケージは、文字列のフォーマット(fmt.Printfなど)やスキャン(fmt.Scanfなど)を行う際に、内部的にpp(printer)やss(scanner)といった一時的な構造体を生成して使用します。これらの構造体は、処理が完了すると不要になり、ガベージコレクタによって回収される必要があります。

しかし、これらの構造体が非常に頻繁に生成・破棄されると、ガベージコレクションの負荷が増大し、アプリケーション全体のパフォーマンスに悪影響を与える可能性があります。特に、高負荷なサーバーアプリケーションや、大量のログ出力を行うようなシナリオでは、このオーバーヘッドが顕著になります。

この問題を解決するため、Goの標準ライブラリにはsync.Poolという機能が導入されました。sync.Poolは、一時的に使用されるオブジェクトをキャッシュし、再利用することで、ガベージコレクションの負担を軽減し、パフォーマンスを向上させるためのメカニズムを提供します。

このコミット以前のfmtパッケージでは、cacheという独自の構造体とそれに関連するメソッド(put, get, newCache)を使って、ppss構造体のプールを自前で実装していました。しかし、sync.Poolが標準ライブラリとして提供されたことで、より効率的で標準的な方法でオブジェクトプールを管理できるようになりました。このコミットは、その既存のカスタムキャッシュ実装をsync.Poolに置き換えることで、fmtパッケージのパフォーマンスと保守性を向上させることを目的としています。

関連するIssue #4720は、fmtパッケージにおけるsync.Poolの利用を提案していたものと考えられます。

前提知識の解説

Go言語のfmtパッケージ

fmtパッケージは、Go言語における基本的な入出力フォーマット機能を提供します。C言語のprintf/scanfライクな関数群を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりするために使用されます。

  • fmt.Printf: 指定されたフォーマット文字列に従って値を整形し、標準出力に出力します。
  • fmt.Sprintf: 指定されたフォーマット文字列に従って値を整形し、文字列として返します。
  • fmt.Scanf: 標準入力からフォーマット文字列に従って値を読み込みます。

これらの関数は内部的に、フォーマットやスキャンの処理を効率的に行うために、pp(printer)やss(scanner)といった内部構造体を利用します。

sync.Pool

sync.Poolは、Go言語のsyncパッケージで提供される、一時的なオブジェクトの再利用を目的とした同期プリミティブです。その主な目的は、ガベージコレクションの負荷を軽減し、アプリケーションのパフォーマンスを向上させることです。

sync.Poolの仕組み:

  1. オブジェクトの取得 (Get()):
    • Pool内に利用可能なオブジェクトがあれば、それを返します。
    • 利用可能なオブジェクトがなければ、PoolNewフィールドに設定された関数を呼び出し、新しいオブジェクトを生成して返します。
  2. オブジェクトの返却 (Put()):
    • 使用済みのオブジェクトをPoolに戻します。これにより、そのオブジェクトは将来のGet()呼び出しで再利用される可能性があります。

sync.Poolの特性と注意点:

  • 一時的なオブジェクトのプール: sync.Poolは、短期間だけ使用され、その後すぐに不要になるようなオブジェクト(例: バッファ、一時的な構造体)のプールに適しています。
  • ガベージコレクションによるクリア: sync.Pool内のオブジェクトは、ガベージコレクションの実行時に自動的にクリアされる可能性があります。これは、sync.Poolがメモリ使用量を無制限に増やさないようにするための設計です。したがって、sync.Poolに格納されたオブジェクトが永続的に利用可能であると期待してはいけません。Get()を呼び出すたびに、新しいオブジェクトが生成される可能性があることを考慮する必要があります。
  • スレッドセーフ: sync.Poolはスレッドセーフであり、複数のゴルーチンから同時にアクセスしても安全です。
  • ゼロ値の利用: sync.Poolから取得したオブジェクトは、以前の状態が残っている可能性があるため、再利用する前に必ず初期化(ゼロ値に戻すなど)する必要があります。New関数で生成されるオブジェクトも同様です。

オブジェクトプールとは

オブジェクトプールは、プログラムのパフォーマンスを最適化するためのデザインパターンの一つです。特に、オブジェクトの生成と破棄が頻繁に行われる場合に有効です。

メリット:

  • メモリ割り当ての削減: オブジェクトを再利用することで、新しいメモリ割り当ての回数を減らします。
  • ガベージコレクションの負荷軽減: オブジェクトの生成と破棄が減るため、ガベージコレクタが実行される頻度や、その際の処理時間が短縮されます。これにより、アプリケーションの応答性が向上し、レイテンシが低減されます。
  • 初期化コストの削減: オブジェクトの初期化にコストがかかる場合、再利用することでそのコストを削減できます。

デメリット/注意点:

  • 複雑性の増加: プール管理のロジックを実装する必要があり、コードが複雑になる可能性があります。
  • 状態の管理: プールから取得したオブジェクトは、以前の状態が残っている可能性があるため、使用前に適切にリセットする必要があります。
  • メモリリークのリスク: オブジェクトをプールに返却し忘れると、メモリリークにつながる可能性があります。

Goのsync.Poolは、これらのオブジェクトプール管理の複雑さをGoランタイムが吸収し、効率的な再利用メカニズムを標準で提供するものです。

技術的詳細

このコミットの主要な変更点は、fmtパッケージ内で使用されていたカスタムのオブジェクトキャッシュ実装を、Go標準ライブラリのsync.Poolに置き換えたことです。

変更前のカスタムキャッシュ実装

変更前は、fmtパッケージ内にcacheという独自の構造体が定義されていました。

type cache struct {
	mu    sync.Mutex
	saved []interface{}
	new   func() interface{}
}

func (c *cache) put(x interface{}) {
	c.mu.Lock()
	if len(c.saved) < cap(c.saved) {
		c.saved = append(c.saved, x)
	}
	c.mu.Unlock()
}

func (c *cache) get() interface{} {
	c.mu.Lock()
	n := len(c.saved)
	if n == 0 {
		c.mu.Unlock()
		return c.new()
	}
	x := c.saved[n-1]
	c.saved = c.saved[0 : n-1]
	c.mu.Unlock()
	return x
}

func newCache(f func() interface{}) *cache {
	return &cache{saved: make([]interface{}, 0, 100), new: f}
}

このカスタムcacheは、以下の特徴を持っていました。

  • sync.Mutexを使用して、複数のゴルーチンからのアクセスを同期していました。
  • savedという[]interface{}スライスをLIFO(後入れ先出し)スタックとして使用し、オブジェクトを保存していました。
  • newフィールドにオブジェクトを新しく生成する関数を保持していました。
  • putメソッドでオブジェクトをプールに戻し、getメソッドでオブジェクトを取得していました。プールが空の場合はnew関数を呼び出して新しいオブジェクトを生成していました。
  • newCache関数で、初期容量100のsavedスライスを持つcacheインスタンスを生成していました。

このカスタム実装は、基本的なオブジェクトプールの機能を提供していましたが、sync.Poolが提供するより高度な最適化(例えば、ガベージコレクションとの連携や、より効率的なロックメカニズム)は含まれていませんでした。

sync.Poolへの移行

このコミットでは、上記のカスタムcache実装を削除し、代わりにsync.Poolを使用するように変更しました。

具体的には、ppFreessFreeという2つのグローバル変数(それぞれpp構造体とss構造体のプール)が、newCache関数で初期化される*cache型から、直接sync.Pool型に変わりました。

// 変更前
var ppFree = newCache(func() interface{} { return new(pp) })
var ssFree = newCache(func() interface{} { return new(ss) })

// 変更後
var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}
var ssFree = sync.Pool{
	New: func() interface{} { return new(ss) },
}

オブジェクトの取得と返却のメソッドも、カスタムcacheget()/put()から、sync.PoolGet()/Put()に置き換えられました。

// pp構造体の取得
// 変更前: p := ppFree.get().(*pp)
// 変更後: p := ppFree.Get().(*pp)

// pp構造体の返却
// 変更前: ppFree.put(p)
// 変更後: ppFree.Put(p)

// ss構造体の取得
// 変更前: s := ssFree.get().(*ss)
// 変更後: s := ssFree.Get().(*ss)

// ss構造体の返却
// 変更前: ssFree.put(s)
// 変更後: ssFree.Put(s)

また、sync.Poolを使用するために、src/pkg/fmt/scan.go"sync"パッケージのインポートが追加されました。

この変更により、fmtパッケージはGoランタイムが提供する最適化されたオブジェクトプールメカニズムを利用できるようになり、ガベージコレクションの効率が向上し、全体的なパフォーマンスが改善されることが期待されます。特に、sync.Poolはガベージコレクションの際にプール内のオブジェクトを自動的にクリアする機能を持っており、これによりメモリ使用量が無制限に増大するのを防ぎつつ、必要な時にオブジェクトを再利用できるという利点があります。

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

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

  1. src/pkg/fmt/print.go: pp(printer)構造体のプール管理に関する変更。
  2. src/pkg/fmt/scan.go: ss(scanner)構造体のプール管理に関する変更。

src/pkg/fmt/print.go の変更

  • 削除されたコード:
    • cache構造体の定義(type cache struct { ... }
    • cache構造体のメソッド(put, get
    • newCache関数の定義
  • 追加されたコード:
    • var ppFree = sync.Pool{ New: func() interface{} { return new(pp) }, }
      • pp構造体をプールするためのsync.Poolインスタンスの宣言と初期化。Newフィールドには、新しいppオブジェクトを生成する匿名関数が設定されています。
  • 変更されたコード:
    • newPrinter()関数内で、ppFree.get().(*pp)ppFree.Get().(*pp)に変更。
    • (*pp).free()メソッド内で、ppFree.put(p)ppFree.Put(p)に変更。

src/pkg/fmt/scan.go の変更

  • 追加されたインポート:
    • "sync"パッケージのインポートが追加されました。
  • 削除されたコード:
    • var ssFree = newCache(func() interface{} { return new(ss) })
      • ss構造体をプールするためのカスタムキャッシュの宣言が削除されました。
  • 追加されたコード:
    • var ssFree = sync.Pool{ New: func() interface{} { return new(ss) }, }
      • ss構造体をプールするためのsync.Poolインスタンスの宣言と初期化。Newフィールドには、新しいssオブジェクトを生成する匿名関数が設定されています。
  • 変更されたコード:
    • newScanState()関数内で、ssFree.get().(*ss)ssFree.Get().(*ss)に変更。
    • (*ss).free()メソッド内で、ssFree.put(s)ssFree.Put(s)に変更。

コアとなるコードの解説

このコミットの核心は、fmtパッケージが内部的に使用するpp(printer)とss(scanner)という構造体のインスタンス管理方法を、カスタム実装からGo標準ライブラリのsync.Poolへと切り替えた点にあります。

pp構造体とppFree

pp構造体は、fmt.Printfなどのフォーマット関数が内部で文字列整形を行う際に使用するプリンタの状態を保持します。これには、出力バッファ(buf)、フォーマットフラグ(fmt)、処理対象の引数(arg, value)などが含まれます。

変更前は、ppFreeというcache型のグローバル変数を通じてppインスタンスをプールしていました。newPrinter()関数でppFree.get()を呼び出してプールからppインスタンスを取得し、(*pp).free()メソッドでppFree.put(p)を呼び出して使用済みのppインスタンスをプールに戻していました。

変更後は、ppFreesync.Pool型となり、sync.PoolNewフィールドにfunc() interface{} { return new(pp) }という関数が設定されました。これは、プールが空で新しいppインスタンスが必要な場合に、new(pp)を呼び出して新しいppインスタンスを生成することを意味します。

  • newPrinter()関数:

    func newPrinter() *pp {
    	p := ppFree.Get().(*pp) // sync.Poolからppインスタンスを取得
    	p.panicking = false
    	p.erroring = false
    	p.fmt.init(&p.buf)
    	return p
    }
    

    ppFree.Get()interface{}型を返すため、(*pp)で型アサーションを行って*pp型に変換しています。取得したppインスタンスは、再利用される可能性があるため、panicking, erroring, fmtなどのフィールドを適切に初期化しています。

  • (*pp).free()メソッド:

    func (p *pp) free() {
    	// ... (各種フィールドのリセット)
    	p.buf = p.buf[:0] // バッファをクリア
    	p.arg = nil
    	p.value = reflect.Value{}
    	ppFree.Put(p) // 使用済みppインスタンスをsync.Poolに戻す
    }
    

    ppFree.Put(p)を呼び出すことで、使用済みのppインスタンスがsync.Poolに戻され、将来のGet()呼び出しで再利用される可能性が生まれます。プールに戻す前に、bufスライスをゼロ長にリセットするなど、インスタンスの状態をクリーンアップしています。

ss構造体とssFree

ss構造体は、fmt.Scanfなどのスキャン関数が内部で入力解析を行う際に使用するスキャナの状態を保持します。これには、入力バッファ(buf)、ルーンリーダー(rr)などが含まれます。

pp構造体と同様に、変更前はssFreeというcache型のグローバル変数を通じてssインスタンスをプールしていました。newScanState()関数でssFree.get()を呼び出してプールからssインスタンスを取得し、(*ss).free()メソッドでssFree.put(s)を呼び出して使用済みのssインスタンスをプールに戻していました。

変更後は、ssFreesync.Pool型となり、sync.PoolNewフィールドにfunc() interface{} { return new(ss) }という関数が設定されました。

  • newScanState()関数:

    func newScanState(r io.Reader, nlIsSpace, nlIsEnd bool) (s *ss, old ssave) {
    	// ... (既存のロジック)
    	s = ssFree.Get().(*ss) // sync.Poolからssインスタンスを取得
    	// ... (取得したssインスタンスの初期化)
    	return
    }
    

    ssFree.Get()ssインスタンスを取得し、型アサーションを行っています。

  • (*ss).free()メソッド:

    func (s *ss) free(old ssave) {
    	// ... (各種フィールドのリセット)
    	s.buf = s.buf[:0] // バッファをクリア
    	s.rr = nil
    	ssFree.Put(s) // 使用済みssインスタンスをsync.Poolに戻す
    }
    

    ssFree.Put(s)を呼び出すことで、使用済みのssインスタンスがsync.Poolに戻されます。

まとめ

この変更により、fmtパッケージは、オブジェクトの生成とガベージコレクションのオーバーヘッドを削減するために、Goランタイムが提供する最適化されたsync.Poolメカニズムを利用するようになりました。これにより、特に高頻度でfmt関数が呼び出されるシナリオにおいて、パフォーマンスの向上が期待されます。また、カスタムキャッシュ実装を削除することで、コードの複雑性が軽減され、保守性も向上しています。

関連リンク

参考にした情報源リンク

  • 上記の関連リンクに記載されているGo公式ドキュメントおよびGitHubのIssue/CLページ。
  • Go言語におけるsync.Poolの利用に関する一般的な解説記事(Web検索を通じて得られた情報)。
    • 例: "Go sync.Pool 使い方" や "Go sync.Pool performance" などのキーワードで検索し、sync.Poolの設計思想やパフォーマンス上の利点について理解を深めました。
  • オブジェクトプールデザインパターンに関する一般的な情報。
    • 例: "オブジェクトプール パターン" などのキーワードで検索し、オブジェクトプールの概念とメリット・デメリットについて確認しました。
  • Go言語のガベージコレクションに関する一般的な情報。
    • 例: "Go ガベージコレクション" などのキーワードで検索し、GoのGCの仕組みと、オブジェクト割り当てがGCに与える影響について確認しました。