[インデックス 18075] ファイルの概要
このコミットは、Go言語の標準ライブラリであるregexp
パッケージにおいて、正規表現のマッチングに使用されるmachine
オブジェクトの管理方法を改善し、sync.Pool
を利用するように変更するものです。これにより、オブジェクトの再利用を促進し、ガベージコレクション(GC)の負荷を軽減することで、パフォーマンスの向上を図っています。
コミット
commit b682f6de5a9d645a92792e2ffad1956c64b7840f
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Dec 18 16:43:19 2013 -0800
regexp: use sync.Pool
For machines, not threads.
Update #4720
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/44150043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b682f6de5a9d645a92792e2ffad1956c64b7840f
元コミット内容
このコミットの元の内容は、regexp
パッケージがsync.Pool
を使用してmachine
オブジェクトを管理するように変更することです。これは、スレッドではなく、machine
オブジェクトの再利用を目的としています。コミットメッセージには、GoのIssue #4720の更新であることも示されています。
変更の背景
Goのregexp
パッケージは、正規表現のマッチングを行う際に内部的にmachine
というオブジェクトを使用します。このmachine
オブジェクトは、正規表現のプログラムを実行するための状態を保持します。正規表現のマッチングが頻繁に行われるアプリケーションでは、これらのmachine
オブジェクトが繰り返し生成され、使用後に破棄されることになります。
オブジェクトの頻繁な生成と破棄は、Goのガベージコレクタ(GC)に大きな負荷をかける可能性があります。GCは、不要になったメモリを解放するプロセスですが、その実行には一定の時間がかかり、アプリケーションの実行を一時的に停止させる(GCポーズ)ことがあります。特に、高スループットが求められるシステムでは、GCポーズがパフォーマンスのボトルネックとなることがあります。
このコミットの背景には、machine
オブジェクトの再利用を促進し、GCの負荷を軽減することで、regexp
パッケージの全体的なパフォーマンスを向上させるという目的があります。sync.Pool
は、このような一時的なオブジェクトの再利用に特化したGoの標準ライブラリ機能であり、この問題に対する適切な解決策として採用されました。
コミットメッセージにある「Update #4720」について補足すると、Web検索の結果ではIssue #4720が直接regexp
パッケージのパフォーマンスに関するものではなく、sync.Pool
の一般的な利用に関する議論や改善提案であった可能性が示唆されています。しかし、このコミットがsync.Pool
を導入していることから、間接的にGC負荷軽減という文脈で関連付けられていると考えられます。
前提知識の解説
Go言語のregexp
パッケージとmachine
の概念
Go言語のregexp
パッケージは、正規表現の処理に特化したライブラリです。このパッケージは、RE2という正規表現エンジンに基づいています。RE2は、バックトラッキングを伴う正規表現エンジンとは異なり、線形時間(入力文字列の長さに比例する時間)でマッチングが完了することを保証します。これにより、正規表現によるサービス拒否攻撃(ReDoS)のリスクを回避し、予測可能なパフォーマンスを提供します。
regexp
パッケージが正規表現をコンパイルすると、内部的にはその正規表現を表現する「プログラム」(syntax.Prog
)が生成されます。このプログラムを実行して入力文字列とのマッチングを行うのが、machine
と呼ばれるオブジェクトです。machine
は、正規表現の実行状態を保持し、入力文字列を走査しながらマッチングを進めます。マッチングのたびに新しいmachine
オブジェクトが作成されると、前述のGC負荷の問題が発生します。
sync.Pool
sync.Pool
は、Go言語のsync
パッケージに含まれる型で、一時的なオブジェクトを効率的に再利用するためのメカニズムを提供します。その主な目的は、頻繁に生成・破棄されるオブジェクトのメモリ割り当てとガベージコレクションのプレッシャーを軽減することです。
sync.Pool
の基本的な動作は以下の通りです。
Put(x interface{})
: オブジェクトx
をプールに戻します。これにより、そのオブジェクトは後で再利用できるようになります。Get() interface{}
: プールからオブジェクトを取得します。プールが空の場合、sync.Pool
の初期化時に指定されたNew
関数が呼び出され、新しいオブジェクトが生成されます。New
関数が指定されていない場合、nil
が返されます。- ガベージコレクションとの関係:
sync.Pool
に格納されたオブジェクトは、ガベージコレクションの対象となります。つまり、GCが実行されると、プール内のオブジェクトが自動的に削除される可能性があります。これは、sync.Pool
が「一時的な」オブジェクトの再利用を目的としているためです。そのため、プールから取得したオブジェクトは、使用前に適切な初期化が必要となる場合があります。 - スレッドセーフ:
sync.Pool
は複数のGoroutineから安全にアクセスできるように設計されており、追加の同期メカニズムは不要です。
sync.Pool
は、バッファ、データベース接続、またはこのコミットのように正規表現のmachine
オブジェクトなど、短命で頻繁に作成されるオブジェクトの再利用に非常に有効です。
Go言語のガベージコレクション(GC)
Go言語は、自動メモリ管理のためにガベージコレクタ(GC)を使用します。開発者は手動でメモリを解放する必要がなく、GCが不要になったオブジェクトを自動的に識別してメモリを回収します。GoのGCは、並行(concurrent)かつ低遅延(low-latency)を目指して設計されていますが、それでも大量のオブジェクトが短期間に生成・破棄されると、GCの実行頻度が増加し、アプリケーションのパフォーマンスに影響を与える可能性があります。特に、GCが実行される際には、一時的にアプリケーションの実行が停止する「GCポーズ」が発生することがあり、これがレイテンシの増加につながることがあります。sync.Pool
のようなメカニズムは、このGCポーズの頻度と長さを減らすのに役立ちます。
技術的詳細
このコミットでは、regexp
パッケージが正規表現のマッチングに使用するmachine
オブジェクトの取得と解放のロジックを、手動で実装されていたミューテックス(sync.Mutex
)とスライス([]*machine
)によるキャッシュから、sync.Pool
を利用したメカニズムに置き換えています。
変更前は、Regexp
構造体内にmu sync.Mutex
とmachine []*machine
というフィールドがあり、get()
メソッドでmachine
スライスからオブジェクトを取り出し、put()
メソッドでスライスに戻す際に、mu
でロックをかけていました。これは、複数のGoroutineが同時にmachine
オブジェクトにアクセスする際の競合状態を防ぐための手動による同期とキャッシュの実装でした。
この手動実装にはいくつかの課題があります。
- 複雑性: ミューテックスのロックとアンロック、スライスの操作(追加、削除)を手動で行う必要があり、コードが複雑になりがちです。
- 効率性: スライスからの要素の取り出しや追加は、スライスの再割り当てや要素の移動を伴う可能性があり、オーバーヘッドが発生することがあります。また、ミューテックスの粒度が粗い場合、競合が発生しやすくなります。
- GCとの連携: スライスに保持されたオブジェクトはGCの対象外とはならず、明示的にスライスから削除されない限りメモリを占有し続けます。また、プールが大きくなりすぎた場合の管理も手動で行う必要があります。
sync.Pool
を導入することで、これらの課題が解決されます。
- 簡素化:
sync.Pool
が内部でオブジェクトの取得・解放ロジックと同期を管理するため、コードが大幅に簡素化されます。 - 効率的な再利用:
sync.Pool
は、内部的にGoroutineごとにローカルなキャッシュを持つなど、効率的なオブジェクトの再利用メカニズムを提供します。これにより、競合が減少し、パフォーマンスが向上します。 - GCフレンドリー:
sync.Pool
に格納されたオブジェクトはGCの対象となり得るため、プールが過度に肥大化するのを防ぎ、メモリ使用量をより適切に管理できます。
この変更により、regexp
パッケージは、正規表現のマッチングが頻繁に行われるシナリオにおいて、より効率的にメモリを使用し、GCの負荷を軽減することで、全体的なアプリケーションの応答性とスループットを向上させることが期待されます。
コアとなるコードの変更箇所
変更はsrc/pkg/regexp/regexp.go
ファイルに集中しています。
--- a/src/pkg/regexp/regexp.go
+++ b/src/pkg/regexp/regexp.go
@@ -85,9 +85,8 @@ type Regexp struct {
subexpNames []string
longest bool
- // cache of machines for running regexp
- mu sync.Mutex
- machine []*machine
+ // pool of machines for running regexp
+ machinePool sync.Pool // of *machine
}
// String returns the source text used to compile the regular expression.
@@ -175,14 +174,9 @@ func compile(expr string, mode syntax.Flags, longest bool) (*Regexp, error) {
// It uses the re's machine cache if possible, to avoid
// unnecessary allocation.
func (re *Regexp) get() *machine {
-\tre.mu.Lock()
-\tif n := len(re.machine); n > 0 {
-\t\tz := re.machine[n-1]
-\t\tre.machine = re.machine[:n-1]
-\t\tre.mu.Unlock()
-\t\treturn z
-\t}\n-\tre.mu.Unlock()
+\tif v := re.machinePool.Get(); v != nil {
+\t\treturn v.(*machine)
+\t}\
z := progMachine(re.prog)
z.re = re
return z
@@ -193,9 +187,7 @@ func (re *Regexp) get() *machine {
// grow to the maximum number of simultaneous matches
// run using re. (The cache empties when re gets garbage collected.)
func (re *Regexp) put(z *machine) {
-\tre.mu.Lock()
-\tre.machine = append(re.machine, z)
-\tre.mu.Unlock()
+\tre.machinePool.Put(z)\
}
// MustCompile is like Compile but panics if the expression cannot be parsed.
コアとなるコードの解説
Regexp
構造体の変更
- 削除:
mu sync.Mutex
とmachine []*machine
フィールドが削除されました。これらは、以前の手動キャッシュと同期のためのフィールドでした。 - 追加:
machinePool sync.Pool // of *machine
フィールドが追加されました。これは、machine
オブジェクトをプールするためのsync.Pool
インスタンスです。コメントにある// of *machine
は、このプールが*machine
型のオブジェクトを格納することを示唆しています。
get()
メソッドの変更
get()
メソッドは、正規表現のマッチングに使用するmachine
オブジェクトを取得する役割を担います。
- 変更前:
re.mu.Lock()
でミューテックスをロックし、排他制御を行います。re.machine
スライスにmachine
オブジェクトが存在する場合、スライスの末尾からオブジェクトを取り出し、スライスを更新します。re.mu.Unlock()
でミューテックスをアンロックします。- スライスが空の場合、新しい
machine
オブジェクトを生成します。
- 変更後:
re.machinePool.Get()
を呼び出して、プールからオブジェクトを取得します。Get()
メソッドはinterface{}
型を返すため、取得した値がnil
でないことを確認し、v.(*machine)
で*machine
型に型アサーションを行います。- プールが空で
Get()
がnil
を返した場合(またはsync.Pool
のNew
関数が設定されていない場合)、progMachine(re.prog)
を呼び出して新しいmachine
オブジェクトを生成します。
この変更により、手動でのロック管理とスライス操作が不要になり、sync.Pool
が提供する効率的なプールメカニズムが利用されます。
put()
メソッドの変更
put()
メソッドは、使用済みのmachine
オブジェクトをプールに戻す役割を担います。
- 変更前:
re.mu.Lock()
でミューテックスをロックします。re.machine
スライスにz
(使用済みmachine
オブジェクト)を追加します。re.mu.Unlock()
でミューテックスをアンロックします。
- 変更後:
re.machinePool.Put(z)
を呼び出して、z
をプールに戻します。
この変更により、手動でのロック管理とスライスへの追加操作が不要になり、sync.Pool
がオブジェクトの管理を効率的に行います。
全体として、このコミットはregexp
パッケージの内部実装をより現代的で効率的なsync.Pool
ベースのオブジェクト再利用パターンに移行させることで、パフォーマンスと保守性の向上に貢献しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/b682f6de5a9d645a92792e2ffad1956c64b7840f
- Go CL (Code Review): https://golang.org/cl/44150043
- Go Issue #4720: https://golang.org/issue/4720 (このコミットで更新されたIssueですが、直接
regexp
のパフォーマンスに関するものではない可能性があります)
参考にした情報源リンク
- Go
sync.Pool
Documentation: - Go
regexp
Package and Performance: - Go
regexp
Package Machine Concept: