[インデックス 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 (コミット自体)