Repository: mlange-42/arche
Pull request #7 (2023-02-06 09:18:24)

Avoid heap escape of Storage and Archetype


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 8be2dcd..f98c66a 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -4,28 +4,17 @@ import (
 	"unsafe"
 )
 
-// Archetype is the interface for archetypes
-type Archetype interface {
-	GetEntity(index int) Entity
-	Get(index int, id ID) unsafe.Pointer
-	Add(entity Entity, components ...Component)
-	Remove(index int)
-}
-
-// NewArchetype creates a new Archetype for the given components
-// Component arguments must be sorted by ID!
-func NewArchetype(components ...Component) Archetype {
-	return newArchetype(components...)
-}
-
-type archetype struct {
+// Archetype represents an ECS archetype
+type Archetype struct {
 	mask       Mask
 	indices    []ID
 	entities   Storage
 	components []Storage
 }
 
-func newArchetype(components ...Component) *archetype {
+// NewArchetype creates a new Archetype for the given components
+// Component arguments must be sorted by ID!
+func NewArchetype(components ...Component) Archetype {
 	var mask Mask
 	indices := make([]ID, len(components))
 	comps := make([]Storage, MaskTotalBits)
@@ -35,7 +24,7 @@ func newArchetype(components ...Component) *archetype {
 		comps[c.ID] = NewReflectStorage(c.Component, 32)
 	}
 
-	return &archetype{
+	return Archetype{
 		mask:       mask,
 		indices:    indices,
 		entities:   NewReflectStorage(Entity{}, 32),
@@ -44,17 +33,17 @@ func newArchetype(components ...Component) *archetype {
 }
 
 // GetEntity returns the entity at the given index
-func (a *archetype) GetEntity(index int) Entity {
+func (a *Archetype) GetEntity(index int) Entity {
 	return *(*Entity)(a.entities.Get(uint32(index)))
 }
 
 // GetEntity returns the component with the given ID at the given index
-func (a *archetype) Get(index int, id ID) unsafe.Pointer {
+func (a *Archetype) Get(index int, id ID) unsafe.Pointer {
 	return a.components[id].Get(uint32(index))
 }
 
 // Add adds an entity with components to the archetype
-func (a *archetype) Add(entity Entity, components ...Component) {
+func (a *Archetype) Add(entity Entity, components ...Component) {
 	if len(components) != len(a.indices) {
 		panic("Invalid number of components")
 	}
@@ -65,7 +54,7 @@ func (a *archetype) Add(entity Entity, components ...Component) {
 }
 
 // Remove removes an entity from the archetype
-func (a *archetype) Remove(index int) {
+func (a *Archetype) Remove(index int) {
 	a.entities.Remove(uint32(index))
 	for _, c := range a.indices {
 		a.components[c].Remove(uint32(index))
diff --git a/ecs/storage.go b/ecs/storage.go
index c34b1ff..8f10ce7 100644
--- a/ecs/storage.go
+++ b/ecs/storage.go
@@ -6,16 +6,8 @@ import (
 	"unsafe"
 )
 
-// Storage is the interface for component storage
-type Storage interface {
-	Get(index uint32) unsafe.Pointer
-	Add(value interface{}) (index uint32)
-	Remove(index uint32)
-	Len() uint32
-}
-
-// ReflectStorage is a storage implementation that works with reflection
-type ReflectStorage struct {
+// Storage is a storage implementation that works with reflection
+type Storage struct {
 	buffer            reflect.Value
 	bufferAddress     unsafe.Pointer
 	typeOf            reflect.Type
@@ -26,12 +18,12 @@ type ReflectStorage struct {
 }
 
 // NewReflectStorage creates a new ReflectStorage
-func NewReflectStorage(obj interface{}, increment int) *ReflectStorage {
+func NewReflectStorage(obj interface{}, increment int) Storage {
 	tp := reflect.TypeOf(obj)
 	size := tp.Size()
 
 	buffer := reflect.New(reflect.ArrayOf(increment, tp)).Elem()
-	return &ReflectStorage{
+	return Storage{
 		buffer:            buffer,
 		bufferAddress:     buffer.Addr().UnsafePointer(),
 		typeOf:            tp,
@@ -43,13 +35,13 @@ func NewReflectStorage(obj interface{}, increment int) *ReflectStorage {
 }
 
 // Get retrieves an unsafe pointer to an element
-func (s *ReflectStorage) Get(index uint32) unsafe.Pointer {
+func (s *Storage) Get(index uint32) unsafe.Pointer {
 	ptr := unsafe.Add(s.bufferAddress, uintptr(index)*s.itemSize)
 	return unsafe.Pointer(ptr)
 }
 
 // Add adds an element to the end of the storage
-func (s *ReflectStorage) Add(value interface{}) (index uint32) {
+func (s *Storage) Add(value interface{}) (index uint32) {
 	if s.cap < s.len+1 {
 		old := s.buffer
 		s.cap = s.cap + s.capacityIncrement
@@ -63,7 +55,7 @@ func (s *ReflectStorage) Add(value interface{}) (index uint32) {
 }
 
 // Remove swap-removes an element
-func (s *ReflectStorage) Remove(index uint32) {
+func (s *Storage) Remove(index uint32) {
 	o := s.len - 1
 	n := index
 	size := s.itemSize
@@ -80,7 +72,7 @@ func (s *ReflectStorage) Remove(index uint32) {
 	// TODO shrink the underlying data arrays
 }
 
-func (s *ReflectStorage) set(index uint32, value interface{}) {
+func (s *Storage) set(index uint32, value interface{}) {
 	rValue := reflect.ValueOf(value)
 	dst := s.Get(index)
 	var src unsafe.Pointer
@@ -95,7 +87,7 @@ func (s *ReflectStorage) set(index uint32, value interface{}) {
 }
 
 // Len returns the number of items in the storage
-func (s *ReflectStorage) Len() uint32 {
+func (s *Storage) Len() uint32 {
 	return s.len
 }
 


Test patch:
diff --git a/benchmark/arche_test.go b/benchmark/arche_test.go
index b2a62b1..b3359dc 100644
--- a/benchmark/arche_test.go
+++ b/benchmark/arche_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mlange-42/arche/ecs"
 )
 
-func BenchmarkArche(b *testing.B) {
+func runArche(b *testing.B, count int) {
 	world := ecs.NewWorld()
 
 	comps := []ecs.Component{
@@ -16,7 +16,7 @@ func BenchmarkArche(b *testing.B) {
 
 	arch := ecs.NewArchetype(comps...)
 
-	for i := 0; i < 1000; i++ {
+	for i := 0; i < count; i++ {
 		arch.Add(
 			world.NewEntity(),
 			ecs.Component{ID: 0, Component: &position{1, 2}},
@@ -25,9 +25,21 @@ func BenchmarkArche(b *testing.B) {
 	}
 
 	for i := 0; i < b.N; i++ {
-		for j := 0; j < 1000; j++ {
+		for j := 0; j < count; j++ {
 			pos := (*position)(arch.Get(i, ecs.ID(0)))
 			_ = pos
 		}
 	}
 }
+
+func BenchmarkArche100(b *testing.B) {
+	runArche(b, 100)
+}
+
+func BenchmarkArche1000(b *testing.B) {
+	runArche(b, 1000)
+}
+
+func BenchmarkArche10000(b *testing.B) {
+	runArche(b, 10000)
+}
diff --git a/benchmark/geecs_test.go b/benchmark/geecs_test.go
index a54ab9e..a2dfb23 100644
--- a/benchmark/geecs_test.go
+++ b/benchmark/geecs_test.go
@@ -6,14 +6,14 @@ import (
 	ecs "github.com/marioolofo/go-gameengine-ecs"
 )
 
-func BenchmarkGameEngineEcs(b *testing.B) {
+func runGameEngineEcs(b *testing.B, count int) {
 	comps := []ecs.ComponentConfig{
 		{ID: 0, Component: position{}},
 		{ID: 1, Component: rotation{}},
 	}
 	world := ecs.NewWorld(comps...)
 
-	for i := 0; i < 1000; i++ {
+	for i := 0; i < count; i++ {
 		entity := world.NewEntity()
 		world.Assign(entity, 0, 1)
 	}
@@ -27,3 +27,15 @@ func BenchmarkGameEngineEcs(b *testing.B) {
 		}
 	}
 }
+
+func BenchmarkGameEngineEcs100(b *testing.B) {
+	runGameEngineEcs(b, 100)
+}
+
+func BenchmarkGameEngineEcs1000(b *testing.B) {
+	runGameEngineEcs(b, 1000)
+}
+
+func BenchmarkGameEngineEcs10000(b *testing.B) {
+	runGameEngineEcs(b, 10000)
+}
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index a2e368d..b940101 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -21,7 +21,7 @@ func TestArchetype(t *testing.T) {
 		{ID: 1, Component: Rotation{}},
 	}
 
-	arch := newArchetype(comps...)
+	arch := NewArchetype(comps...)
 
 	arch.Add(
 		newEntity(0),
@@ -90,7 +90,7 @@ func BenchmarkArchetypeAccess(b *testing.B) {
 		{ID: 1, Component: Rotation{}},
 	}
 
-	arch := newArchetype(comps...)
+	arch := NewArchetype(comps...)
 
 	for i := 0; i < 1000; i++ {
 		arch.Add(