go のslice は非常によくできて楽しい。連続領域を配列管理するという形で よくできている。
しかし、ruby のblock やほかの言語の iteration /iterator に比べて不便であるのも事実だ。
多少解決しようとして loncha というのもつくったりした。すでにこういったものはあるので sort.Slice でやったやり方で sortが高速化したのと同じだ方法で多少最適化した。
sort.Slice が現れた理由は
- 要素のメソッドがある前提であるが、比較回数が多いとかならずinterface を介さなければはらず、ソートのような要素が多く比較回数が多いとばかにならない。
- slice 内の置換のコスト
これを解決するために func(i,j int)
系の比較関数を渡して
その中で外部からの参照で slice[i]
みたいなアクセスをするので
sort 時の比較関数呼び出しでinterface{} が経由しない、という解決と
reflect.Swapper でslice での汎用的な入れ替えのための置き換え情報を渡して裏で置換してもらうのを一撃でやることで reflect のコストも気にならないという素晴らしいhack による高速化だった。
これを真似て loncha でも基本アプローチでくりかえしていたが、 そもそも 実行時にtype parameter だとたぶん使われる型はわかるので, constraints 自体はinterface{} であるが展開された実行時はinterface{} としての動作ではない。
loncha.Filter という条件にあった要素だけ削るという 関数があるのだが
現在はこんな感じでつかう。
ints := []int{11,2,3,4,5,56}
loncha.Filter(&ints, func(i index) bool {
return ints[i] == 5
})
新たに 条件関数の引数を添え字ではなくて要素のポインタで受けれるようにしてみた。 functional option が複雑でアレなのは実験用で多様なやつができるようにしてるためなので気にしなくていいw
ints := []int{11,2,3,4,5,56}
ints err = Filter2(&ints,
Cond2[FilterOpt[Element]](func(obj *int) bool {
return *obj == 5
}))
loncha にあるベンチマークに追加したら
func BenchmarkFilter(b *testing.B) {
...
b.ResetTimer()
b.Run("loncha.Filter ", func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
objs := make([]Element, len(orig))
copy(objs, orig)
b.StartTimer()
Filter(&objs, func(i int) bool {
return objs[i].ID == 555
})
}
})
b.ResetTimer()
b.Run("loncha.Filter2 ", func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
objs := make([]Element, len(orig))
copy(objs, orig)
b.StartTimer()
objs, _ = Filter2(objs,
Cond2[FilterOpt[Element]](func(obj *Element) bool {
return obj.ID == 555
}))
}
})
これを -benchtime 1000x
でとってみると、ほぼ変わらずに、かつ条件関数の副作用がなくなったからなのか少しはやくなっている。
まぁこれで func(index int)
はおさらばできそうかな。alloc が増えてるのはどこだろうかちょっと調べてみるがとりあえずここまで
BenchmarkFilter/loncha.Filter-16 1000 59957 ns/op 82020 B/op 4 allocs/op
BenchmarkFilter/loncha.Filter2-16 1000 58535 ns/op 82320 B/op 13 allocs/op