go には [container/list] package というlinked-list を扱う パッケージがあるんだけど。どうにも具合がよくない。ということで自分でつくってみた
なにが具合が良くないかというと
- list を管理するstruct をelement が別であり、無駄。
- 数が多くなると heavy にheap を使いgc 案件である。
- 要素がinterface{} なので型変換のコストがでかいでござる。
LRU みたいなのに使うとパフォーマンス的な問題がおきたりしていた。 詳細とかは improve container/list に詳しかったりします。
根本的に interface{} は使わざるをえない。
という感じ。generics はまだ当然ないので、以前に template をつかってcode 生成することで linux kernel のlist_head のようなやり口のlinked-list をつくった。
ですが作成時にvscode の補助がきかないとかだいぶつらかったです。 最近は genny というすばらしいものがあって、偽装的な type をつくって それでコードをかくのでvscode で補助もきいてすばらしいということでつくりました。
いちよインターフェースはほぼ [container/list] と同じものをもつので 使うのにもこまりません。またconcurrent access にも対応してるので 複数のgoroutine からアクセスしても壊れません。
使い方
最初に linked-list にいれる 要素を定義
package example
import (
"github.com/kazu/loncha/list_head"
)
type SampleParent struct {
Name string
}
type Sample struct {
ID int
Name string
Parent *SampleParent
list_head.ListHead
}
linked-list は listHead をstruct に内包することで実現している。
生成してみる。
$ wget -q -O - "https://github.com/kazu/loncha/master/container_list/container_list.go" | genny gen "ListEntry=Sample" > sample_list.go
これでできた。
...
func (d *Sample) Init() {
d.ListHead.Init()
}
// Next ... returns the next list element or nil.
func (d *Sample) Next() *Sample {
if d.ListHead.Next() == nil {
panic(errors.New("d.next is nil"))
}
return (*Sample)(unsafe.Pointer(uintptr(unsafe.Pointer(d.ListHead.Next())) - unsafe.Offsetof(d.ListHead)))
}
// Prev ... returns the previous list element or nil.
func (d *Sample) Prev() *Sample {
if d.ListHead.Next() == nil {
panic(errors.New("d.prev is nil"))
}
return (*Sample)(unsafe.Pointer(uintptr(unsafe.Pointer(d.ListHead.Prev())) - unsafe.Offsetof(d.ListHead)))
}
// NewSampleList ... New returns an initialized list.
func NewSampleList(h *Sample) *Sample {
h.Init()
return h
}
...
できてる。
ついでにbenchmark もとってみる。
const (
CREATE_NODE_MAX int = 10000
)
func MakeSample() (samples []*Sample, oldSamples []*OldSample) {
samples = make([]*Sample, CREATE_NODE_MAX)
oldSamples = make([]*OldSample, CREATE_NODE_MAX)
for i ,_ := range samples {
id := int(math.Abs(float64(rand.Intn(CREATE_NODE_MAX))))
samples[i] = &Sample{
ID: id,
Name: fmt.Sprintf("aaa%d", i),
}
oldSamples[i] = &OldSample{ID: samples[i].ID, Name: samples[i].Name}
}
return
}
func BenchmarkInsert(b *testing.B) {
orig, origOld := MakeSample()
b.ResetTimer()
b.Run("original container/list", func(b *testing.B) {
for j := 1; j < b.N; j++ {
b.StopTimer()
objs := make([]*OldSample, len(origOld))
copy(objs, origOld)
l := list.New()
for i := 0; i < len(objs); i++ {
b.StartTimer()
l.PushBack(objs[i])
b.StopTimer()
}
}
})
b.ResetTimer()
b.Run("loncha.container/list", func(b *testing.B) {
objs := make([]*Sample, len(orig))
copy(objs, orig)
objs[0].Init()
b.StopTimer()
for j := 1; j < b.N; j++ {
b.StopTimer()
objs := make([]*Sample, len(orig))
copy(objs, orig)
objs[0].Init()
for i := 1; i < len(objs); i++ {
b.StartTimer()
objs[i].Init()
objs[i-1].PushBack(objs[i])
b.StopTimer()
}
}
})
}
とりあえず 10000件のリストをPushBack するのを10回計測してみた。 いっぱい回すのは時間かかるのでとりあえず。
% go test -bench . --benchmem --benchtime 10x --run=BenchmarkInsert
BenchmarkInsert/original_container/list-16 10 4145263 ns/op 432030 B/op 9000 allocs/op
BenchmarkInsert/loncha.container/list-16 10 1148802 ns/op 8192 B/op 0 allocs/op
PASS
ok github.com/kazu/loncha/example/genny 5.552s
我が軍は圧倒的じゃないか!
- 7/7 追記 bencnmark コードが間違えてて速すぎたので酒精しましたw さすがに速すぎるとおもったw