Repository: mlange-42/arche
Pull request #128 (2023-03-19 16:25:14)

Minor component storage optimization
* Reduce size of `storage`
* Avoid integer conversions by more us of `uintptr`
* Move nil check from archetypes to storage


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7092854..8811884 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@
 
 ### Other
 
-* Speedup archetype access by 10%, by elimination of bounds checks (!125)
+* Speedup archetype access by 10%, by elimination of bounds checks (!126)
+* Minor optimizations of component storage (!127)
 
 ## [[v0.4.5]](https://github.com/mlange-42/arche/compare/v0.4.4...v0.4.5)
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index f78002d..91222b5 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -48,10 +48,8 @@ func (a *archetypeNode) SetTransitionRemove(id ID, to *archetypeNode) {
 
 // archetype represents an ECS archetype
 type archetype struct {
-	Mask Mask
-	Ids  []ID
-	// Indirection to avoid a fixed-size array of storages
-	// Increases access time by 50-100%
+	Mask        Mask
+	Ids         []ID
 	references  []*storage
 	entities    genericStorage[Entity]
 	components  []storage
@@ -97,12 +95,8 @@ func (a *archetype) GetEntity(index uint32) Entity {
 }
 
 // Get returns the component with the given ID at the given index
-func (a *archetype) Get(index uint32, id ID) unsafe.Pointer {
-	ref := a.getStorage(id)
-	if ref != nil {
-		return ref.Get(index)
-	}
-	return nil
+func (a *archetype) Get(index uintptr, id ID) unsafe.Pointer {
+	return a.getStorage(id).Get(index)
 }
 
 func (a *archetype) getStorage(id ID) *storage {
@@ -110,11 +104,12 @@ func (a *archetype) getStorage(id ID) *storage {
 }
 
 // Add adds an entity with zeroed components to the archetype
-func (a *archetype) Alloc(entity Entity, zero bool) uint32 {
-	idx := a.entities.Add(entity)
-	len := len(a.components)
+func (a *archetype) Alloc(entity Entity, zero bool) uintptr {
+	idx := uintptr(a.entities.Add(entity))
+	len := uintptr(len(a.components))
 
-	for i := 0; i < len; i++ {
+	var i uintptr
+	for i = 0; i < len; i++ {
 		comp := &a.components[i]
 		idx := comp.Alloc()
 		if zero {
@@ -137,8 +132,8 @@ func (a *archetype) Add(entity Entity, components ...Component) uint32 {
 }
 
 // Remove removes an entity from the archetype
-func (a *archetype) Remove(index uint32) bool {
-	swapped := a.entities.Remove(index)
+func (a *archetype) Remove(index uintptr) bool {
+	swapped := a.entities.Remove(uint32(index))
 	len := len(a.components)
 	for i := 0; i < len; i++ {
 		a.components[i].Remove(index)
@@ -167,17 +162,17 @@ func (a *archetype) Cap() uint32 {
 }
 
 // Set overwrites a component with the data behind the given pointer
-func (a *archetype) Set(index uint32, id ID, comp interface{}) unsafe.Pointer {
+func (a *archetype) Set(index uintptr, id ID, comp interface{}) unsafe.Pointer {
 	return a.getStorage(id).Set(index, comp)
 }
 
 // SetPointer overwrites a component with the data behind the given pointer
-func (a *archetype) SetPointer(index uint32, id ID, comp unsafe.Pointer) unsafe.Pointer {
+func (a *archetype) SetPointer(index uintptr, id ID, comp unsafe.Pointer) unsafe.Pointer {
 	return a.getStorage(id).SetPointer(index, comp)
 }
 
 // Zero resets th memory at the given position
-func (a *archetype) Zero(index uint32, id ID) {
+func (a *archetype) Zero(index uintptr, id ID) {
 	a.getStorage(id).Zero(index)
 }
 
diff --git a/ecs/entity.go b/ecs/entity.go
index 592ade8..6b6bf75 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -35,5 +35,5 @@ func (e Entity) IsZero() bool {
 // entityIndex indicates where an entity is currently stored
 type entityIndex struct {
 	arch  *archetype
-	index uint32
+	index uintptr
 }
diff --git a/ecs/query.go b/ecs/query.go
index fd93889..56508e9 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -149,14 +149,14 @@ func (q *queryIter) Close() {
 
 type archetypeIter struct {
 	Archetype *archetype
-	Length    uint32
-	Index     uint32
+	Length    uintptr
+	Index     uintptr
 }
 
 func newArchetypeIter(arch *archetype) archetypeIter {
 	return archetypeIter{
 		Archetype: arch,
-		Length:    arch.Len(),
+		Length:    uintptr(arch.Len()),
 	}
 }
 
@@ -169,7 +169,7 @@ func (it *archetypeIter) Step(count uint32) (int, bool) {
 	if it.Length == 0 {
 		return int(count - 1), false
 	}
-	it.Index += count
+	it.Index += uintptr(count)
 	if it.Index < it.Length {
 		return 0, true
 	}
@@ -188,5 +188,5 @@ func (it *archetypeIter) Get(comp ID) unsafe.Pointer {
 
 // Entity returns the entity at the iterator's position
 func (it *archetypeIter) Entity() Entity {
-	return it.Archetype.GetEntity(it.Index)
+	return it.Archetype.GetEntity(uint32(it.Index))
 }
diff --git a/ecs/storage.go b/ecs/storage.go
index 858842d..1f83af5 100644
--- a/ecs/storage.go
+++ b/ecs/storage.go
@@ -10,7 +10,6 @@ import (
 type storage struct {
 	buffer            reflect.Value
 	bufferAddress     unsafe.Pointer
-	typeOf            reflect.Type
 	itemSize          uintptr
 	len               uint32
 	cap               uint32
@@ -29,7 +28,6 @@ func (s *storage) Init(tp reflect.Type, increment int, forStorage bool) {
 	}
 	s.buffer = reflect.New(reflect.ArrayOf(cap, tp)).Elem()
 	s.bufferAddress = s.buffer.Addr().UnsafePointer()
-	s.typeOf = tp
 	s.itemSize = size
 	s.len = 0
 	s.cap = uint32(cap)
@@ -37,15 +35,18 @@ func (s *storage) Init(tp reflect.Type, increment int, forStorage bool) {
 }
 
 // Get retrieves an unsafe pointer to an element
-func (s *storage) Get(index uint32) unsafe.Pointer {
-	return unsafe.Add(s.bufferAddress, uintptr(index)*s.itemSize)
+func (s *storage) Get(index uintptr) unsafe.Pointer {
+	if s == nil {
+		return nil
+	}
+	return unsafe.Add(s.bufferAddress, index*s.itemSize)
 }
 
 // Add adds an element to the end of the storage
 func (s *storage) Add(value interface{}) (index uint32) {
 	s.extend()
 	s.len++
-	s.Set(s.len-1, value)
+	s.Set(uintptr(s.len-1), value)
 	return s.len - 1
 }
 
@@ -53,17 +54,17 @@ func (s *storage) Add(value interface{}) (index uint32) {
 func (s *storage) AddPointer(value unsafe.Pointer) (index uint32) {
 	s.extend()
 	s.len++
-	s.SetPointer(s.len-1, value)
+	s.SetPointer(uintptr(s.len-1), value)
 	return s.len - 1
 }
 
 // Alloc adds an empty element to the end of the storage.
 // It does not zero the storage!
-func (s *storage) Alloc() (index uint32) {
+func (s *storage) Alloc() (index uintptr) {
 	s.extend()
 	s.len++
 	//s.Zero(s.len - 1)
-	return s.len - 1
+	return uintptr(s.len - 1)
 }
 
 func (s *storage) extend() {
@@ -73,15 +74,15 @@ func (s *storage) extend() {
 
 	old := s.buffer
 	s.cap = s.capacityIncrement * ((s.cap + s.capacityIncrement) / s.capacityIncrement)
-	s.buffer = reflect.New(reflect.ArrayOf(int(s.cap), s.typeOf)).Elem()
+	s.buffer = reflect.New(reflect.ArrayOf(int(s.cap), s.buffer.Type().Elem())).Elem()
 	s.bufferAddress = s.buffer.Addr().UnsafePointer()
 	reflect.Copy(s.buffer, old)
 }
 
 // Remove swap-removes an element
-func (s *storage) Remove(index uint32) bool {
-	o := s.len - 1
-	n := index
+func (s *storage) Remove(index uintptr) bool {
+	o := uintptr(s.len - 1)
+	n := uintptr(index)
 
 	if n == o || s.itemSize == 0 {
 		s.len--
@@ -91,8 +92,8 @@ func (s *storage) Remove(index uint32) bool {
 	// TODO shrink the underlying data arrays?
 	size := s.itemSize
 
-	src := unsafe.Add(s.bufferAddress, uintptr(o)*s.itemSize)
-	dst := unsafe.Add(s.bufferAddress, uintptr(n)*s.itemSize)
+	src := unsafe.Add(s.bufferAddress, o*s.itemSize)
+	dst := unsafe.Add(s.bufferAddress, n*s.itemSize)
 
 	dstSlice := (*[math.MaxInt32]byte)(dst)[:size:size]
 	srcSlice := (*[math.MaxInt32]byte)(src)[:size:size]
@@ -104,7 +105,7 @@ func (s *storage) Remove(index uint32) bool {
 }
 
 // Set sets the storage at the given index
-func (s *storage) Set(index uint32, value interface{}) unsafe.Pointer {
+func (s *storage) Set(index uintptr, value interface{}) unsafe.Pointer {
 	dst := s.Get(index)
 
 	if s.itemSize == 0 {
@@ -124,7 +125,7 @@ func (s *storage) Set(index uint32, value interface{}) unsafe.Pointer {
 	return dst
 }
 
-func (s *storage) SetPointer(index uint32, value unsafe.Pointer) unsafe.Pointer {
+func (s *storage) SetPointer(index uintptr, value unsafe.Pointer) unsafe.Pointer {
 	dst := s.Get(index)
 	if s.itemSize == 0 {
 		return dst
@@ -141,7 +142,7 @@ func (s *storage) SetPointer(index uint32, value unsafe.Pointer) unsafe.Pointer
 }
 
 // Zero resets a block of storage
-func (s *storage) Zero(index uint32) {
+func (s *storage) Zero(index uintptr) {
 	if s.itemSize == 0 {
 		return
 	}
@@ -167,8 +168,9 @@ func (s *storage) Cap() uint32 {
 // toSlice converts the content of a storage to a slice of structs
 func toSlice[T any](s storage) []T {
 	res := make([]T, s.Len())
-	var i uint32
-	for i = 0; i < s.Len(); i++ {
+	var i uintptr
+	len := uintptr(s.Len())
+	for i = 0; i < len; i++ {
 		ptr := (*T)(s.Get(i))
 		res[i] = *ptr
 	}
diff --git a/ecs/world.go b/ecs/world.go
index 4ed7376..77f1c61 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -172,7 +172,7 @@ func (w *World) RemoveEntity(entity Entity) {
 	w.entityPool.Recycle(entity)
 
 	if swapped {
-		swapEntity := oldArch.GetEntity(index.index)
+		swapEntity := oldArch.GetEntity(uint32(index.index))
 		w.entities[swapEntity.id].index = index.index
 	}
 
@@ -352,7 +352,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	swapped := oldArch.Remove(index.index)
 
 	if swapped {
-		swapEntity := oldArch.GetEntity(index.index)
+		swapEntity := oldArch.GetEntity(uint32(index.index))
 		w.entities[swapEntity.id].index = index.index
 	}
 	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex}


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 1568c93..0ade5b9 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -104,7 +104,7 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 		len := int(arch.Len())
 		id := ID(0)
 		for j := 0; j < len; j++ {
-			pos := (*testStruct0)(arch.Get(uint32(j), id))
+			pos := (*testStruct0)(arch.Get(uintptr(j), id))
 			pos.Val = 1
 		}
 	}
diff --git a/ecs/storage_test.go b/ecs/storage_test.go
index eeb6787..aed2f72 100644
--- a/ecs/storage_test.go
+++ b/ecs/storage_test.go
@@ -186,9 +186,10 @@ func BenchmarkIterStorage_1000(b *testing.B) {
 	b.StartTimer()
 
 	for i := 0; i < b.N; i++ {
-		len := int(s.Len())
-		for j := 0; j < len; j++ {
-			a := (*testStruct0)(s.Get(uint32(j)))
+		len := uintptr(s.Len())
+		var j uintptr
+		for j = 0; j < len; j++ {
+			a := (*testStruct0)(s.Get(j))
 			a.Val = 1
 		}
 	}