Repository: mlange-42/arche
Pull request #217 (2023-04-13 22:30:51)

Unsafe component access with World.GetUnsafe/HasUnsafe
Mitigate performance degradation introduced by #216:

* Adds methods `World.GetUnsafe` and `World.HasUnsafe` as optimized variants for known static entities
* Adds method `MapX.GetUnsafe` to all generic mappers, as equivalent to previous point
* Adds methods `Map.GetUnsafe` and `Map.HasUnsafe` to generic `Map`, as equivalent to previous points


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 389f016..f0d4345 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@
 ### Features
 
 * Adds method `World.ComponentType(ID)` to get the `reflect.Type` for component IDs (#215)
+* Adds methods `World.GetUnsafe` and `World.HasUnsafe` as optimized variants for known static entities (#217)
+* Adds method `MapX.GetUnsafe` to all generic mappers, as equivalent to previous point (#217)
+* Adds methods `Map.GetUnsafe` and `Map.HasUnsafe` to generic `Map`, as equivalent to previous points (#217)
 
 ### Bugfixes
 
diff --git a/ecs/world.go b/ecs/world.go
index 509a8f0..ec53ac3 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -325,6 +325,7 @@ func (w *World) Alive(entity Entity) bool {
 //
 // Panics when called for a removed (and potentially recycled) entity.
 //
+// See [World.GetUnsafe] for an optimized version for static entities.
 // See also [github.com/mlange-42/arche/generic.Map.Get] for a generic variant.
 func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 	if !w.entityPool.Alive(entity) {
@@ -334,10 +335,26 @@ func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 	return index.arch.Get(index.index, comp)
 }
 
+// GetUnsafe returns a pointer to the given component of an [Entity].
+// Returns nil if the entity has no such component.
+//
+// GetUnsafe is an optimized version of [World.Get],
+// for cases where entities are static or checked with [World.Alive] in user code.
+// It can also be used after getting another component of the same entity with [World.Get].
+//
+// Panics when called for a removed entity, but not for a recycled entity.
+//
+// See also [github.com/mlange-42/arche/generic.Map.Get] for a generic variant.
+func (w *World) GetUnsafe(entity Entity, comp ID) unsafe.Pointer {
+	index := &w.entities[entity.id]
+	return index.arch.Get(index.index, comp)
+}
+
 // Has returns whether an [Entity] has a given component.
 //
 // Panics when called for a removed (and potentially recycled) entity.
 //
+// See [World.HasUnsafe] for an optimized version for static entities.
 // See also [github.com/mlange-42/arche/generic.Map.Has] for a generic variant.
 func (w *World) Has(entity Entity, comp ID) bool {
 	if !w.entityPool.Alive(entity) {
@@ -346,6 +363,18 @@ func (w *World) Has(entity Entity, comp ID) bool {
 	return w.entities[entity.id].arch.HasComponent(comp)
 }
 
+// HasUnsafe returns whether an [Entity] has a given component.
+//
+// HasUnsafe is an optimized version of [World.Has],
+// for cases where entities are static or checked with [World.Alive] in user code.
+//
+// Panics when called for a removed entity, but not for a recycled entity.
+//
+// See also [github.com/mlange-42/arche/generic.Map.Has] for a generic variant.
+func (w *World) HasUnsafe(entity Entity, comp ID) bool {
+	return w.entities[entity.id].arch.HasComponent(comp)
+}
+
 // Add adds components to an [Entity].
 //
 // Panics:
diff --git a/generic/generate/main.go b/generic/generate/main.go
index a31feed..e5f46e3 100644
--- a/generic/generate/main.go
+++ b/generic/generate/main.go
@@ -15,20 +15,21 @@ var numbers = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8"}
 var numberStr = []string{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight"}
 
 type query struct {
-	Index       int
-	NumberStr   string
-	Types       string
-	TypesFull   string
-	TypesReturn string
-	Variables   string
-	ReturnAll   string
-	Include     string
-	Components  string
-	Arguments   string
-	IDTypes     string
-	IDAssign    string
-	IDAssign2   string
-	IDList      string
+	Index         int
+	NumberStr     string
+	Types         string
+	TypesFull     string
+	TypesReturn   string
+	Variables     string
+	ReturnAll     string
+	ReturnAllSafe string
+	Include       string
+	Components    string
+	Arguments     string
+	IDTypes       string
+	IDAssign      string
+	IDAssign2     string
+	IDList        string
 }
 
 func main() {
@@ -61,6 +62,7 @@ func generateMaps() {
 		returnTypes := ""
 		fullTypes := ""
 		returnAll := ""
+		returnAllSafe := ""
 		components := ""
 		arguments := ""
 		idTypes := ""
@@ -79,31 +81,38 @@ func generateMaps() {
 		}
 
 		for j := 0; j < i; j++ {
-			returnAll += fmt.Sprintf("(*%s)(m.world.Get(entity, m.id%d))", typeLetters[j], j)
+			returnAll += fmt.Sprintf("(*%s)(m.world.GetUnsafe(entity, m.id%d))", typeLetters[j], j)
+			if j == 0 {
+				returnAllSafe += fmt.Sprintf("(*%s)(m.world.Get(entity, m.id%d))", typeLetters[j], j)
+			} else {
+				returnAllSafe += fmt.Sprintf("(*%s)(m.world.GetUnsafe(entity, m.id%d))", typeLetters[j], j)
+			}
 			arguments += fmt.Sprintf("%s *%s", strings.ToLower(typeLetters[j]), typeLetters[j])
 			idAssign += fmt.Sprintf("	id%d: ecs.ComponentID[%s](w),\n", j, typeLetters[j])
 			idAssign2 += fmt.Sprintf("	id%d: m.id%d,\n", j, j)
 			if j < i-1 {
 				returnAll += ",\n"
+				returnAllSafe += ",\n"
 				arguments += ", "
 			}
 			components += fmt.Sprintf("ecs.Component{ID: m.id%d, Comp: %s},\n", j, strings.ToLower(typeLetters[j]))
 		}
 
 		data := query{
-			Index:       i,
-			NumberStr:   numberStr[i],
-			Types:       types,
-			TypesReturn: returnTypes,
-			TypesFull:   fullTypes,
-			ReturnAll:   returnAll,
-			Variables:   variables,
-			Components:  components,
-			Arguments:   arguments,
-			IDTypes:     idTypes,
-			IDAssign:    idAssign,
-			IDAssign2:   idAssign2,
-			IDList:      idList,
+			Index:         i,
+			NumberStr:     numberStr[i],
+			Types:         types,
+			TypesReturn:   returnTypes,
+			TypesFull:     fullTypes,
+			ReturnAll:     returnAll,
+			ReturnAllSafe: returnAllSafe,
+			Variables:     variables,
+			Components:    components,
+			Arguments:     arguments,
+			IDTypes:       idTypes,
+			IDAssign:      idAssign,
+			IDAssign2:     idAssign2,
+			IDList:        idList,
 		}
 		err = maps.Execute(&text, data)
 		if err != nil {
diff --git a/generic/generate/map.go.txt b/generic/generate/map.go.txt
index 93359a6..f1b7f2d 100644
--- a/generic/generate/map.go.txt
+++ b/generic/generate/map.go.txt
@@ -31,8 +31,19 @@ func NewMap{{ .Index }}{{ .TypesFull }}(w *ecs.World) Map{{ .Index }}{{ .Types }
 
 // Get all the Map{{ .Index }}'s components for the given entity.
 //
+// See [Map{{ .Index }}.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map{{ .Index }}{{ .Types }}) Get(entity ecs.Entity) ({{ .TypesReturn }}) {
+	return {{ .ReturnAllSafe }}
+}
+
+// GetUnsafe all the Map{{ .Index }}'s components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map{{ .Index }}.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map{{ .Index }}{{ .Types }}) GetUnsafe(entity ecs.Entity) ({{ .TypesReturn }}) {
 	return {{ .ReturnAll }}
 }
 
diff --git a/generic/map.go b/generic/map.go
index e3d5c90..3edc7b5 100644
--- a/generic/map.go
+++ b/generic/map.go
@@ -29,18 +29,40 @@ func (g *Map[T]) ID() ecs.ID {
 
 // Get gets the component for the given entity.
 //
+// See [Map.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (g *Map[T]) Get(entity ecs.Entity) *T {
 	return (*T)(g.world.Get(entity, g.id))
 }
 
+// GetUnsafe gets the component for the given entity.
+//
+// GetUnsafe is an optimized version of [Map.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (g *Map[T]) GetUnsafe(entity ecs.Entity) *T {
+	return (*T)(g.world.GetUnsafe(entity, g.id))
+}
+
 // Has returns whether the entity has the component.
 //
+// See [Map.HasUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Has].
 func (g *Map[T]) Has(entity ecs.Entity) bool {
 	return g.world.Has(entity, g.id)
 }
 
+// HasUnsafe returns whether the entity has the component.
+//
+// HasUnsafe is an optimized version of [Map.Has],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.HasUnsafe].
+func (g *Map[T]) HasUnsafe(entity ecs.Entity) bool {
+	return g.world.HasUnsafe(entity, g.id)
+}
+
 // Set overwrites the component for the given entity.
 //
 // Panics if the entity does not have a component of that type.
diff --git a/generic/map_generated.go b/generic/map_generated.go
index 0d8d4aa..c55b822 100644
--- a/generic/map_generated.go
+++ b/generic/map_generated.go
@@ -38,11 +38,22 @@ func NewMap1[A any](w *ecs.World) Map1[A] {
 
 // Get all the Map1's components for the given entity.
 //
+// See [Map1.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map1[A]) Get(entity ecs.Entity) *A {
 	return (*A)(m.world.Get(entity, m.id0))
 }
 
+// GetUnsafe all the Map1's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map1.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map1[A]) GetUnsafe(entity ecs.Entity) *A {
+	return (*A)(m.world.GetUnsafe(entity, m.id0))
+}
+
 // NewEntity creates a new [ecs.Entity] with the Map1's components.
 //
 // See also [ecs.World.NewEntity].
@@ -176,10 +187,22 @@ func NewMap2[A any, B any](w *ecs.World) Map2[A, B] {
 
 // Get all the Map2's components for the given entity.
 //
+// See [Map2.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map2[A, B]) Get(entity ecs.Entity) (*A, *B) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1))
+		(*B)(m.world.GetUnsafe(entity, m.id1))
+}
+
+// GetUnsafe all the Map2's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map2.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map2[A, B]) GetUnsafe(entity ecs.Entity) (*A, *B) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map2's components.
@@ -325,11 +348,24 @@ func NewMap3[A any, B any, C any](w *ecs.World) Map3[A, B, C] {
 
 // Get all the Map3's components for the given entity.
 //
+// See [Map3.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map3[A, B, C]) Get(entity ecs.Entity) (*A, *B, *C) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1)),
-		(*C)(m.world.Get(entity, m.id2))
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2))
+}
+
+// GetUnsafe all the Map3's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map3.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map3[A, B, C]) GetUnsafe(entity ecs.Entity) (*A, *B, *C) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map3's components.
@@ -483,12 +519,26 @@ func NewMap4[A any, B any, C any, D any](w *ecs.World) Map4[A, B, C, D] {
 
 // Get all the Map4's components for the given entity.
 //
+// See [Map4.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map4[A, B, C, D]) Get(entity ecs.Entity) (*A, *B, *C, *D) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1)),
-		(*C)(m.world.Get(entity, m.id2)),
-		(*D)(m.world.Get(entity, m.id3))
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3))
+}
+
+// GetUnsafe all the Map4's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map4.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map4[A, B, C, D]) GetUnsafe(entity ecs.Entity) (*A, *B, *C, *D) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map4's components.
@@ -650,13 +700,28 @@ func NewMap5[A any, B any, C any, D any, E any](w *ecs.World) Map5[A, B, C, D, E
 
 // Get all the Map5's components for the given entity.
 //
+// See [Map5.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map5[A, B, C, D, E]) Get(entity ecs.Entity) (*A, *B, *C, *D, *E) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1)),
-		(*C)(m.world.Get(entity, m.id2)),
-		(*D)(m.world.Get(entity, m.id3)),
-		(*E)(m.world.Get(entity, m.id4))
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4))
+}
+
+// GetUnsafe all the Map5's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map5.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map5[A, B, C, D, E]) GetUnsafe(entity ecs.Entity) (*A, *B, *C, *D, *E) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map5's components.
@@ -826,14 +891,30 @@ func NewMap6[A any, B any, C any, D any, E any, F any](w *ecs.World) Map6[A, B,
 
 // Get all the Map6's components for the given entity.
 //
+// See [Map6.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map6[A, B, C, D, E, F]) Get(entity ecs.Entity) (*A, *B, *C, *D, *E, *F) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1)),
-		(*C)(m.world.Get(entity, m.id2)),
-		(*D)(m.world.Get(entity, m.id3)),
-		(*E)(m.world.Get(entity, m.id4)),
-		(*F)(m.world.Get(entity, m.id5))
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4)),
+		(*F)(m.world.GetUnsafe(entity, m.id5))
+}
+
+// GetUnsafe all the Map6's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map6.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map6[A, B, C, D, E, F]) GetUnsafe(entity ecs.Entity) (*A, *B, *C, *D, *E, *F) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4)),
+		(*F)(m.world.GetUnsafe(entity, m.id5))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map6's components.
@@ -1011,15 +1092,32 @@ func NewMap7[A any, B any, C any, D any, E any, F any, G any](w *ecs.World) Map7
 
 // Get all the Map7's components for the given entity.
 //
+// See [Map7.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map7[A, B, C, D, E, F, G]) Get(entity ecs.Entity) (*A, *B, *C, *D, *E, *F, *G) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1)),
-		(*C)(m.world.Get(entity, m.id2)),
-		(*D)(m.world.Get(entity, m.id3)),
-		(*E)(m.world.Get(entity, m.id4)),
-		(*F)(m.world.Get(entity, m.id5)),
-		(*G)(m.world.Get(entity, m.id6))
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4)),
+		(*F)(m.world.GetUnsafe(entity, m.id5)),
+		(*G)(m.world.GetUnsafe(entity, m.id6))
+}
+
+// GetUnsafe all the Map7's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map7.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map7[A, B, C, D, E, F, G]) GetUnsafe(entity ecs.Entity) (*A, *B, *C, *D, *E, *F, *G) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4)),
+		(*F)(m.world.GetUnsafe(entity, m.id5)),
+		(*G)(m.world.GetUnsafe(entity, m.id6))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map7's components.
@@ -1205,16 +1303,34 @@ func NewMap8[A any, B any, C any, D any, E any, F any, G any, H any](w *ecs.Worl
 
 // Get all the Map8's components for the given entity.
 //
+// See [Map8.GetUnsafe] for an optimized version for static entities.
 // See also [ecs.World.Get].
 func (m *Map8[A, B, C, D, E, F, G, H]) Get(entity ecs.Entity) (*A, *B, *C, *D, *E, *F, *G, *H) {
 	return (*A)(m.world.Get(entity, m.id0)),
-		(*B)(m.world.Get(entity, m.id1)),
-		(*C)(m.world.Get(entity, m.id2)),
-		(*D)(m.world.Get(entity, m.id3)),
-		(*E)(m.world.Get(entity, m.id4)),
-		(*F)(m.world.Get(entity, m.id5)),
-		(*G)(m.world.Get(entity, m.id6)),
-		(*H)(m.world.Get(entity, m.id7))
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4)),
+		(*F)(m.world.GetUnsafe(entity, m.id5)),
+		(*G)(m.world.GetUnsafe(entity, m.id6)),
+		(*H)(m.world.GetUnsafe(entity, m.id7))
+}
+
+// GetUnsafe all the Map8's components for the given entity.
+//
+// GetUnsafe is an optimized version of [Map8.Get],
+// for cases where entities are static or checked with [ecs.World.Alive] in user code.
+//
+// See also [ecs.World.GetUnsafe].
+func (m *Map8[A, B, C, D, E, F, G, H]) GetUnsafe(entity ecs.Entity) (*A, *B, *C, *D, *E, *F, *G, *H) {
+	return (*A)(m.world.GetUnsafe(entity, m.id0)),
+		(*B)(m.world.GetUnsafe(entity, m.id1)),
+		(*C)(m.world.GetUnsafe(entity, m.id2)),
+		(*D)(m.world.GetUnsafe(entity, m.id3)),
+		(*E)(m.world.GetUnsafe(entity, m.id4)),
+		(*F)(m.world.GetUnsafe(entity, m.id5)),
+		(*G)(m.world.GetUnsafe(entity, m.id6)),
+		(*H)(m.world.GetUnsafe(entity, m.id7))
 }
 
 // NewEntity creates a new [ecs.Entity] with the Map8's components.


Test patch:
diff --git a/benchmark/arche/iterate/arche_test.go b/benchmark/arche/iterate/arche_test.go
index 14bd399..8696cfb 100644
--- a/benchmark/arche/iterate/arche_test.go
+++ b/benchmark/arche/iterate/arche_test.go
@@ -398,50 +398,6 @@ func runArcheQuery1Of1kArchCached(b *testing.B, count int) {
 	}
 }
 
-func runArcheWorldGet(b *testing.B, count int) {
-	b.StopTimer()
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[c.Position](&world)
-	rotID := ecs.ComponentID[c.Rotation](&world)
-
-	entities := make([]ecs.Entity, count)
-	for i := 0; i < count; i++ {
-		entities[i] = world.NewEntity(posID, rotID)
-	}
-	b.StartTimer()
-
-	for i := 0; i < b.N; i++ {
-		for _, e := range entities {
-			pos := (*c.Position)(world.Get(e, posID))
-			pos.X = 1
-		}
-	}
-}
-
-func runArcheWorldGetGeneric(b *testing.B, count int) {
-	b.StopTimer()
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[c.Position](&world)
-	rotID := ecs.ComponentID[c.Rotation](&world)
-
-	get := generic.NewMap[c.Position](&world)
-
-	entities := make([]ecs.Entity, count)
-	for i := 0; i < count; i++ {
-		entities[i] = world.NewEntity(posID, rotID)
-	}
-	b.StartTimer()
-
-	for i := 0; i < b.N; i++ {
-		for _, e := range entities {
-			pos := get.Get(e)
-			pos.X = 1
-		}
-	}
-}
-
 func BenchmarkArcheIter_1_000(b *testing.B) {
 	runArcheIter(b, 1000)
 }
@@ -550,30 +506,6 @@ func BenchmarkArcheIterQueryGeneric_5C_100_000(b *testing.B) {
 	runArcheQueryGeneric5C(b, 100000)
 }
 
-func BenchmarkArcheIterWorldID_1_000(b *testing.B) {
-	runArcheWorldGet(b, 1000)
-}
-
-func BenchmarkArcheIterWorldID_10_000(b *testing.B) {
-	runArcheWorldGet(b, 10000)
-}
-
-func BenchmarkArcheIterWorldID_100_000(b *testing.B) {
-	runArcheWorldGet(b, 100000)
-}
-
-func BenchmarkArcheIterWorldGeneric_1_000(b *testing.B) {
-	runArcheWorldGetGeneric(b, 1000)
-}
-
-func BenchmarkArcheIterWorldGeneric_10_000(b *testing.B) {
-	runArcheWorldGetGeneric(b, 10000)
-}
-
-func BenchmarkArcheIterWorldGeneric_100_000(b *testing.B) {
-	runArcheWorldGetGeneric(b, 100000)
-}
-
 func BenchmarkArcheIter1kArchID_1_000(b *testing.B) {
 	runArcheQuery1kArch(b, 1000)
 }
diff --git a/benchmark/arche/iterate_world/arche_test.go b/benchmark/arche/iterate_world/arche_test.go
new file mode 100644
index 0000000..cc10bca
--- /dev/null
+++ b/benchmark/arche/iterate_world/arche_test.go
@@ -0,0 +1,145 @@
+package iterate
+
+import (
+	"testing"
+
+	c "github.com/mlange-42/arche/benchmark/arche/common"
+	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/generic"
+)
+
+func runArcheWorldGet(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[c.Position](&world)
+	rotID := ecs.ComponentID[c.Rotation](&world)
+
+	entities := make([]ecs.Entity, count)
+	for i := 0; i < count; i++ {
+		entities[i] = world.NewEntity(posID, rotID)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		for _, e := range entities {
+			pos := (*c.Position)(world.Get(e, posID))
+			pos.X = 1
+		}
+	}
+}
+
+func runArcheWorldGetGeneric(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[c.Position](&world)
+	rotID := ecs.ComponentID[c.Rotation](&world)
+
+	get := generic.NewMap1[c.Position](&world)
+
+	entities := make([]ecs.Entity, count)
+	for i := 0; i < count; i++ {
+		entities[i] = world.NewEntity(posID, rotID)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		for _, e := range entities {
+			pos := get.Get(e)
+			pos.X = 1
+		}
+	}
+}
+
+func runArcheWorldGetUnsafe(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[c.Position](&world)
+	rotID := ecs.ComponentID[c.Rotation](&world)
+
+	entities := make([]ecs.Entity, count)
+	for i := 0; i < count; i++ {
+		entities[i] = world.NewEntity(posID, rotID)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		for _, e := range entities {
+			pos := (*c.Position)(world.GetUnsafe(e, posID))
+			pos.X = 1
+		}
+	}
+}
+
+func runArcheWorldGetGenericUnsafe(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[c.Position](&world)
+	rotID := ecs.ComponentID[c.Rotation](&world)
+
+	get := generic.NewMap1[c.Position](&world)
+
+	entities := make([]ecs.Entity, count)
+	for i := 0; i < count; i++ {
+		entities[i] = world.NewEntity(posID, rotID)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		for _, e := range entities {
+			pos := get.GetUnsafe(e)
+			pos.X = 1
+		}
+	}
+}
+
+func BenchmarkArcheIterWorldID_1_000(b *testing.B) {
+	runArcheWorldGet(b, 1000)
+}
+
+func BenchmarkArcheIterWorldID_10_000(b *testing.B) {
+	runArcheWorldGet(b, 10000)
+}
+
+func BenchmarkArcheIterWorldID_100_000(b *testing.B) {
+	runArcheWorldGet(b, 100000)
+}
+
+func BenchmarkArcheIterWorldGeneric_1_000(b *testing.B) {
+	runArcheWorldGetGeneric(b, 1000)
+}
+
+func BenchmarkArcheIterWorldGeneric_10_000(b *testing.B) {
+	runArcheWorldGetGeneric(b, 10000)
+}
+
+func BenchmarkArcheIterWorldGeneric_100_000(b *testing.B) {
+	runArcheWorldGetGeneric(b, 100000)
+}
+
+func BenchmarkArcheIterWorldIDUnsafe_1_000(b *testing.B) {
+	runArcheWorldGetUnsafe(b, 1000)
+}
+
+func BenchmarkArcheIterWorldIDUnsafe_10_000(b *testing.B) {
+	runArcheWorldGetUnsafe(b, 10000)
+}
+
+func BenchmarkArcheIterWorldIDUnsafe_100_000(b *testing.B) {
+	runArcheWorldGetUnsafe(b, 100000)
+}
+
+func BenchmarkArcheIterWorldGenericUnsafe_1_000(b *testing.B) {
+	runArcheWorldGetGenericUnsafe(b, 1000)
+}
+
+func BenchmarkArcheIterWorldGenericUnsafe_10_000(b *testing.B) {
+	runArcheWorldGetGenericUnsafe(b, 10000)
+}
+
+func BenchmarkArcheIterWorldGenericUnsafe_100_000(b *testing.B) {
+	runArcheWorldGetGenericUnsafe(b, 100000)
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 2d255a6..130e66b 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -312,6 +312,8 @@ func TestWorldGetComponents(t *testing.T) {
 
 	assert.False(t, w.Has(e2, posID))
 	assert.True(t, w.Has(e2, rotID))
+	assert.False(t, w.HasUnsafe(e2, posID))
+	assert.True(t, w.HasUnsafe(e2, rotID))
 
 	pos1 := (*Position)(w.Get(e1, posID))
 	assert.Equal(t, &Position{}, pos1)
@@ -324,6 +326,11 @@ func TestWorldGetComponents(t *testing.T) {
 	assert.Equal(t, &Position{}, pos0)
 	assert.Equal(t, &Position{100, 101}, pos1)
 
+	pos0 = (*Position)(w.GetUnsafe(e0, posID))
+	pos1 = (*Position)(w.GetUnsafe(e1, posID))
+	assert.Equal(t, &Position{}, pos0)
+	assert.Equal(t, &Position{100, 101}, pos1)
+
 	w.RemoveEntity(e0)
 	assert.Panics(t, func() { w.Get(e0, posID) })
 	assert.Panics(t, func() { w.Mask(e0) })
diff --git a/generic/map_generated_test.go b/generic/map_generated_test.go
index a3d0331..bc89c51 100644
--- a/generic/map_generated_test.go
+++ b/generic/map_generated_test.go
@@ -42,10 +42,15 @@ func TestMap1Generated(t *testing.T) {
 	q.Close()
 	q = mut.NewEntitiesWithQuery(2, &testStruct0{})
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	cnt := mut.RemoveEntities(true)
 	assert.Equal(t, 11, cnt)
 	cnt = mut.RemoveEntities(false)
 	assert.Equal(t, 0, cnt)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap2Generated(t *testing.T) {
@@ -92,8 +97,13 @@ func TestMap2Generated(t *testing.T) {
 		&testStruct0{}, &testStruct1{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap3Generated(t *testing.T) {
@@ -146,8 +156,13 @@ func TestMap3Generated(t *testing.T) {
 		&testStruct0{}, &testStruct1{}, &testStruct2{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap4Generated(t *testing.T) {
@@ -200,8 +215,13 @@ func TestMap4Generated(t *testing.T) {
 		&testStruct0{}, &testStruct1{}, &testStruct2{}, &testStruct3{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap5Generated(t *testing.T) {
@@ -261,8 +281,13 @@ func TestMap5Generated(t *testing.T) {
 		&testStruct4{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap6Generated(t *testing.T) {
@@ -322,8 +347,13 @@ func TestMap6Generated(t *testing.T) {
 		&testStruct4{}, &testStruct5{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap7Generated(t *testing.T) {
@@ -383,8 +413,13 @@ func TestMap7Generated(t *testing.T) {
 		&testStruct4{}, &testStruct5{}, &testStruct6{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
 
 func TestMap8Generated(t *testing.T) {
@@ -444,6 +479,11 @@ func TestMap8Generated(t *testing.T) {
 		&testStruct4{}, &testStruct5{}, &testStruct6{}, &testStruct7{},
 	)
 	q.Close()
+
+	mut.GetUnsafe(e)
+
 	mut.RemoveEntities(true)
 	mut.RemoveEntities(false)
+
+	assert.Panics(t, func() { mut.Get(e) })
 }
diff --git a/generic/map_test.go b/generic/map_test.go
index 9fc1192..bdc9e21 100644
--- a/generic/map_test.go
+++ b/generic/map_test.go
@@ -21,6 +21,10 @@ func TestGenericMap(t *testing.T) {
 	_ = get.Get(e0)
 	assert.True(t, has)
 
+	has = get.HasUnsafe(e0)
+	_ = get.GetUnsafe(e0)
+	assert.True(t, has)
+
 	_ = get.Set(e0, &testStruct0{100})
 	str := get.Get(e0)
 
@@ -29,16 +33,31 @@ func TestGenericMap(t *testing.T) {
 	get2 := NewMap[testStruct1](&w)
 	assert.Equal(t, ecs.ComponentID[testStruct1](&w), get2.ID())
 	assert.Panics(t, func() { get2.Set(e0, &testStruct1{}) })
+
+	w.RemoveEntity(e0)
+	_ = w.NewEntity()
+
+	assert.Panics(t, func() { get.Has(e0) })
+	assert.Panics(t, func() { get.Get(e0) })
 }
 
 func ExampleMap() {
+	// Create a world.
 	world := ecs.NewWorld()
 
+	// Spawn some entities using the generic API.
 	spawner := NewMap2[Position, Velocity](&world)
 	entity := spawner.NewEntity()
 
+	// Create a new Map.
 	mapper := NewMap[Position](&world)
+
+	// Get the map's component.
 	pos := mapper.Get(entity)
 	pos.X, pos.Y = 10, 5
+
+	// Get the map's component, optimized for cases when the entity is guaranteed to be still alive.
+	pos = mapper.GetUnsafe(entity)
+	pos.X, pos.Y = 10, 5
 	// Output:
 }