diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 71fa673b..5f3f3509 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -12,54 +12,65 @@ However, all benchmarks run in the CI in the same job and hence on the same mach | Operation | Time | Remark | |----------------------------------|-------------:|------------------------------| | Query.Next | 1.0 ns | | -| Query.Next + 1x Get | 1.6 ns | | -| Query.Next + 2x Get | 2.3 ns | | -| Query.Next + 5x Get | 4.4 ns | | +| Query.Next + 1x Query.Get | 1.6 ns | | +| Query.Next + 2x Query.Get | 2.3 ns | | +| Query.Next + 5x Query.Get | 4.4 ns | | +| Query.Next + Query.Relation | 2.2 ns | | | Query.EntityAt, 1 arch | 12.0 ns | | -| Query.EntityAt, 1 arch | 3.1 ns | registered filter | -| Query.EntityAt, 5 arch | 14.9 ns | | +| Query.EntityAt, 1 arch | 2.8 ns | registered filter | +| Query.EntityAt, 5 arch | 16.1 ns | | | Query.EntityAt, 5 arch | 2.8 ns | registered filter | -| World.Query | 46.9 ns | | -| World.Query | 34.0 ns | registered filter | +| World.Query | 46.0 ns | | +| World.Query | 33.6 ns | registered filter | + +## World access + +| Operation | Time | Remark | +|----------------------------------|-------------:|------------------------------| +| World.Get | 2.0 ns | random, 1000 entities | +| World.Has | 1.2 ns | random, 1000 entities | +| World.Alive | 0.6 ns | random, 1000 entities | +| World.Relations.Get | 3.5 ns | random, 1000 entities | +| World.Relations.GetUnchecked | 0.8 ns | random, 1000 entities | ## Entities | Operation | Time | Remark | |----------------------------------|-------------:|------------------------------| -| World.NewEntity | 16.3 ns | memory already allocated | -| World.NewEntity w/ 1 Comp | 34.6 ns | memory already allocated | -| World.NewEntity w/ 5 Comps | 46.7 ns | memory already allocated | -| World.RemoveEntity | 15.1 ns | | -| World.RemoveEntity w/ 1 Comp | 25.3 ns | | -| World.RemoveEntity w/ 5 Comps | 54.8 ns | | +| World.NewEntity | 16.6 ns | memory already allocated | +| World.NewEntity w/ 1 Comp | 33.6 ns | memory already allocated | +| World.NewEntity w/ 5 Comps | 47.0 ns | memory already allocated | +| World.RemoveEntity | 15.4 ns | | +| World.RemoveEntity w/ 1 Comp | 25.7 ns | | +| World.RemoveEntity w/ 5 Comps | 54.4 ns | | ## Entities, batched | Operation | Time | Remark | |----------------------------------|-------------:|------------------------------| -| Builder.NewBatch | 10.3 ns | 1000, memory already allocated | +| Builder.NewBatch | 9.6 ns | 1000, memory already allocated | | Builder.NewBatch w/ 1 Comp | 9.8 ns | 1000, memory already allocated | -| Builder.NewBatch w/ 5 Comps | 10.6 ns | 1000, memory already allocated | -| Batch.RemoveEntities | 7.0 ns | 1000 | -| Batch.RemoveEntities w/ 1 Comp | 8.1 ns | 1000 | -| Batch.RemoveEntities w/ 5 Comps | 8.1 ns | 1000 | +| Builder.NewBatch w/ 5 Comps | 9.6 ns | 1000, memory already allocated | +| Batch.RemoveEntities | 6.9 ns | 1000 | +| Batch.RemoveEntities w/ 1 Comp | 7.1 ns | 1000 | +| Batch.RemoveEntities w/ 5 Comps | 7.9 ns | 1000 | ## Components | Operation | Time | Remark | |----------------------------------|-------------:|------------------------------| -| World.Add 1 Comp | 47.3 ns | memory already allocated | -| World.Add 5 Comps | 65.0 ns | memory already allocated | -| World.Remove 1 Comp | 58.9 ns | | -| World.Remove 5 Comps | 101.2 ns | | -| World.Exchange 1 Comp | 54.7 ns | memory already allocated | +| World.Add 1 Comp | 50.7 ns | memory already allocated | +| World.Add 5 Comps | 67.6 ns | memory already allocated | +| World.Remove 1 Comp | 59.0 ns | | +| World.Remove 5 Comps | 101.0 ns | | +| World.Exchange 1 Comp | 60.5 ns | memory already allocated | ## Components, batched | Operation | Time | Remark | |----------------------------------|-------------:|------------------------------| -| Batch.Add 1 Comp | 9.3 ns | 1000, memory already allocated | -| Batch.Add 5 Comps | 8.9 ns | 1000, memory already allocated | -| Batch.Remove 1 Comp | 11.3 ns | 1000 | -| Batch.Remove 5 Comps | 17.3 ns | 1000 | -| Batch.Exchange 1 Comp | 10.5 ns | 1000, memory already allocated | +| Batch.Add 1 Comp | 9.0 ns | 1000, memory already allocated | +| Batch.Add 5 Comps | 9.1 ns | 1000, memory already allocated | +| Batch.Remove 1 Comp | 9.8 ns | 1000 | +| Batch.Remove 5 Comps | 14.9 ns | 1000 | +| Batch.Exchange 1 Comp | 10.0 ns | 1000, memory already allocated | diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e861bf..7b5afe65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Documentation -* Adds [BENCHMARKS.md](https://github.com/mlange-42/arche/blob/main/BENCHMARKS.md) for a tabular overview of the runtime cost of typical *Arche* ECS operations (#367) +* Adds [BENCHMARKS.md](https://github.com/mlange-42/arche/blob/main/BENCHMARKS.md) for a tabular overview of the runtime cost of typical *Arche* ECS operations (#367, #372) ### Bugfixes diff --git a/benchmark/table/main.go b/benchmark/table/main.go index 5ecc7960..08a2e5cb 100644 --- a/benchmark/table/main.go +++ b/benchmark/table/main.go @@ -14,6 +14,7 @@ type bench struct { func main() { runBenches("Query", benchesQuery(), toMarkdown) + runBenches("World access", benchesWorld(), toMarkdown) runBenches("Entities", benchesEntities(), toMarkdown) runBenches("Entities, batched", benchesEntitiesBatch(), toMarkdown) runBenches("Components", benchesComponents(), toMarkdown) diff --git a/benchmark/table/query.go b/benchmark/table/query.go index 629aee06..7ebda178 100644 --- a/benchmark/table/query.go +++ b/benchmark/table/query.go @@ -10,9 +10,11 @@ import ( func benchesQuery() []bench { return []bench{ {Name: "Query.Next", Desc: "", F: queryIter_100_000, N: 100_000}, - {Name: "Query.Next + 1x Get", Desc: "", F: queryIterGet_1_100_000, N: 100_000}, - {Name: "Query.Next + 2x Get", Desc: "", F: queryIterGet_2_100_000, N: 100_000}, - {Name: "Query.Next + 5x Get", Desc: "", F: queryIterGet_5_100_000, N: 100_000}, + {Name: "Query.Next + 1x Query.Get", Desc: "", F: queryIterGet_1_100_000, N: 100_000}, + {Name: "Query.Next + 2x Query.Get", Desc: "", F: queryIterGet_2_100_000, N: 100_000}, + {Name: "Query.Next + 5x Query.Get", Desc: "", F: queryIterGet_5_100_000, N: 100_000}, + + {Name: "Query.Next + Query.Relation", Desc: "", F: queryRelation_100_000, N: 100_000}, {Name: "Query.EntityAt, 1 arch", Desc: "", F: querEntityAt_1Arch_1000, N: 1000}, {Name: "Query.EntityAt, 1 arch", Desc: "registered filter", F: querEntityAtRegistered_1Arch_1000, N: 1000}, @@ -127,6 +129,31 @@ func queryIterGet_5_100_000(b *testing.B) { _ = sum } +func queryRelation_100_000(b *testing.B) { + w := ecs.NewWorld() + id1 := ecs.ComponentID[relComp1](&w) + parent := w.NewEntity() + + builder := ecs.NewBuilder(&w, id1).WithRelation(id1) + builder.NewBatch(100_000, parent) + filter := ecs.All(id1) + + var par ecs.Entity + + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + query := w.Query(filter) + b.StartTimer() + for query.Next() { + par = query.Relation(id1) + } + } + b.StopTimer() + sum := par.IsZero() + _ = sum +} + func querEntityAt_1Arch_1000(b *testing.B) { b.StopTimer() w := ecs.NewWorld() diff --git a/benchmark/table/types.go b/benchmark/table/types.go index d5605fe9..581d308a 100644 --- a/benchmark/table/types.go +++ b/benchmark/table/types.go @@ -1,5 +1,7 @@ package main +import "github.com/mlange-42/arche/ecs" + type comp1 struct { V int64 } @@ -19,3 +21,7 @@ type comp4 struct { type comp5 struct { V int64 } + +type relComp1 struct { + ecs.Relation +} diff --git a/benchmark/table/world.go b/benchmark/table/world.go new file mode 100644 index 00000000..272ccc0b --- /dev/null +++ b/benchmark/table/world.go @@ -0,0 +1,152 @@ +package main + +import ( + "math/rand" + "testing" + + "github.com/mlange-42/arche/ecs" +) + +func benchesWorld() []bench { + return []bench{ + {Name: "World.Get", Desc: "random, 1000 entities", F: worldGet_1000, N: 1000}, + {Name: "World.Has", Desc: "random, 1000 entities", F: worldHas_1000, N: 1000}, + {Name: "World.Alive", Desc: "random, 1000 entities", F: worldAlive_1000, N: 1000}, + {Name: "World.Relations.Get", Desc: "random, 1000 entities", F: worldRelation_1000, N: 1000}, + {Name: "World.Relations.GetUnchecked", Desc: "random, 1000 entities", F: worldRelationUnchecked_1000, N: 1000}, + } +} + +func worldGet_1000(b *testing.B) { + b.StopTimer() + + w := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024)) + id1 := ecs.ComponentID[comp1](&w) + + entities := make([]ecs.Entity, 0, 1000) + builder := ecs.NewBuilder(&w, id1) + query := builder.NewBatchQ(1000) + for query.Next() { + entities = append(entities, query.Entity()) + } + rand.Shuffle(len(entities), func(i, j int) { entities[i], entities[j] = entities[j], entities[i] }) + + var comp *comp1 + b.StartTimer() + for i := 0; i < b.N; i++ { + for _, e := range entities { + comp = (*comp1)(w.Get(e, id1)) + } + } + b.StopTimer() + v := comp.V * comp.V + _ = v +} + +func worldHas_1000(b *testing.B) { + b.StopTimer() + + w := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024)) + id1 := ecs.ComponentID[comp1](&w) + + entities := make([]ecs.Entity, 0, 1000) + builder := ecs.NewBuilder(&w, id1) + query := builder.NewBatchQ(1000) + for query.Next() { + entities = append(entities, query.Entity()) + } + rand.Shuffle(len(entities), func(i, j int) { entities[i], entities[j] = entities[j], entities[i] }) + + var has bool + b.StartTimer() + for i := 0; i < b.N; i++ { + for _, e := range entities { + has = w.Has(e, id1) + } + } + b.StopTimer() + v := !has + _ = v +} + +func worldAlive_1000(b *testing.B) { + b.StopTimer() + + w := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024)) + id1 := ecs.ComponentID[comp1](&w) + + entities := make([]ecs.Entity, 0, 1000) + builder := ecs.NewBuilder(&w, id1) + query := builder.NewBatchQ(1000) + for query.Next() { + entities = append(entities, query.Entity()) + } + rand.Shuffle(len(entities), func(i, j int) { entities[i], entities[j] = entities[j], entities[i] }) + + var has bool + b.StartTimer() + for i := 0; i < b.N; i++ { + for _, e := range entities { + has = w.Alive(e) + } + } + b.StopTimer() + v := !has + _ = v +} + +func worldRelation_1000(b *testing.B) { + b.StopTimer() + + w := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024)) + id1 := ecs.ComponentID[relComp1](&w) + parent := w.NewEntity() + + entities := make([]ecs.Entity, 0, 1000) + builder := ecs.NewBuilder(&w, id1).WithRelation(id1) + query := builder.NewBatchQ(1000, parent) + for query.Next() { + entities = append(entities, query.Entity()) + } + rand.Shuffle(len(entities), func(i, j int) { entities[i], entities[j] = entities[j], entities[i] }) + + var par ecs.Entity + rel := w.Relations() + b.StartTimer() + for i := 0; i < b.N; i++ { + for _, e := range entities { + par = rel.Get(e, id1) + } + } + b.StopTimer() + v := par.IsZero() + _ = v +} + +func worldRelationUnchecked_1000(b *testing.B) { + b.StopTimer() + + w := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024)) + id1 := ecs.ComponentID[relComp1](&w) + parent := w.NewEntity() + + entities := make([]ecs.Entity, 0, 1000) + builder := ecs.NewBuilder(&w, id1).WithRelation(id1) + query := builder.NewBatchQ(1000, parent) + for query.Next() { + entities = append(entities, query.Entity()) + } + rand.Shuffle(len(entities), func(i, j int) { entities[i], entities[j] = entities[j], entities[i] }) + + var par ecs.Entity + rel := w.Relations() + b.StartTimer() + for i := 0; i < b.N; i++ { + for _, e := range entities { + par = rel.GetUnchecked(e, id1) + } + } + b.StopTimer() + v := par.IsZero() + _ = v +}