Repository: mlange-42/arche
Pull request #459 (2025-01-05 23:28:08)

Optimize removal in archetypes
Zeroing memory of removed components in archetypes to make the GC handle pointers properly takes a significant amount of time. With this change, zeroing is only performed on components for which it is really necessary.

Achieves a speedup of approx. 10-20% for methods that remove entities from or move them between archetypes. Slows down component registration by approx. 40ns per component struct field.

Also introduces separate registry types for components and resources. Speeds up resource type registration by approx. 40ns.

coveralls: ## Pull Request Test Coverage Report for [Build 12623865204](https://coveralls.io/builds/71588205)


### Details

* **57** of **74**   **(77.03%)**  changed or added relevant lines in **5** files are covered.
* **2** unchanged lines in **1** file lost coverage.
* Overall coverage decreased (**-0.3%**) to **99.71%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [ecs/registry.go](https://coveralls.io/builds/71588205/source?filename=ecs%2Fregistry.go#L46) | 50 | 67 | 74.63%
<!-- | **Total:** | **57** | **74** | **77.03%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [ecs/archetype.go](https://coveralls.io/builds/71588205/source?filename=ecs%2Farchetype.go#L188) | 2 | 99.16% |
<!-- | **Total:** | **2** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/71588205/badge)](https://coveralls.io/builds/71588205) |
| :-- | --: |
| Change from base [Build 12591341285](https://coveralls.io/builds/71566396): |  -0.3% |
| Covered Lines: | 6527 |
| Relevant Lines: | 6546 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8169a23..df29e8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.14.5...main)
+
+### Performance
+
+* Zeros only the memory of components with pointers, speeding up most component operations and entity removal by 10-20% (#459)
+
 ## [[v0.14.5]](https://github.com/mlange-42/arche/compare/v0.14.4...v0.14.5)
 
 ### Documentation
diff --git a/ecs/archetype.go b/ecs/archetype.go
index fe10d3c..3a951a0 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -173,6 +173,9 @@ func (a *archetype) Remove(index uint32) bool {
 // ZeroAll resets a block of storage in all buffers.
 func (a *archetype) ZeroAll(index uint32) {
 	for _, id := range a.node.Ids {
+		if !a.node.compIsPointer.Get(id) {
+			continue
+		}
 		a.Zero(index, id)
 	}
 }
@@ -181,9 +184,6 @@ func (a *archetype) ZeroAll(index uint32) {
 func (a *archetype) Zero(index uint32, id ID) {
 	lay := a.getLayout(id)
 	size := lay.itemSize
-	if size == 0 {
-		return
-	}
 	dst := unsafe.Add(lay.pointer, index*size)
 	a.copy(a.node.zeroPointer, dst, size)
 }
diff --git a/ecs/archetype_node.go b/ecs/archetype_node.go
index 896db67..7b76686 100644
--- a/ecs/archetype_node.go
+++ b/ecs/archetype_node.go
@@ -27,9 +27,14 @@ type nodeData struct {
 	archetypes        pagedSlice[archetype] // Storage for archetypes in nodes with entity relation
 	archetypeData     pagedSlice[archetypeData]
 	neighbors         idMap[*archNode] // Mapping from component ID to add/remove, to the resulting archetype
+	compIsPointer     *Mask            // Mapping from component IDs to whether they are or contain pointers.
 	capacityIncrement uint32           // Capacity increment
 }
 
+func newNodeData(compIsPointer *Mask) nodeData {
+	return nodeData{compIsPointer: compIsPointer}
+}
+
 // Creates a new archNode
 func newArchNode(mask Mask, data *nodeData, relation ID, hasRelation bool, capacityIncrement int, components []componentType) archNode {
 	var arch map[Entity]*archetype
diff --git a/ecs/registry.go b/ecs/registry.go
index 4f08133..158e8fa 100644
--- a/ecs/registry.go
+++ b/ecs/registry.go
@@ -5,29 +5,27 @@ import (
 	"reflect"
 )
 
-// componentRegistry keeps track of component IDs.
-type componentRegistry struct {
-	Components map[reflect.Type]uint8
-	Types      []reflect.Type
-	IDs        []uint8
-	Used       Mask
-	IsRelation Mask
+// componentRegistry keeps track of type IDs.
+type registry struct {
+	Components map[reflect.Type]uint8 // Mapping from types to IDs.
+	Types      []reflect.Type         // Mapping from IDs to types.
+	IDs        []uint8                // List of IDs.
+	Used       Mask                   // Mapping from IDs tu used status.
 }
 
 // newComponentRegistry creates a new ComponentRegistry.
-func newComponentRegistry() componentRegistry {
-	return componentRegistry{
+func newRegistry() registry {
+	return registry{
 		Components: map[reflect.Type]uint8{},
 		Types:      make([]reflect.Type, MaskTotalBits),
 		Used:       Mask{},
-		IsRelation: Mask{},
 		IDs:        []uint8{},
 	}
 }
 
 // ComponentID returns the ID for a component type, and registers it if not already registered.
 // The second return value indicates if it is a newly created ID.
-func (r *componentRegistry) ComponentID(tp reflect.Type) (uint8, bool) {
+func (r *registry) ComponentID(tp reflect.Type) (uint8, bool) {
 	if id, ok := r.Components[tp]; ok {
 		return id, false
 	}
@@ -35,17 +33,29 @@ func (r *componentRegistry) ComponentID(tp reflect.Type) (uint8, bool) {
 }
 
 // ComponentType returns the type of a component by ID.
-func (r *componentRegistry) ComponentType(id uint8) (reflect.Type, bool) {
+func (r *registry) ComponentType(id uint8) (reflect.Type, bool) {
 	return r.Types[id], r.Used.Get(ID{id: id})
 }
 
 // Count returns the total number of reserved IDs. It is the maximum ID plus 1.
-func (r *componentRegistry) Count() int {
+func (r *registry) Count() int {
 	return len(r.Components)
 }
 
+// Reset clears the registry.
+func (r *registry) Reset() {
+	for t := range r.Components {
+		delete(r.Components, t)
+	}
+	for i := range r.Types {
+		r.Types[i] = nil
+	}
+	r.Used.Reset()
+	r.IDs = r.IDs[:0]
+}
+
 // registerComponent registers a components and assigns an ID for it.
-func (r *componentRegistry) registerComponent(tp reflect.Type, totalBits int) uint8 {
+func (r *registry) registerComponent(tp reflect.Type, totalBits int) uint8 {
 	val := len(r.Components)
 	if val >= totalBits {
 		panic(fmt.Sprintf("exceeded the maximum of %d component types or resource types", totalBits))
@@ -54,24 +64,74 @@ func (r *componentRegistry) registerComponent(tp reflect.Type, totalBits int) ui
 	id := id(newID)
 	r.Components[tp], r.Types[newID] = newID, tp
 	r.Used.Set(id, true)
-	if r.isRelation(tp) {
-		r.IsRelation.Set(id, true)
-	}
 	r.IDs = append(r.IDs, newID)
 	return newID
 }
 
-func (r *componentRegistry) unregisterLastComponent() {
+func (r *registry) unregisterLastComponent() {
 	newID := uint8(len(r.Components) - 1)
 	id := id(newID)
 	tp, _ := r.ComponentType(newID)
 	delete(r.Components, tp)
 	r.Types[newID] = nil
 	r.Used.Set(id, false)
-	r.IsRelation.Set(id, false)
 	r.IDs = r.IDs[:len(r.IDs)-1]
 }
 
+// componentRegistry keeps track of component IDs.
+// In addition to [registry], it determines whether types
+// are relation components and/or contain (or are) pointers.
+type componentRegistry struct {
+	registry
+	IsRelation Mask // Mapping from IDs to whether the component is a relation component.
+	IsPointer  Mask // Mapping from IDs to whether the component contains pointers.
+}
+
+// newComponentRegistry creates a new ComponentRegistry.
+func newComponentRegistry() componentRegistry {
+	return componentRegistry{
+		registry:   newRegistry(),
+		IsRelation: Mask{},
+		IsPointer:  Mask{},
+	}
+}
+
+// ComponentID returns the ID for a component type, and registers it if not already registered.
+// The second return value indicates if it is a newly created ID.
+func (r *componentRegistry) ComponentID(tp reflect.Type) (uint8, bool) {
+	if id, ok := r.Components[tp]; ok {
+		return id, false
+	}
+	return r.registerComponent(tp, MaskTotalBits), true
+}
+
+// Reset clears the registry.
+func (r *componentRegistry) Reset() {
+	r.registry.Reset()
+	r.IsRelation.Reset()
+	r.IsPointer.Reset()
+}
+
+// registerComponent registers a components and assigns an ID for it.
+func (r *componentRegistry) registerComponent(tp reflect.Type, totalBits int) uint8 {
+	newID := r.registry.registerComponent(tp, totalBits)
+	if r.isRelation(tp) {
+		r.IsRelation.Set(id(newID), true)
+	}
+	if r.isPointer(tp) {
+		r.IsPointer.Set(id(newID), true)
+	}
+	return newID
+}
+
+func (r *componentRegistry) unregisterLastComponent() {
+	newID := uint8(len(r.Components) - 1)
+	r.registry.unregisterLastComponent()
+	r.IsRelation.Set(id(newID), false)
+	r.IsPointer.Set(id(newID), false)
+}
+
+// isRelation determines whether a type is a relation component.
 func (r *componentRegistry) isRelation(tp reflect.Type) bool {
 	if tp.Kind() != reflect.Struct || tp.NumField() == 0 {
 		return false
@@ -79,3 +139,20 @@ func (r *componentRegistry) isRelation(tp reflect.Type) bool {
 	field := tp.Field(0)
 	return field.Type == relationType && field.Name == relationType.Name()
 }
+
+// isPointerRecursive determines whether a type contains pointers that need proper garbage collection, or is a pointer itself.
+func (r *componentRegistry) isPointer(tp reflect.Type) bool {
+	switch tp.Kind() {
+	case reflect.Pointer, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
+		return true
+	case reflect.Struct:
+		for i := 0; i < tp.NumField(); i++ {
+			if r.isPointer(tp.Field(i).Type) {
+				return true
+			}
+		}
+		return false
+	default:
+		return false
+	}
+}
diff --git a/ecs/resources.go b/ecs/resources.go
index 21a7f80..5e01b5b 100644
--- a/ecs/resources.go
+++ b/ecs/resources.go
@@ -10,13 +10,13 @@ import (
 // Access it using [World.Resources].
 type Resources struct {
 	resources []any
-	registry  componentRegistry
+	registry  registry
 }
 
 // newResources creates a new Resources manager.
 func newResources() Resources {
 	return Resources{
-		registry:  newComponentRegistry(),
+		registry:  newRegistry(),
 		resources: make([]any, MaskTotalBits),
 	}
 }
diff --git a/ecs/world.go b/ecs/world.go
index 9f6ff93..9f45b0b 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -318,7 +318,7 @@ func (w *World) Add(entity Entity, comps ...ID) {
 	w.Exchange(entity, comps, nil)
 }
 
-// Add adds components to an [Entity].
+// AddFn adds components to an [Entity].
 //
 // The callback fn is called before the world's listener is notified.
 // Use this to configure the entity's components so that listener subscribers
@@ -411,7 +411,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	w.exchange(entity, add, rem, ID{}, false, Entity{}, nil)
 }
 
-// Exchange adds and removes components in one pass.
+// ExchangeFn adds and removes components in one pass.
 // This is more efficient than subsequent use of [World.Add] and [World.Remove].
 //
 // The callback fn is called before the world's listener is notified.
diff --git a/ecs/world_internal.go b/ecs/world_internal.go
index e94ad29..630f272 100644
--- a/ecs/world_internal.go
+++ b/ecs/world_internal.go
@@ -1001,7 +1001,7 @@ func (w *World) createArchetypeNode(mask Mask, relation ID, hasRelation bool) *a
 
 	types := mask.toTypes(&w.registry)
 
-	w.nodeData.Add(nodeData{})
+	w.nodeData.Add(newNodeData(&w.registry.IsPointer))
 	w.nodes.Add(newArchNode(mask, w.nodeData.Get(w.nodeData.Len()-1), relation, hasRelation, capInc, types))
 	nd := w.nodes.Get(w.nodes.Len() - 1)
 	w.relationNodes = append(w.relationNodes, nd)


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 92993ee..bd70940 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -13,7 +13,7 @@ func TestArchetype(t *testing.T) {
 		{ID: id(1), Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 32, comps)
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 32, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, false, 16, Entity{})
@@ -61,7 +61,7 @@ func TestNewArchetype(t *testing.T) {
 		{ID: id(1), Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 32, comps)
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 32, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, true, 16, Entity{})
@@ -77,7 +77,7 @@ func TestNewArchetype(t *testing.T) {
 		{ID: id(0), Type: reflect.TypeOf(Position{})},
 	}
 	assert.PanicsWithValue(t, "component arguments must be sorted by ID", func() {
-		node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 32, comps)
+		node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 32, comps)
 		arch := archetype{}
 		data := archetypeData{}
 		arch.Init(&node, &data, 0, true, 16, Entity{})
@@ -90,7 +90,7 @@ func TestArchetypeExtend(t *testing.T) {
 		{ID: id(1), Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 8, comps)
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 8, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, true, 16, Entity{})
@@ -116,7 +116,7 @@ func TestArchetypeExtendLayouts(t *testing.T) {
 	}
 	entity := newEntity(1)
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 8, comps[:2])
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 8, comps[:2])
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, true, 16, Entity{})
@@ -128,7 +128,7 @@ func TestArchetypeExtendLayouts(t *testing.T) {
 	node.ExtendArchetypeLayouts(32)
 	assert.Equal(t, len(arch.layouts), 32)
 
-	node = newArchNode(All(id(0), id(1)), &nodeData{}, id(2), true, 8, comps)
+	node = newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, id(2), true, 8, comps)
 	node.CreateArchetype(16, entity)
 	arch2, ok := node.GetArchetype(entity)
 
@@ -145,7 +145,7 @@ func TestArchetypeAlloc(t *testing.T) {
 		{ID: id(0), Type: reflect.TypeOf(Position{})},
 		{ID: id(1), Type: reflect.TypeOf(rotation{})},
 	}
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 8, comps)
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 8, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, true, 16, Entity{})
@@ -171,7 +171,7 @@ func TestArchetypeAddGetSet(t *testing.T) {
 		{ID: id(1), Type: reflect.TypeOf(label{})},
 	}
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 1, comps)
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 1, comps)
 	a := archetype{}
 	data := archetypeData{}
 	a.Init(&node, &data, 0, true, 16, Entity{})
@@ -212,7 +212,7 @@ func TestArchetypeReset(t *testing.T) {
 		{ID: id(1), Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 32, comps)
+	node := newArchNode(All(id(0), id(1)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 32, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, false, 16, Entity{})
@@ -248,10 +248,13 @@ func TestArchetypeReset(t *testing.T) {
 func TestArchetypeZero(t *testing.T) {
 	comps := []componentType{
 		{ID: id(0), Type: reflect.TypeOf(Position{})},
-		{ID: id(1), Type: reflect.TypeOf(rotation{})},
+		{ID: id(1), Type: reflect.TypeOf(PointerComp{})},
+		{ID: id(2), Type: reflect.TypeOf(label{})},
 	}
 
-	node := newArchNode(All(id(0), id(1)), &nodeData{}, ID{}, false, 32, comps)
+	isPointer := All(id(1))
+
+	node := newArchNode(All(id(0), id(1), id(2)), &nodeData{compIsPointer: &isPointer}, ID{}, false, 32, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, false, 16, Entity{})
@@ -262,20 +265,37 @@ func TestArchetypeZero(t *testing.T) {
 	assert.Equal(t, Position{0, 0}, *(*Position)(arch.Get(0, id(0))))
 	assert.Equal(t, Position{0, 0}, *(*Position)(arch.Get(1, id(0))))
 
+	assert.Equal(t, PointerComp{nil, 0}, *(*PointerComp)(arch.Get(0, id(1))))
+	assert.Equal(t, PointerComp{nil, 0}, *(*PointerComp)(arch.Get(1, id(1))))
+
 	pos := (*Position)(arch.Get(0, id(0)))
 	pos.X = 100
 	pos = (*Position)(arch.Get(1, id(0)))
 	pos.X = 100
 
+	ptr := (*PointerComp)(arch.Get(0, id(1)))
+	ptr.Value = 99
+	ptr = (*PointerComp)(arch.Get(1, id(1)))
+	ptr.Value = 99
+
 	assert.Equal(t, Position{100, 0}, *(*Position)(arch.Get(0, id(0))))
 	assert.Equal(t, Position{100, 0}, *(*Position)(arch.Get(1, id(0))))
 
+	assert.Equal(t, PointerComp{nil, 99}, *(*PointerComp)(arch.Get(0, id(1))))
+	assert.Equal(t, PointerComp{nil, 99}, *(*PointerComp)(arch.Get(1, id(1))))
+
 	arch.Remove(0)
 	arch.Remove(0)
 	arch.Alloc(newEntity(0))
 	arch.Alloc(newEntity(1))
-	assert.Equal(t, Position{0, 0}, *(*Position)(arch.Get(0, id(0))))
-	assert.Equal(t, Position{0, 0}, *(*Position)(arch.Get(1, id(0))))
+
+	// Don't zero, as there are no pointers involved
+	assert.Equal(t, Position{100, 0}, *(*Position)(arch.Get(0, id(0))))
+	assert.Equal(t, Position{100, 0}, *(*Position)(arch.Get(1, id(0))))
+
+	// These should be zeroed
+	assert.Equal(t, PointerComp{nil, 0}, *(*PointerComp)(arch.Get(0, id(1))))
+	assert.Equal(t, PointerComp{nil, 0}, *(*PointerComp)(arch.Get(1, id(1))))
 }
 
 func BenchmarkIterArchetype_1000(b *testing.B) {
@@ -284,7 +304,7 @@ func BenchmarkIterArchetype_1000(b *testing.B) {
 		{ID: id(0), Type: reflect.TypeOf(testStruct0{})},
 	}
 
-	node := newArchNode(All(id(0)), &nodeData{}, ID{}, false, 32, comps)
+	node := newArchNode(All(id(0)), &nodeData{compIsPointer: &Mask{}}, ID{}, false, 32, comps)
 	arch := archetype{}
 	data := archetypeData{}
 	arch.Init(&node, &data, 0, true, 16, Entity{})
diff --git a/ecs/registry_test.go b/ecs/registry_test.go
index 45ba0ab..65ca208 100644
--- a/ecs/registry_test.go
+++ b/ecs/registry_test.go
@@ -81,3 +81,115 @@ func TestRegistryRelations(t *testing.T) {
 	assert.False(t, registry.IsRelation.Get(id(id3)))
 	assert.False(t, registry.IsRelation.Get(id(id4)))
 }
+
+func TestRegistryIsPointer(t *testing.T) {
+	registry := newComponentRegistry()
+
+	structComp := reflect.TypeOf(Position{})
+	ptrComp := reflect.TypeOf(&Position{})
+	withPtrComp := reflect.TypeOf(PointerType{})
+	withSliceComp := reflect.TypeOf(SliceType{})
+
+	assert.False(t, registry.isPointer(structComp))
+	assert.True(t, registry.isPointer(ptrComp))
+	assert.True(t, registry.isPointer(withPtrComp))
+	assert.True(t, registry.isPointer(withSliceComp))
+
+	id1, _ := registry.ComponentID(structComp)
+	id2, _ := registry.ComponentID(ptrComp)
+	id3, _ := registry.ComponentID(withPtrComp)
+	id4, _ := registry.ComponentID(withSliceComp)
+
+	assert.False(t, registry.IsPointer.Get(id(id1)))
+	assert.True(t, registry.IsPointer.Get(id(id2)))
+	assert.True(t, registry.IsPointer.Get(id(id3)))
+	assert.True(t, registry.IsPointer.Get(id(id4)))
+}
+
+func TestRegistryReset(t *testing.T) {
+	registry := newComponentRegistry()
+
+	structComp := reflect.TypeOf(Position{})
+	ptrComp := reflect.TypeOf(&Position{})
+	relCompTp := reflect.TypeOf(relationComp{})
+
+	id1, _ := registry.ComponentID(structComp)
+	id2, _ := registry.ComponentID(ptrComp)
+	id3, _ := registry.ComponentID(relCompTp)
+
+	assert.False(t, registry.IsRelation.Get(id(id1)))
+	assert.False(t, registry.IsRelation.Get(id(id2)))
+	assert.True(t, registry.IsRelation.Get(id(id3)))
+
+	assert.False(t, registry.IsPointer.Get(id(id1)))
+	assert.True(t, registry.IsPointer.Get(id(id2)))
+	assert.False(t, registry.IsPointer.Get(id(id3)))
+
+	registry.Reset()
+
+	assert.Equal(t, 0, len(registry.Components))
+	assert.Equal(t, make([]reflect.Type, MaskTotalBits), registry.Types)
+	assert.Equal(t, 0, len(registry.IDs))
+
+	assert.True(t, registry.Used.IsZero())
+	assert.True(t, registry.IsRelation.IsZero())
+	assert.True(t, registry.IsPointer.IsZero())
+}
+
+func BenchmarkComponentRegistryIsRelation(b *testing.B) {
+	b.StopTimer()
+
+	reg := newComponentRegistry()
+	tp := reflect.TypeOf((*Position)(nil)).Elem()
+	_ = reg.registerComponent(tp, MaskTotalBits)
+
+	isRel := false
+
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		isRel = reg.isRelation(tp)
+	}
+	b.StopTimer()
+
+	assert.False(b, isRel)
+}
+
+func BenchmarkComponentRegistryIsPointer2Fields(b *testing.B) {
+	b.StopTimer()
+
+	template := struct{ A, B int }{}
+
+	reg := newComponentRegistry()
+	tp := reflect.TypeOf(template)
+	_ = reg.registerComponent(tp, MaskTotalBits)
+
+	isPtr := false
+
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		isPtr = reg.isPointer(tp)
+	}
+	b.StopTimer()
+
+	assert.False(b, isPtr)
+}
+
+func BenchmarkComponentRegistryIsPointer5Fields(b *testing.B) {
+	b.StopTimer()
+
+	template := struct{ A, B, C, D, E int }{}
+
+	reg := newComponentRegistry()
+	tp := reflect.TypeOf(template)
+	_ = reg.registerComponent(tp, MaskTotalBits)
+
+	isPtr := false
+
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		isPtr = reg.isPointer(tp)
+	}
+	b.StopTimer()
+
+	assert.False(b, isPtr)
+}
diff --git a/ecs/types_test.go b/ecs/types_test.go
index 53268fa..305fea0 100644
--- a/ecs/types_test.go
+++ b/ecs/types_test.go
@@ -36,13 +36,18 @@ type ChildOf struct {
 	Relation
 }
 type PointerComp struct {
-	Ptr *PointerType
+	Ptr   *PointerType
+	Value int
 }
 
 type PointerType struct {
 	Pos *Position
 }
 
+type SliceType struct {
+	Slice []int
+}
+
 type testStruct0 struct{ Val int32 }
 type testStruct1 struct{ val int32 }
 type testStruct2 struct{ val int32 }
diff --git a/ecs/world_benchmark_test.go b/ecs/world_benchmark_test.go
index a1a3642..1459e22 100644
--- a/ecs/world_benchmark_test.go
+++ b/ecs/world_benchmark_test.go
@@ -5,8 +5,20 @@ import (
 
 	"github.com/mlange-42/arche/ecs/event"
 	"github.com/mlange-42/arche/ecs/stats"
+	"github.com/stretchr/testify/assert"
 )
 
+func BenchmarkNewWorld(b *testing.B) {
+	var world World
+
+	for i := 0; i < b.N; i++ {
+		world = NewWorld()
+	}
+
+	b.StopTimer()
+	assert.False(b, world.Alive(Entity{}))
+}
+
 func BenchmarkEntityAlive_1000(b *testing.B) {
 	b.StopTimer()
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 902cde5..8f10523 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -2039,7 +2039,7 @@ func TestWorldPointerStressTestAssign(t *testing.T) {
 			w.Assign(e,
 				Component{
 					ID:   id,
-					Comp: &PointerComp{&PointerType{&Position{X: int(e.id), Y: 2}}},
+					Comp: &PointerComp{&PointerType{&Position{X: int(e.id), Y: 2}}, 1},
 				},
 			)
 		}