[インデックス 1096] ファイルの概要
このコミットは、Go言語のreflectパッケージにおいて、値の構築時に型文字列の不要なパース処理を避けるためのキャッシュ機構を追加するものです。これにより、リフレクションを用いた値の生成処理のパフォーマンスが向上します。
コミット
commit 842e1a9aa70648a013d5a48073683f08332e461d
Author: Rob Pike <r@golang.org>
Date: Mon Nov 10 14:53:40 2008 -0800
Add a cache to avoid unnecessary parsing of type strings when constructing values
R=rsc
DELTA=12 (9 added, 0 deleted, 3 changed)
OCL=18916
CL=18921
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/842e1a9aa70648a013d5a48073683f08332e461d
元コミット内容
Add a cache to avoid unnecessary parsing of type strings when constructing values
R=rsc
DELTA=12 (9 added, 0 deleted, 3 changed)
OCL=18916
CL=18921
変更の背景
Go言語のリフレクション機能は、実行時に型情報を検査したり、値の操作を行ったりするための強力なメカニズムを提供します。しかし、この機能は通常、コンパイル時に型が確定している通常のコードパスと比較して、オーバーヘッドが大きくなる傾向があります。
特に、reflectパッケージ内で型情報を扱う際、型を表す文字列(typestring)をパースして実際のTypeオブジェクトに変換する処理は、計算コストが高い操作です。同じ型に対して何度もNewValueのような関数が呼び出され、そのたびに型文字列のパースが行われると、パフォーマンスのボトルネックとなる可能性がありました。
このコミットは、このような重複する型文字列のパース処理を避けるために、パース済みのTypeオブジェクトをキャッシュするメカニズムを導入することで、リフレクションのパフォーマンスを改善することを目的としています。これにより、一度パースされた型はキャッシュから再利用され、不要な計算が削減されます。
前提知識の解説
Go言語のリフレクション
Go言語のreflectパッケージは、プログラムが自身の構造を検査し、実行時に値を操作するための機能を提供します。主な概念は以下の通りです。
Type: Goの型を表すインターフェースです。例えば、int、string、struct { Name string; Age int }などがTypeとして表現されます。reflect.TypeOf(i)のようにして、任意のインターフェース値の動的な型情報を取得できます。Value: Goの値を表す構造体です。任意のGoの変数や定数をreflect.ValueOf(i)のようにしてValueとしてラップできます。Valueは、その値の型情報(Type)と、実際のデータを含みます。Valueを通じて、フィールドへのアクセス、メソッドの呼び出し、値の変更(ポインタ経由の場合)などが行えます。
リフレクションは、ジェネリックなデータ処理、シリアライゼーション/デシリアライゼーション、ORM(Object-Relational Mapping)などのライブラリやフレームワークで広く利用されます。
型文字列のパース
Goの内部では、型の情報は様々な形式で表現されます。リフレクションの初期の実装では、型を識別するために文字列形式の型情報(typestring)が使用されることがありました。このtypestringからreflect.Typeオブジェクトを生成する際には、文字列を解析してGoの型システムにマッピングする「パース」処理が必要になります。このパース処理は、文字列の長さや型の複雑さによっては、それなりのCPU時間を消費する可能性があります。
キャッシュの概念
キャッシュとは、計算コストの高い処理の結果を一時的に保存しておき、同じ入力が再度与えられた際に、再計算せずに保存しておいた結果を再利用する仕組みです。これにより、処理の高速化やリソースの節約が期待できます。今回のケースでは、型文字列のパース結果であるTypeオブジェクトをキャッシュすることで、同じ型文字列が再度現れた際にパース処理をスキップし、パフォーマンスを向上させます。
技術的詳細
このコミットでは、reflectパッケージのvalue.goファイルにtypecacheというグローバルなマップ変数を導入しています。
-
var typecache *map[string] *Type:typecacheは、string(型文字列)をキーとし、*Type(Typeオブジェクトへのポインタ)を値とするマップへのポインタとして宣言されています。- このマップは、
init関数内でnew(map[string] *Type)として初期化されます。
-
NewValue関数の変更:NewValue関数は、Emptyインターフェース値からValueを構築する際に呼び出されます。- 以前は、
sys.reflect(e)から得られたtypestringを直接ParseTypeString("", typestring)に渡してTypeオブジェクトを生成していました。 - 変更後、まず
typecacheにtypestringがキーとして存在するかどうかを確認します。p, ok := typecache[typestring]
- もし存在すれば(
okがtrue)、キャッシュされた*Typeポインタpを直接使用します。*pをNewValueAddrに渡します。
- もし存在しなければ(
okがfalse)、従来通りParseTypeString("", typestring)を呼び出してTypeオブジェクトをパースします。- パースした
Typeオブジェクトを新しい*Typeポインタpに格納し、そのポインタをtypecacheにtypestringをキーとして保存します。 *pをNewValueAddrに渡します。
- パースした
この変更により、同じ型文字列が複数回NewValueに渡された場合でも、ParseTypeStringの呼び出しは初回のみとなり、2回目以降はキャッシュから高速にTypeオブジェクトを取得できるようになります。
コアとなるコードの変更箇所
--- a/src/lib/reflect/value.go
+++ b/src/lib/reflect/value.go
@@ -695,6 +695,7 @@ func FuncCreator(typ Type, addr Addr) Value {
}
var creator *map[int] Creator
+var typecache *map[string] *Type
func init() {
creator = new(map[int] Creator);
@@ -722,6 +723,8 @@ func init() {
creator[StructKind] = &StructCreator;
creator[InterfaceKind] = &InterfaceCreator;
creator[FuncKind] = &FuncCreator;
+\
+\ttypecache = new(map[string] *Type);
}
func NewValueAddr(typ Type, addr Addr) Value {
@@ -752,10 +755,16 @@ export func NewInitValue(typ Type) Value {
export func NewValue(e Empty) Value {
value, typestring := sys.reflect(e);
- typ := ParseTypeString("", typestring);
+\tp, ok := typecache[typestring];
+\tif !ok {
+\t\ttyp := ParseTypeString("", typestring);
+\t\tp = new(Type);\n+\t\t*p = typ;
+\t\ttypecache[typestring] = p;
+\t}
// Content of interface is a value; need a permanent copy to take its address
// so we can modify the contents. Values contain pointers to 'values'.
ap := new(uint64);
*ap = value;
- return NewValueAddr(typ, PtrUint64ToAddr(ap));
+\treturn NewValueAddr(*p, PtrUint64ToAddr(ap));
}
コアとなるコードの解説
-
var typecache *map[string] *Typeの追加:reflectパッケージのグローバル変数としてtypecacheが宣言されています。これは、型文字列(string)をキーとし、対応するTypeオブジェクトへのポインタ(*Type)を値とするマップを指すポインタです。
-
init関数でのtypecacheの初期化:- Goの
init関数は、パッケージがインポートされた際に自動的に実行される特別な関数です。 typecache = new(map[string] *Type);の行が追加され、typecacheマップが初期化されます。これにより、プログラムの実行開始時にキャッシュが利用可能な状態になります。
- Goの
-
NewValue関数の変更:value, typestring := sys.reflect(e);の行で、インターフェース値eから実際の値と型文字列typestringを取得します。- キャッシュの参照:
p, ok := typecache[typestring];typecacheマップからtypestringに対応する*Typeポインタを取得しようとします。okは、キーが存在したかどうかを示すブール値です。
- キャッシュミスの場合:
if !ok { ... }- もし
typestringがキャッシュに存在しない場合(!ok)、以下の処理が行われます。typ := ParseTypeString("", typestring);:型文字列をパースして、新しいTypeオブジェクトtypを生成します。p = new(Type);:新しいTypeポインタpを割り当てます。*p = typ;:パースしたTypeオブジェクトtypを、新しく割り当てたポインタpが指すメモリ位置にコピーします。typecache[typestring] = p;:typestringをキーとして、新しく生成した*Typeポインタpをtypecacheに保存します。これにより、次回同じ型文字列が来た際にはキャッシュから取得できるようになります。
- もし
- キャッシュヒットの場合、またはキャッシュミス後の処理:
return NewValueAddr(*p, PtrUint64ToAddr(ap));:最終的に、キャッシュから取得した(または新しくパースしてキャッシュに保存した)*Typeポインタpが指すTypeオブジェクト(*p)を使用して、新しいValueが構築されます。
この一連の変更により、NewValue関数が呼び出されるたびに型文字列のパースが繰り返されることを防ぎ、リフレクションのパフォーマンスが向上します。
関連リンク
- Go言語の
reflectパッケージ公式ドキュメント: https://pkg.go.dev/reflect - A Little Tour of Go: Reflection (Go言語の公式ブログ記事、リフレクションの概要): https://go.dev/blog/laws-of-reflection
参考にした情報源リンク
- Go言語の
reflectパッケージの内部実装に関する一般的な知識 - Go言語の
init関数の動作に関する知識 - Go言語のマップ(
map)の基本的な使用方法に関する知識 - Go言語のポインタに関する知識
- Go言語のパフォーマンス最適化に関する一般的な原則
- https://github.com/golang/go/commit/842e1a9aa70648a013d5a48073683f08332e461d (コミット自体)