Repository: mlange-42/arche
Pull request #165 (2023-03-25 22:10:22)

More efficiently clear the memory of removed components
* More efficiently clear the memory of removed components, with 2-3x speedup
* Do not clear when allocating, as it is already zero


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1acd33c..add6953 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@
 * 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)
+* More efficiently clear the memory of removed components, with 2-3x speedup (#165)
+* Do not clear memory when adding entities to archetypes, not required anymore as of #147 (#165)
 
 ### Bugfixes
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index f899ea0..80e48ff 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -128,6 +128,8 @@ type archetype struct {
 	len               uint32          // Current number of entities
 	cap               uint32          // Current capacity
 	capacityIncrement uint32          // Capacity increment
+	zeroValue         []byte
+	zeroPointer       unsafe.Pointer
 }
 
 // Init initializes an archetype
@@ -147,6 +149,7 @@ func (a *archetype) Init(node *archetypeNode, capacityIncrement int, forStorage
 	}
 
 	prev := -1
+	var maxSize uintptr = 0
 	for i, c := range components {
 		if int(c.ID) <= prev {
 			panic("component arguments must be sorted by ID")
@@ -156,6 +159,9 @@ func (a *archetype) Init(node *archetypeNode, capacityIncrement int, forStorage
 
 		size, align := c.Type.Size(), uintptr(c.Type.Align())
 		size = (size + (align - 1)) / align * align
+		if size > maxSize {
+			maxSize = size
+		}
 
 		a.Ids[i] = c.ID
 		a.buffers[i] = reflect.New(reflect.ArrayOf(cap, c.Type)).Elem()
@@ -178,30 +184,25 @@ func (a *archetype) Init(node *archetypeNode, capacityIncrement int, forStorage
 	a.capacityIncrement = uint32(capacityIncrement)
 	a.len = 0
 	a.cap = uint32(cap)
+
+	if maxSize > 0 {
+		a.zeroValue = make([]byte, maxSize)
+		a.zeroPointer = unsafe.Pointer(&a.zeroValue[0])
+	}
 }
 
 // Add adds an entity with optionally zeroed components to the archetype
-func (a *archetype) Alloc(entity Entity, zero bool) uintptr {
+func (a *archetype) Alloc(entity Entity) uintptr {
 	idx := uintptr(a.len)
 	a.extend(1)
 	a.addEntity(idx, &entity)
-	if zero {
-		a.ZeroAll(idx)
-	}
 	a.len++
 	return idx
 }
 
 // Add adds storage to the archetype
-func (a *archetype) AllocN(count uint32, zero bool) {
-	idx := uintptr(a.len)
+func (a *archetype) AllocN(count uint32) {
 	a.extend(count)
-	if zero {
-		var i uint32
-		for i = 0; i < count; i++ {
-			a.ZeroAll(idx + uintptr(i))
-		}
-	}
 	a.len += count
 }
 
@@ -228,6 +229,9 @@ func (a *archetype) Add(entity Entity, components ...Component) uintptr {
 }
 
 // Remove removes an entity and its components from the archetype.
+//
+// Performs a swap-remove and reports whether a swap was necessary
+// (i.e. not the last entity that was removed).
 func (a *archetype) Remove(index uintptr) bool {
 	swapped := a.removeEntity(index)
 
@@ -264,11 +268,7 @@ func (a *archetype) Zero(index uintptr, id ID) {
 		return
 	}
 	dst := unsafe.Add(lay.pointer, index*lay.itemSize)
-
-	for i := uintptr(0); i < lay.itemSize; i++ {
-		*(*byte)(dst) = 0
-		dst = unsafe.Add(dst, 1)
-	}
+	a.copy(a.zeroPointer, dst, lay.itemSize)
 }
 
 // SetEntity overwrites an entity
diff --git a/ecs/world.go b/ecs/world.go
index bd6eb50..0cf8983 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -110,7 +110,7 @@ func (w *World) NewEntity(comps ...ID) Entity {
 		arch = w.findOrCreateArchetype(arch, comps, nil)
 	}
 
-	entity := w.createEntity(arch, true)
+	entity := w.createEntity(arch)
 
 	if w.listener != nil {
 		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.Ids, 1})
@@ -143,7 +143,7 @@ func (w *World) NewEntityWith(comps ...Component) Entity {
 	arch := w.archetypes.Get(0)
 	arch = w.findOrCreateArchetype(arch, ids, nil)
 
-	entity := w.createEntity(arch, false)
+	entity := w.createEntity(arch)
 
 	for _, c := range comps {
 		w.copyTo(entity, c.ID, c.Comp)
@@ -405,7 +405,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	oldIDs := oldArch.Components()
 
 	arch := w.findOrCreateArchetype(oldArch, add, rem)
-	newIndex := arch.Alloc(entity, false)
+	newIndex := arch.Alloc(entity)
 
 	for _, id := range oldIDs {
 		if mask.Get(id) {
@@ -413,9 +413,6 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 			arch.SetPointer(newIndex, id, comp)
 		}
 	}
-	for _, id := range add {
-		arch.Zero(newIndex, id)
-	}
 
 	swapped := oldArch.Remove(index.index)
 
@@ -568,7 +565,7 @@ func (w *World) newEntitiesNoNotify(count int, comps ...ID) (*archetype, uint32)
 		arch = w.findOrCreateArchetype(arch, comps, nil)
 	}
 	startIdx := arch.Len()
-	w.createEntities(arch, uint32(count), true)
+	w.createEntities(arch, uint32(count))
 
 	return arch, startIdx
 }
@@ -591,7 +588,7 @@ func (w *World) newEntitiesWithNoNotify(count int, ids []ID, comps ...Component)
 		arch = w.findOrCreateArchetype(arch, ids, nil)
 	}
 	startIdx := arch.Len()
-	w.createEntities(arch, uint32(count), true)
+	w.createEntities(arch, uint32(count))
 
 	var i uint32
 	for i = 0; i < cnt; i++ {
@@ -606,9 +603,9 @@ func (w *World) newEntitiesWithNoNotify(count int, ids []ID, comps ...Component)
 }
 
 // createEntity creates an Entity and adds it to the given archetype.
-func (w *World) createEntity(arch *archetype, zero bool) Entity {
+func (w *World) createEntity(arch *archetype) Entity {
 	entity := w.entityPool.Get()
-	idx := arch.Alloc(entity, zero)
+	idx := arch.Alloc(entity)
 	len := len(w.entities)
 	if int(entity.id) == len {
 		if len == cap(w.entities) {
@@ -624,9 +621,9 @@ func (w *World) createEntity(arch *archetype, zero bool) Entity {
 }
 
 // createEntity creates multiple Entities and adds them to the given archetype.
-func (w *World) createEntities(arch *archetype, count uint32, zero bool) {
+func (w *World) createEntities(arch *archetype, count uint32) {
 	startIdx := arch.Len()
-	arch.AllocN(uint32(count), zero)
+	arch.AllocN(uint32(count))
 
 	len := len(w.entities)
 	required := len + int(count) - w.entityPool.Available()


Test patch:
diff --git a/benchmark/arche/move/arche_test.go b/benchmark/arche/move/arche_test.go
index dc1ee3e..00ff98a 100644
--- a/benchmark/arche/move/arche_test.go
+++ b/benchmark/arche/move/arche_test.go
@@ -26,10 +26,13 @@ func runArcheMove(b *testing.B, count int, add, rem []g.Comp) {
 		}
 
 		entities := make([]ecs.Entity, count)
-		for i := 0; i < count; i++ {
-			e := world.NewEntity(addIDs...)
-			entities[i] = e
+		query := world.Batch().NewEntitiesQuery(count, addIDs...)
+		cnt := 0
+		for query.Next() {
+			entities[cnt] = query.Entity()
+			cnt++
 		}
+
 		b.StartTimer()
 
 		for _, e := range entities {
@@ -40,14 +43,14 @@ func runArcheMove(b *testing.B, count int, add, rem []g.Comp) {
 
 func BenchmarkArcheMove_1C_1_000(b *testing.B) {
 	runArcheMove(b, 1000,
-		g.T1[c.TestStruct0](),
+		g.T2[c.TestStruct0, c.TestStruct1](),
 		g.T1[c.TestStruct0](),
 	)
 }
 
 func BenchmarkArcheMove_5C_1_000(b *testing.B) {
 	runArcheMove(b, 1000,
-		g.T5[c.TestStruct0, c.TestStruct1, c.TestStruct2, c.TestStruct3, c.TestStruct4](),
+		g.T6[c.TestStruct0, c.TestStruct1, c.TestStruct2, c.TestStruct3, c.TestStruct4, c.TestStruct5](),
 		g.T1[c.TestStruct0](),
 	)
 }
diff --git a/benchmark/arche/move/ggecs_test.go b/benchmark/arche/move/ggecs_test.go
index 7a736b5..d878bda 100644
--- a/benchmark/arche/move/ggecs_test.go
+++ b/benchmark/arche/move/ggecs_test.go
@@ -49,14 +49,14 @@ func runGameEngineEcsMove(b *testing.B, count int, add, rem []uint) {
 
 func BenchmarkGGECSMove_1C_1_000(b *testing.B) {
 	runGameEngineEcsMove(b, 1000,
-		[]uint{0},
+		[]uint{0, 1},
 		[]uint{0},
 	)
 }
 
 func BenchmarkGGECSMove_5C_1_000(b *testing.B) {
 	runGameEngineEcsMove(b, 1000,
-		[]uint{0, 1, 2, 3, 4},
+		[]uint{0, 1, 2, 3, 4, 5},
 		[]uint{0},
 	)
 }
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 954dcb1..306ddd7 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -119,14 +119,14 @@ func TestArchetypeAlloc(t *testing.T) {
 	assert.Equal(t, 8, int(arch.Cap()))
 	assert.Equal(t, 0, int(arch.Len()))
 
-	arch.AllocN(1, true)
+	arch.AllocN(1)
 	assert.Equal(t, 1, int(arch.Len()))
 
-	arch.AllocN(7, true)
+	arch.AllocN(7)
 	assert.Equal(t, 8, int(arch.Len()))
 	assert.Equal(t, 8, int(arch.Cap()))
 
-	arch.AllocN(1, true)
+	arch.AllocN(1)
 	assert.Equal(t, 9, int(arch.Len()))
 	assert.Equal(t, 16, int(arch.Cap()))
 }
@@ -210,6 +210,37 @@ func TestArchetypeReset(t *testing.T) {
 	assert.Equal(t, 2, int(arch.Len()))
 }
 
+func TestArchetypeZero(t *testing.T) {
+	comps := []componentType{
+		{ID: 0, Type: reflect.TypeOf(position{})},
+		{ID: 1, Type: reflect.TypeOf(rotation{})},
+	}
+
+	arch := archetype{}
+	arch.Init(nil, 32, false, comps...)
+
+	arch.Alloc(newEntity(0))
+	arch.Alloc(newEntity(1))
+
+	assert.Equal(t, position{0, 0}, *(*position)(arch.Get(0, 0)))
+	assert.Equal(t, position{0, 0}, *(*position)(arch.Get(1, 0)))
+
+	pos := (*position)(arch.Get(0, 0))
+	pos.X = 100
+	pos = (*position)(arch.Get(1, 0))
+	pos.X = 100
+
+	assert.Equal(t, position{100, 0}, *(*position)(arch.Get(0, 0)))
+	assert.Equal(t, position{100, 0}, *(*position)(arch.Get(1, 0)))
+
+	arch.Remove(0)
+	arch.Remove(0)
+	arch.Alloc(newEntity(0))
+	arch.Alloc(newEntity(1))
+	assert.Equal(t, position{0, 0}, *(*position)(arch.Get(0, 0)))
+	assert.Equal(t, position{0, 0}, *(*position)(arch.Get(1, 0)))
+}
+
 func BenchmarkIterArchetype_1000(b *testing.B) {
 	b.StopTimer()
 	comps := []componentType{
@@ -220,7 +251,7 @@ func BenchmarkIterArchetype_1000(b *testing.B) {
 	arch.Init(nil, 32, true, comps...)
 
 	for i := 0; i < 1000; i++ {
-		arch.Alloc(newEntity(eid(i)), true)
+		arch.Alloc(newEntity(eid(i)))
 	}
 	b.StartTimer()