[インデックス 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
)を使って、pp
やss
構造体のプールを自前で実装していました。しかし、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
の仕組み:
- オブジェクトの取得 (
Get()
):Pool
内に利用可能なオブジェクトがあれば、それを返します。- 利用可能なオブジェクトがなければ、
Pool
のNew
フィールドに設定された関数を呼び出し、新しいオブジェクトを生成して返します。
- オブジェクトの返却 (
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
を使用するように変更しました。
具体的には、ppFree
とssFree
という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) },
}
オブジェクトの取得と返却のメソッドも、カスタムcache
のget()
/put()
から、sync.Pool
のGet()
/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つのファイルに集中しています。
src/pkg/fmt/print.go
:pp
(printer)構造体のプール管理に関する変更。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
インスタンスをプールに戻していました。
変更後は、ppFree
がsync.Pool
型となり、sync.Pool
のNew
フィールドに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
インスタンスをプールに戻していました。
変更後は、ssFree
がsync.Pool
型となり、sync.Pool
のNew
フィールドに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言語の
sync.Pool
に関する公式ドキュメント: https://pkg.go.dev/sync#Pool - Go言語の
fmt
パッケージに関する公式ドキュメント: https://pkg.go.dev/fmt - Go Issue #4720:
fmt: use sync.Pool
(このコミットが解決したIssue): https://github.com/golang/go/issues/4720 - このコミットのGo CL (Code Review) ページ: https://golang.org/cl/43990043
参考にした情報源リンク
- 上記の関連リンクに記載されているGo公式ドキュメントおよびGitHubのIssue/CLページ。
- Go言語における
sync.Pool
の利用に関する一般的な解説記事(Web検索を通じて得られた情報)。- 例: "Go sync.Pool 使い方" や "Go sync.Pool performance" などのキーワードで検索し、
sync.Pool
の設計思想やパフォーマンス上の利点について理解を深めました。
- 例: "Go sync.Pool 使い方" や "Go sync.Pool performance" などのキーワードで検索し、
- オブジェクトプールデザインパターンに関する一般的な情報。
- 例: "オブジェクトプール パターン" などのキーワードで検索し、オブジェクトプールの概念とメリット・デメリットについて確認しました。
- Go言語のガベージコレクションに関する一般的な情報。
- 例: "Go ガベージコレクション" などのキーワードで検索し、GoのGCの仕組みと、オブジェクト割り当てがGCに与える影響について確認しました。