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

[インデックス 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.Mutexmachine []*machineというフィールドがあり、get()メソッドでmachineスライスからオブジェクトを取り出し、put()メソッドでスライスに戻す際に、muでロックをかけていました。これは、複数のGoroutineが同時にmachineオブジェクトにアクセスする際の競合状態を防ぐための手動による同期とキャッシュの実装でした。

この手動実装にはいくつかの課題があります。

  1. 複雑性: ミューテックスのロックとアンロック、スライスの操作(追加、削除)を手動で行う必要があり、コードが複雑になりがちです。
  2. 効率性: スライスからの要素の取り出しや追加は、スライスの再割り当てや要素の移動を伴う可能性があり、オーバーヘッドが発生することがあります。また、ミューテックスの粒度が粗い場合、競合が発生しやすくなります。
  3. 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.Mutexmachine []*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.PoolNew関数が設定されていない場合)、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ベースのオブジェクト再利用パターンに移行させることで、パフォーマンスと保守性の向上に貢献しています。

関連リンク

参考にした情報源リンク