Skip to content

Commit 06715a8

Browse files
authored
Pre-fetch column pointers to speed up queries by 20-30% (#426)
1 parent 324539f commit 06715a8

14 files changed

+644
-238
lines changed

ecs/checks_debug.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
package ecs
44

5+
const isDebug = true
6+
57
func (c *cursor) checkQueryNext() {
68
if c.table < -1 {
79
panic("query iteration already finished. Create a new query to iterate again")

ecs/checks_nodebug.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
package ecs
44

5+
const isDebug = false
6+
57
func (s *storage) checkHasComponent(_ Entity, _ ID) {}

ecs/filter_gen.go

Lines changed: 48 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ecs/internal/generate/filter.go.template

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package ecs
33

44
// Code generated by go generate; DO NOT EDIT.
55

6-
import "sync"
6+
import (
7+
"sync"
8+
"unsafe"
9+
)
710

811
{{range makeRange 0 8}}
912
{{- $upper := upperLetters . -}}
@@ -178,6 +181,9 @@ func (f *Filter{{.}}{{$genericsShort}}) Query(rel ...Relation) Query{{.}}{{$gene
178181
maxIndex: -1,
179182
},
180183
rareComp: f.rareComp,
184+
{{- range $i, $v := $upper}}
185+
columnPtr{{$v}}: unsafe.Pointer(nilDummy),
186+
{{- end}}
181187
{{if eq . 0 -}}
182188
hasRareComp: len(f.ids) > 0,
183189
{{- end}}

ecs/internal/generate/query.go.template

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package ecs
33

44
// Code generated by go generate; DO NOT EDIT.
55

6+
import "unsafe"
7+
68
type cursor struct {
79
archetype int32
810
table int32
@@ -38,7 +40,8 @@ type Query{{.}}{{$generics}} struct {
3840
table *table
3941
cache *cacheEntry
4042
{{- range $upper}}
41-
column{{.}} *column
43+
columnPtr{{.}} unsafe.Pointer
44+
itemSize{{.}} uintptr
4245
{{- end}}
4346
relations []relationID
4447
tables []tableID
@@ -120,7 +123,8 @@ func (q *Query{{.}}{{$genericsShort}}) Close() {
120123
q.table = nil
121124
q.cache = nil
122125
{{- range $i, $v := $upper}}
123-
q.column{{$v}} = nil
126+
q.columnPtr{{$v}} = unsafe.Pointer(nilDummy)
127+
q.itemSize{{$v}} = 0
124128
{{- end}}
125129
q.world.unlockSafe(q.lock)
126130
}
@@ -196,7 +200,9 @@ func (q *Query{{.}}{{$genericsShort}}) setTable(index int32, table *table) {
196200
q.cursor.table = index
197201
q.table = table
198202
{{- range $i, $v := $upper}}
199-
q.column{{$v}} = q.components[{{$i}}].columns[q.table.id]
203+
column{{$v}} := q.components[{{$i}}].columns[q.table.id]
204+
q.columnPtr{{$v}} = column{{$v}}.pointer
205+
q.itemSize{{$v}} = column{{$v}}.itemSize
200206
{{- end}}
201207
q.cursor.index = 0
202208
q.cursor.maxIndex = int64(q.table.len - 1)

ecs/internal/generate/query_debug.go.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package ecs
55

66
// Code generated by go generate; DO NOT EDIT.
77

8+
import "unsafe"
9+
810
{{range makeRange 0 8}}
911
{{- $n := . -}}
1012
{{- $upper := upperLetters . -}}
@@ -37,8 +39,9 @@ func (q *Query{{.}}{{$genericsShort}}) Entity() Entity {
3739
// ⚠️ Do not store the obtained pointers outside of the current context (i.e. the query loop)!
3840
func (q *Query{{.}}{{$genericsShort}}) Get() {{$return}} {
3941
q.cursor.checkQueryGet()
42+
index := q.cursor.index
4043
return {{range $i, $v := $upper}}{{if $i}},
41-
{{end}}(*{{$v}})(q.column{{$v}}.Get(q.cursor.index)){{end}}
44+
{{end}}(*{{$v}})(unsafe.Add(q.columnPtr{{$v}}, index*q.itemSize{{$v}})){{end}}
4245
}
4346
{{- end}}
4447

ecs/internal/generate/query_nodebug.go.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package ecs
55

66
// Code generated by go generate; DO NOT EDIT.
77

8+
import "unsafe"
9+
810
{{range makeRange 0 8}}
911
{{- $n := . -}}
1012
{{- $upper := upperLetters . -}}
@@ -34,8 +36,9 @@ func (q *Query{{.}}{{$genericsShort}}) Entity() Entity {
3436
//
3537
// ⚠️ Do not store the obtained pointers outside of the current context (i.e. the query loop)!
3638
func (q *Query{{.}}{{$genericsShort}}) Get() {{$return}} {
39+
index := q.cursor.index
3740
return {{range $i, $v := $upper}}{{if $i}},
38-
{{end}}(*{{$v}})(q.column{{$v}}.Get(q.cursor.index)){{end}}
41+
{{end}}(*{{$v}})(unsafe.Add(q.columnPtr{{$v}}, index*q.itemSize{{$v}})){{end}}
3942
}
4043
{{- end}}
4144

ecs/internal/generate/query_test.go.template

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,32 @@ func TestQuery{{.}}Empty(t *testing.T) {
167167
query := filter.Query()
168168
expectEqual(t, 0, query.Count())
169169

170-
expectPanics(t, func() { query.Get() })
171170
expectPanics(t, func() { query.Entity() })
171+
if isDebug {
172+
expectPanics(t, func() { query.Get() })
173+
} else {
174+
{{$comps}} := query.Get()
175+
{{- range $i, $v := $lower}}
176+
expectNil(t, {{$v}})
177+
{{- end}}
178+
}
172179

173180
cnt := 0
174181
for query.Next() {
175182
cnt++
176183
}
177184
expectEqual(t, 0, cnt)
178185

179-
expectPanics(t, func() { query.Get() })
180186
expectPanics(t, func() { query.Entity() })
181187
expectPanics(t, func() { query.Next() })
188+
if isDebug {
189+
expectPanics(t, func() { query.Get() })
190+
} else {
191+
{{$comps}} := query.Get()
192+
{{- range $i, $v := $lower}}
193+
expectNil(t, {{$v}})
194+
{{- end}}
195+
}
182196
}
183197

184198
func TestQuery{{.}}Relations(t *testing.T) {

ecs/query_bench_test.go

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,61 +5,47 @@ import (
55
"testing"
66
)
77

8-
func BenchmarkPosVelQuery_1000(b *testing.B) {
8+
func BenchmarkPosVelQueryInline_1000(b *testing.B) {
99
n := 1000
1010
world := NewWorld(1024)
1111

1212
mapper := NewMap2[Position, Velocity](&world)
1313
mapper.NewBatch(n, &Position{}, &Velocity{X: 1, Y: 0})
1414

1515
filter := NewFilter2[Position, Velocity](&world)
16-
for b.Loop() {
16+
loop := func(filter *Filter2[Position, Velocity]) {
1717
query := filter.Query()
1818
for query.Next() {
1919
pos, vel := query.Get()
2020
pos.X += vel.X
2121
pos.Y += vel.Y
2222
}
2323
}
24+
25+
for b.Loop() {
26+
loop(filter)
27+
}
2428
}
2529

26-
func BenchmarkPosVelQueryCached_1000(b *testing.B) {
27-
n := 1000
30+
func BenchmarkPosVelQueryInline_100k(b *testing.B) {
31+
n := 100_000
2832
world := NewWorld(1024)
2933

3034
mapper := NewMap2[Position, Velocity](&world)
3135
mapper.NewBatch(n, &Position{}, &Velocity{X: 1, Y: 0})
3236

33-
filter := NewFilter2[Position, Velocity](&world).Register()
34-
for b.Loop() {
37+
filter := NewFilter2[Position, Velocity](&world)
38+
loop := func(filter *Filter2[Position, Velocity]) {
3539
query := filter.Query()
3640
for query.Next() {
3741
pos, vel := query.Get()
3842
pos.X += vel.X
3943
pos.Y += vel.Y
4044
}
4145
}
42-
}
43-
44-
func BenchmarkPosVelQueryUnsafe_1000(b *testing.B) {
45-
n := 1000
46-
world := NewWorld(1024)
47-
48-
posID := ComponentID[Position](&world)
49-
velID := ComponentID[Velocity](&world)
5046

51-
mapper := NewMap2[Position, Velocity](&world)
52-
mapper.NewBatch(n, &Position{}, &Velocity{X: 1, Y: 0})
53-
54-
filter := NewUnsafeFilter(&world, posID, velID)
5547
for b.Loop() {
56-
query := filter.Query()
57-
for query.Next() {
58-
pos := (*Position)(query.Get(posID))
59-
vel := (*Velocity)(query.Get(velID))
60-
pos.X += vel.X
61-
pos.Y += vel.Y
62-
}
48+
loop(filter)
6349
}
6450
}
6551

@@ -120,6 +106,46 @@ func BenchmarkPosVelQueryParallel4_100k(b *testing.B) {
120106
}
121107
}
122108

109+
func BenchmarkPosVelQueryCached_1000(b *testing.B) {
110+
n := 1000
111+
world := NewWorld(1024)
112+
113+
mapper := NewMap2[Position, Velocity](&world)
114+
mapper.NewBatch(n, &Position{}, &Velocity{X: 1, Y: 0})
115+
116+
filter := NewFilter2[Position, Velocity](&world).Register()
117+
for b.Loop() {
118+
query := filter.Query()
119+
for query.Next() {
120+
pos, vel := query.Get()
121+
pos.X += vel.X
122+
pos.Y += vel.Y
123+
}
124+
}
125+
}
126+
127+
func BenchmarkPosVelQueryUnsafe_1000(b *testing.B) {
128+
n := 1000
129+
world := NewWorld(1024)
130+
131+
posID := ComponentID[Position](&world)
132+
velID := ComponentID[Velocity](&world)
133+
134+
mapper := NewMap2[Position, Velocity](&world)
135+
mapper.NewBatch(n, &Position{}, &Velocity{X: 1, Y: 0})
136+
137+
filter := NewUnsafeFilter(&world, posID, velID)
138+
for b.Loop() {
139+
query := filter.Query()
140+
for query.Next() {
141+
pos := (*Position)(query.Get(posID))
142+
vel := (*Velocity)(query.Get(velID))
143+
pos.X += vel.X
144+
pos.Y += vel.Y
145+
}
146+
}
147+
}
148+
123149
func BenchmarkPosVelMap_1000(b *testing.B) {
124150
n := 1000
125151
world := NewWorld(1024)

0 commit comments

Comments
 (0)