Repository: mlange-42/arche
Pull request #159 (2023-03-24 23:15:12)

Optimize entity batch-allocation
Reserve sufficient memory for all entities to add ad once, not in increments


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c4f5d7..ee542fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@
 * Speedup of archetype mask checks by 10% by checking mask before empty archetype (#139)
 * Speedup of generic queries and mappers to come closer to ID-based access (#144)
 * Speedup of archetype mask checks by casting filter interface to concrete type when possible (#148)
+* Optimized batch creation of entities (#159)
 
 ### Bugfixes
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 4c77298..19313bb 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -372,10 +372,7 @@ func (a *archetype) extend(by uint32) {
 	if a.cap >= required {
 		return
 	}
-	a.cap = a.capacityIncrement * (required / a.capacityIncrement)
-	if required%a.capacityIncrement != 0 {
-		a.cap += a.capacityIncrement
-	}
+	a.cap = capacityU32(required, a.capacityIncrement)
 
 	old := a.entityBuffer
 	a.entityBuffer = reflect.New(reflect.ArrayOf(int(a.cap), entityType)).Elem()
diff --git a/ecs/util.go b/ecs/util.go
new file mode 100644
index 0000000..8d9cdc6
--- /dev/null
+++ b/ecs/util.go
@@ -0,0 +1,17 @@
+package ecs
+
+func capacity(size, increment int) int {
+	cap := increment * (size / increment)
+	if size%increment != 0 {
+		cap += increment
+	}
+	return cap
+}
+
+func capacityU32(size, increment uint32) uint32 {
+	cap := increment * (size / increment)
+	if size%increment != 0 {
+		cap += increment
+	}
+	return cap
+}
diff --git a/ecs/world.go b/ecs/world.go
index cdf4dbb..4d4a272 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -617,22 +617,23 @@ func (w *World) createEntities(arch *archetype, count uint32, zero bool) {
 	startIdx := arch.Len()
 	arch.AllocN(uint32(count), zero)
 
+	len := len(w.entities)
+	required := len + int(count) - w.entityPool.Available()
+	if required > cap(w.entities) {
+		cap := capacity(required, w.config.CapacityIncrement)
+		old := w.entities
+		w.entities = make([]entityIndex, required, cap)
+		copy(w.entities, old)
+	} else if required > len {
+		w.entities = w.entities[:required]
+	}
+
 	var i uint32
 	for i = 0; i < count; i++ {
 		idx := startIdx + i
 		entity := w.entityPool.Get()
 		arch.SetEntity(uintptr(idx), entity)
-		len := len(w.entities)
-		if int(entity.id) == len {
-			if len == cap(w.entities) {
-				old := w.entities
-				w.entities = make([]entityIndex, len, len+w.config.CapacityIncrement)
-				copy(w.entities, old)
-			}
-			w.entities = append(w.entities, entityIndex{arch: arch, index: uintptr(idx)})
-		} else {
-			w.entities[entity.id] = entityIndex{arch: arch, index: uintptr(idx)}
-		}
+		w.entities[entity.id] = entityIndex{arch: arch, index: uintptr(idx)}
 	}
 }
 


Test patch:
diff --git a/ecs/util_test.go b/ecs/util_test.go
new file mode 100644
index 0000000..1e1073f
--- /dev/null
+++ b/ecs/util_test.go
@@ -0,0 +1,21 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCapacity(t *testing.T) {
+	assert.Equal(t, 0, capacity(0, 8))
+	assert.Equal(t, 8, capacity(1, 8))
+	assert.Equal(t, 8, capacity(8, 8))
+	assert.Equal(t, 16, capacity(9, 8))
+}
+
+func TestCapacityU32(t *testing.T) {
+	assert.Equal(t, 0, int(capacityU32(0, 8)))
+	assert.Equal(t, 8, int(capacityU32(1, 8)))
+	assert.Equal(t, 8, int(capacityU32(8, 8)))
+	assert.Equal(t, 16, int(capacityU32(9, 8)))
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 8a349c1..f62930b 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -372,11 +372,13 @@ func TestWorldNewEntities(t *testing.T) {
 	rotID := ComponentID[rotation](&world)
 
 	world.NewEntity(posID, rotID)
+	assert.Equal(t, 2, len(world.entities))
 
 	assert.Panics(t, func() { world.newEntitiesQuery(0, posID, rotID) })
 
 	query := world.newEntitiesQuery(100, posID, rotID)
 	assert.Equal(t, 100, query.Count())
+	assert.Equal(t, 102, len(world.entities))
 	assert.Equal(t, 1, len(events))
 
 	cnt := 0
@@ -400,6 +402,7 @@ func TestWorldNewEntities(t *testing.T) {
 	}
 
 	world.Reset()
+	assert.Equal(t, 1, len(world.entities))
 
 	query = world.newEntitiesQuery(100, posID, rotID)
 	assert.Equal(t, 100, query.Count())
@@ -418,14 +421,17 @@ func TestWorldNewEntities(t *testing.T) {
 		world.RemoveEntity(e)
 	}
 	assert.Equal(t, 301, len(events))
+	assert.Equal(t, 101, len(world.entities))
 
 	query = world.newEntitiesQuery(100, posID, rotID)
 	assert.Equal(t, 301, len(events))
 	query.Close()
 	assert.Equal(t, 401, len(events))
+	assert.Equal(t, 101, len(world.entities))
 
 	world.newEntities(100, posID, rotID)
 	assert.Equal(t, 501, len(events))
+	assert.Equal(t, 201, len(world.entities))
 }
 
 func TestWorldNewEntitiesWith(t *testing.T) {