Go言語でインタプリタを作っているのだが、動的値型の宣言の仕方でぜんぜん実行速度が変わってくるので、メンテナンス性と実行速度いろいろ検討してみる。
構造体に全部の型を詰め込むパターン
動的値型を作る上で最も直感的に使えるのが全部の型を構造体に詰め込むパターン。あまり考えなくても使えるがメモリ効率はよくない。
type Value struct {
TypeNo int
IntValue int
StrValue string
}
// 必要な時に変換関数を呼ぶ
func (v *Value) ToInt() int {
if TypeNo==TypeInt { return v.IntVaue }
if TypeNo==TypeStr { return StrToInt(v.StrValue) }
}
func (v *Value) ToString() string { ... }
全てをinterface{}で表現するパターン
動的値型をinterface{}で表す方法。必要に応じて、任意の型にキャストして使う。キャストが面倒なので使い勝手は良くない上に間違えると落ちる。しかし、Go言語の機能を使うため動作速度は比較的早い。メモリも必要最小限。
type Value interface{}
// switchで型を分けて使う
func ToInt(v Value) int {
switch v := v.(type) {
case int: return v.(int)
case string: return StrToInt(v.(string))
default: return 0
}
}
func ToString(v Value) string { ... }
- (参考) go-lua > types.go
- (参考) DCLua > value.go
Goの型を利用しつつinterfaceで変換メソッドを用意
Goの型を利用しつつもメソッドを追加して共通型として利用する。メモリ効率もよくinterfaceの良さを活かしつつ、それなりに安全に運用できる。Gopher-luaの作者の型はこの部分が動作ネックになっているとブログに書かれていたが、Go製のLuaの中ではGopher-luaは速い方。
type Value interface {
ValueType() int
ToInt() int
ToString() string
}
type MyInt int
func (p MyInt) ToInt() { return p }
func (p MyInt) ToString() { return IntToStr(p) }
type MyStr string
func (p MyStr) ToInt() { return StrToInt(p) }
func (p MyStr) ToString() { return p }
interface{}を利用しつつも構造体に閉じ込める
内部的には、interface{}を利用しつつも、構造体とそのメソッドと安全に動的値型を操作できる。ただし、動的値型を生成する際に、一枚皮を被せているので、インタプリタ実装時大きなオーバーヘッドが必要となる。この記事執筆時点なでしこGo版では、この方式を採用しているが、値型の生成がとにかく遅い。
struct Value {
ValueType int
value interface{}
}
func (v *Value) ToInt() int {
switch value.(type) {
case int: return v.value.(int)
case string: return StrToInt(v.value.(string))
}
}
- (参考) なでしこ3Go > value.go
なお、筆者が作っている日本語プログラミング言語「なでしこ」のGo言語実装版はこちら。
その他
その他、参考になりそうな点