Repository: mlange-42/arche
Pull request #50 (2023-02-10 12:46:57)

Optimize component access in archetypes
* Move component check from world to archetype
* Add unsafe but faster archetype access for use by queries


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 117560c..9a51b3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
 * Use of an archetype graph to speed up finding the target archetype for component addition/removal (#42)
 * Reduced dependencies by moving profiling and benchmarking to sub-modules (#46)
 * Smaller integer type for component identifiers (#47)
+* Minor optimization of component access by queries (#50)
 
 ## [[v0.2.0]](https://github.com/mlange-42/arche/compare/v0.1.4...v0.2.0)
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 2556de4..d36332e 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -54,6 +54,17 @@ func (a *archetype) GetEntity(index int) Entity {
 
 // Get returns the component with the given ID at the given index
 func (a *archetype) Get(index int, id ID) unsafe.Pointer {
+	if !a.mask.Get(id) {
+		return nil
+	}
+	return a.components[a.indices[id]].Get(uint32(index))
+}
+
+// GetUnsafe returns the component with the given ID at the given index,
+// without checking if the entity contains that component.
+//
+// This is used by queries, where the entity is guaranteed to be in the archetype.
+func (a *archetype) GetUnsafe(index int, id ID) unsafe.Pointer {
 	return a.components[a.indices[id]].Get(uint32(index))
 }
 
diff --git a/ecs/query.go b/ecs/query.go
index 2c8ffd8..61df0f8 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -100,7 +100,7 @@ func (it *archetypeIter) Has(comp ID) bool {
 
 // Get returns the pointer to the given component at the iterator's position
 func (it *archetypeIter) Get(comp ID) unsafe.Pointer {
-	return it.Archetype.Get(it.Index, comp)
+	return it.Archetype.GetUnsafe(it.Index, comp)
 }
 
 // Entity returns the entity at the iterator's position
diff --git a/ecs/world.go b/ecs/world.go
index 89e62b8..4303426 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -107,13 +107,7 @@ func (w *World) Alive(entity Entity) bool {
 // See also [Map.Get] for a generic variant.
 func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 	index := w.entities[entity.id]
-	arch := index.arch
-
-	if !arch.HasComponent(comp) {
-		return nil
-	}
-
-	return arch.Get(int(index.index), comp)
+	return index.arch.Get(int(index.index), comp)
 }
 
 // Has returns whether an [Entity] has a given component.
@@ -122,8 +116,7 @@ func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 //
 // See also [Map.Has] for a generic variant.
 func (w *World) Has(entity Entity, comp ID) bool {
-	index := w.entities[entity.id]
-	return index.arch.HasComponent(comp)
+	return w.entities[entity.id].arch.HasComponent(comp)
 }
 
 // Add adds components to an [Entity].


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 53e2049..eadd92f 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -116,3 +116,30 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 		}
 	}
 }
+
+func BenchmarkArchetypeAccessUnsafe_1000(b *testing.B) {
+	b.StopTimer()
+	comps := []componentType{
+		{ID: 0, Type: reflect.TypeOf(testStruct{})},
+	}
+
+	arch := archetype{}
+	arch.init(32, comps...)
+
+	for i := 0; i < 1000; i++ {
+		arch.Add(
+			newEntity(eid(i)),
+			Component{ID: 0, Component: &testStruct{}},
+		)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		len := int(arch.Len())
+		id := ID(0)
+		for j := 0; j < len; j++ {
+			pos := (*testStruct)(arch.GetUnsafe(i, id))
+			_ = pos
+		}
+	}
+}