Golang の generics パフォーマンス
LuneScript は、 Golang (1.16 以降)へのトランスコンパイルを対応しています。 また、LuneScript は Generics に対応しています。
一方で、 Golang は version 1.18 から Generics に対応しています。
つまり、 LuneScript は Golang が Generics 対応する前から Generics を利用できていました。
では、 Generics を利用していた LuneScript のコードを どうやって Generics 対応前の Golang にトランスコンパイルしていたかというと、 Generics の型パラメータの値を interface{} に変換して処理を行なっていました。
Java でいうところの autoboxing のようなことを変換時にやっていた、 と思ってもらえば良いです。
で、 Golang ネイティブで Generics 対応されて autoboxing する必要がなくなったので、 LuneScript の Golang へのトランスコンパイルで Golang の Generics を利用するように変更する検討作業に入りました。
検討に利用する golang のバージョン
今回は以下の go のバージョンを利用して検討します。
$ go version go version go1.19.2 linux/amd64
Generics のパフォーマンス確認
既存の処理を変更するので、 それなりのメリットがないと意味がないです。
そのメリットとは、 autoboxing 相当の処理をやめて Golang ネイティブの Generics を利用することで、 多少なりとも処理が速くなるんじゃないか? ということです。
そのために、 次の処理を既存 autoboxing 処理と、 ネイティブの Generics 処理とで実行したパフォーマンスを比較します。
- 「LuneScript の
List<int>
の要素の合計を計算する。」
テストコード
具体的なコードは以下です。
このコードの GenList[V any]
が generics を利用した List<int> の構造で、
BoxingList
が autoboxing を利用している従来の List<int> の構造です。
それぞれの構造に 1000 個の int 要素を事前に追加しておき、 リストから値を取りだしてトータルを計算する処理を 1000000 回繰替えして、 その時間を計測します。
package main
import . "github.com/ifritJP/LuneScript/src/lune/base/runtime_go"
var init_miniGo bool
var miniGo__mod__ string
// generics
type GenList[V any] struct {
items []V
}
func (list *GenList[V]) GetAt( index int ) V {
return list.items[index]
}
var list GenList[int]
func miniGo_generics(_env *LnsEnv) LnsInt {
total := 0
for _forWork0 := 1; _forWork0 <= 1000000; _forWork0++ {
for _forWork1 := 1; _forWork1 <= 1000; _forWork1++ {
loop := _forWork1
total = total + list.GetAt(loop-1)
// total = total + list.items[loop-1]
}
}
return total
}
// autoboxing
type BoxingList struct {
items []any
}
func (list *BoxingList) GetAt( index int ) any {
return list.items[index]
}
var boxing *BoxingList
func miniGo_autoboxing(_env *LnsEnv) LnsInt {
total := 0
for _forWork0 := 1; _forWork0 <= 1000000; _forWork0++ {
for _forWork1 := 1; _forWork1 <= 1000; _forWork1++ {
loop := _forWork1
total = total + boxing.GetAt(loop-1).(int)
// total = total + boxing.items[loop-1].(int)
}
}
return total
}
func Lns_miniGo_init(_env *LnsEnv) {
if init_miniGo { return }
init_miniGo = true
miniGo__mod__ = "@miniGo"
Lns_InitMod()
list = GenList[int]{[]int{}}
boxing = &BoxingList{[]any{}}
{
var _forFrom0 LnsInt = 1
var _forTo0 LnsInt = 1000
for _forWork0 := _forFrom0; _forWork0 <= _forTo0; _forWork0++ {
count := _forWork0
list.items = append(list.items,count)
boxing.items = append(boxing.items,count)
}
}
miniGo_prev2 := _env.GetVM().OS_clock()
Lns_print([]LnsAny{
"generics", miniGo_generics(_env),
"time = ", _env.GetVM().OS_clock() - miniGo_prev2})
miniGo_prev3 := _env.GetVM().OS_clock()
Lns_print([]LnsAny{
"autoboxing", miniGo_autoboxing(_env),
"time = ", _env.GetVM().OS_clock() - miniGo_prev3})
}
func MiniGo___main( _env *LnsEnv, args *LnsList ) LnsInt {
Lns_miniGo_init( _env )
return 0
}
func init() {
init_miniGo = false
}
実行結果
実行結果が以下です。
generics 500500000000 time = 2.1711650000000002
autoboxing 500500000000 time = 0.9791500000000002
これを見ると分かりますが、 なんと ネイティブの Generics を利用した方が倍も遅くなってしまいました。
これは意外でした。
効果がないどころか、逆に遅くなってしまいました。
なお、 このサンプルプログラムでは List の要素にアクセスする際、
定義したメソッド GetAt()
を介します。
このメソッドを通さずに直節メンバにアクセスするように変更したところ (コメントアウトしている箇所のコメントを外し、 その直前処理を代わりにコメントアウトする)、 次のように generics を利用した方が速く処理が終りました。
generics 500500000000 time = 0.6483559999999999
autoboxing 500500000000 time = 1.000772
generics を利用したメソッドは、オーバーヘッドが異様に大きいという結果になりました。
ところで、 generics のメソッド対応方法って、これであってるよね??
type GenList[V any] struct {
items []V
}
func (list *GenList[V]) GetAt( index int ) V {
return list.items[index]
}
まとめ
以上の結果をまとめると、次になります。
- generics を利用したメンバアクセスは、any との相互変換がなくなる分、速くなる。
- 但し generics を利用したメソッドのオーバーヘッドが大きい。
このことから、 LuneScript の autoboxing 処理をそのまま golang の generics へ 置き換えることはしません。
ですが、generics を利用した方が速くなるケースがあるのも事実なので、 今後も generics の検討を進めて、 効果的な適応方法が見つかったら対応を進めたいと思います。