├── .github
├── actions
│ └── setup-go
│ │ └── action.yml
└── workflows
│ └── benchmarks.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench
├── add_remove
│ ├── arche.go
│ ├── ark.go
│ ├── doc.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── add_remove_large
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── comps
│ └── comps.go
├── create10comp
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── create2comp
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── create2comp_alloc
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── delete10comp
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── delete2comp
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── new_world
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── query256arch
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── query2comp
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── query32arch
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── random
│ ├── arche.go
│ ├── ark.go
│ ├── donburi.go
│ ├── ggecs.go
│ ├── run.go
│ ├── uot.go
│ └── volt.go
├── run.go
└── util
│ └── util.go
├── docs
└── README-template.md
├── go.mod
├── go.sum
├── main.go
└── plot
├── plot.py
└── requirements.txt
/.github/actions/setup-go/action.yml:
--------------------------------------------------------------------------------
1 | name: Set up Go
2 | description: Sets uo Go and installs dependencies
3 |
4 | runs:
5 | using: "composite" # <-- makes it re-usable
6 | steps:
7 | - name: Setup Go
8 | uses: actions/setup-go@v3
9 | with:
10 | go-version: '1.24.2'
11 | - name: Install dependencies
12 | run: go get .
13 | shell: bash
14 |
--------------------------------------------------------------------------------
/.github/workflows/benchmarks.yml:
--------------------------------------------------------------------------------
1 | name: Benchmarks
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | benchmarks_fast_1:
11 | name: Fast benchmarks 1
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: ./.github/actions/setup-go
16 | - name: Run benchmarks
17 | run: |
18 | go run . -test.benchtime=1s \
19 | query2comp \
20 | query32arch \
21 | query256arch \
22 |
23 | - name: Archive results
24 | uses: actions/upload-artifact@v4
25 | with:
26 | name: results_fast_1
27 | path: results/*
28 |
29 | benchmarks_fast_2:
30 | name: Fast benchmarks 2
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v3
34 | - uses: ./.github/actions/setup-go
35 | - name: Run benchmarks
36 | run: |
37 | go run . -test.benchtime=1s \
38 | random \
39 | add_remove \
40 | add_remove_large \
41 | new_world
42 |
43 | - name: Archive results
44 | uses: actions/upload-artifact@v4
45 | with:
46 | name: results_fast_2
47 | path: results/*
48 |
49 | benchmarks_slow_1:
50 | name: Slow benchmarks 1
51 | runs-on: ubuntu-latest
52 | steps:
53 | - uses: actions/checkout@v3
54 | - uses: ./.github/actions/setup-go
55 | - name: Run benchmarks
56 | run: |
57 | go run . -test.benchtime=0.1s \
58 | create2comp \
59 | create2comp_alloc \
60 | create10comp \
61 |
62 | - name: Archive results
63 | uses: actions/upload-artifact@v4
64 | with:
65 | name: results_slow_1
66 | path: results/*
67 |
68 | benchmarks_slow_2:
69 | name: Slow benchmarks 2
70 | runs-on: ubuntu-latest
71 | steps:
72 | - uses: actions/checkout@v3
73 | - uses: ./.github/actions/setup-go
74 | - name: Run benchmarks
75 | run: |
76 | go run . -test.benchtime=0.1s \
77 | delete2comp \
78 | delete10comp \
79 |
80 | - name: Archive results
81 | uses: actions/upload-artifact@v4
82 | with:
83 | name: results_slow_2
84 | path: results/*
85 |
86 | plots:
87 | name: Plot benchmarks
88 | runs-on: ubuntu-latest
89 | needs:
90 | - benchmarks_fast_1
91 | - benchmarks_fast_2
92 | - benchmarks_slow_1
93 | - benchmarks_slow_2
94 | steps:
95 | - uses: actions/checkout@v3
96 | - name: Setup Python
97 | uses: actions/setup-python@v5
98 | with:
99 | python-version: '3.13'
100 | cache: 'pip'
101 | - name: Install Python dependencies
102 | run: |
103 | pip install -r ./plot/requirements.txt
104 |
105 | - name: Download results_fast_1
106 | uses: actions/download-artifact@v4
107 | with:
108 | name: results_fast_1
109 | path: results
110 | - name: Download results_fast_2
111 | uses: actions/download-artifact@v4
112 | with:
113 | name: results_fast_2
114 | path: results
115 | - name: Download results_slow_1
116 | uses: actions/download-artifact@v4
117 | with:
118 | name: results_slow_1
119 | path: results
120 | - name: Download results_slow_2
121 | uses: actions/download-artifact@v4
122 | with:
123 | name: results_slow_2
124 | path: results
125 |
126 | - name: Plot results
127 | run: |
128 | python plot/plot.py
129 | - name: Archive results
130 | uses: actions/upload-artifact@v4
131 | with:
132 | name: results
133 | path: results
134 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /results/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Martin Lange
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Go ECS Benchmarks
2 |
3 | Comparative benchmarks for Go Entity Component System (ECS) implementations.
4 |
5 | > Disclaimer: This repository is maintained by the author of
6 | > [Arche](https://github.com/mlange-42/arche) and [Ark](https://github.com/mlange-42/ark).
7 |
8 | ## Benchmark candidates
9 |
10 | | ECS | Version |
11 | |-----|---------|
12 | | [Arche](https://github.com/mlange-42/arche) | v0.15.3 |
13 | | [Ark](https://github.com/mlange-42/ark) | v0.4.2 |
14 | | [Donburi](https://github.com/yottahmd/donburi) | v1.15.7 |
15 | | [go-gameengine-ecs](https://github.com/marioolofo/go-gameengine-ecs) | v0.9.0 |
16 | | [unitoftime/ecs](https://github.com/unitoftime/ecs) | v0.0.3 |
17 | | [Volt](https://github.com/akmonengine/volt) | v1.6.0 |
18 |
19 | Candidates are always displayed in alphabetical order.
20 |
21 | In case you develop or use a Go ECS that is not in the list and that want to see here,
22 | please open an issue or make a pull request.
23 | See the section on [Contributing](#contributing) for details.
24 |
25 | In case you are a developer or user of an implementation included here,
26 | feel free to check the benchmarked code for any possible improvements.
27 | Open an issue if you want a version update.
28 |
29 | ## Benchmarks
30 |
31 | Last run: Tue, 29 Apr 2025 09:47:51 UTC
32 | CPU: AMD EPYC 7763 64-Core Processor
33 |
34 |
35 | For each benchmark, the left plot panel and the table show the time spent per entity,
36 | while the right panel shows the total time.
37 |
38 | Note that the Y axis has logarithmic scale in all plots.
39 | So doubled bar or line height is not doubled time!
40 |
41 | All components used in the benchmarks have two `float64` fields.
42 | The initial capacity of the world is set to 1024 where this is supported.
43 |
44 | ### Query
45 |
46 | `N` entities with components `Position` and `Velocity`.
47 |
48 | - Query all `[Position, Velocity]` entities, and add the velocity vector to the position vector.
49 |
50 | 
51 |
52 | | N | Arche | Arche (cached) | Ark | Ark (cached) | Donburi | ggecs | uot | Volt |
53 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
54 | | 1 | 54.46ns | 46.14ns | 51.49ns | 49.70ns | 65.25ns | 44.00ns | 16.21ns | 200.49ns |
55 | | 4 | 18.02ns | 16.85ns | 15.64ns | 15.03ns | 31.97ns | 16.58ns | 6.40ns | 53.90ns |
56 | | 16 | 9.02ns | 8.50ns | 6.29ns | 6.46ns | 23.58ns | 11.26ns | 3.93ns | 16.95ns |
57 | | 64 | 7.08ns | 6.90ns | 4.24ns | 4.51ns | 21.67ns | 9.90ns | 3.45ns | 8.08ns |
58 | | 256 | 6.43ns | 6.39ns | 3.63ns | 3.95ns | 22.32ns | 9.51ns | 3.28ns | 5.55ns |
59 | | 1k | 6.30ns | 6.29ns | 3.56ns | 3.82ns | 22.39ns | 9.38ns | 3.15ns | 4.92ns |
60 | | 16k | 6.30ns | 6.33ns | 3.49ns | 3.79ns | 22.95ns | 9.38ns | 3.18ns | 4.81ns |
61 | | 256k | 6.30ns | 6.29ns | 3.48ns | 3.80ns | 22.06ns | 9.38ns | 3.18ns | 4.76ns |
62 | | 1M | 6.31ns | 6.29ns | 3.48ns | 3.78ns | 26.82ns | 9.41ns | 3.20ns | 4.77ns |
63 |
64 |
65 | ### Query fragmented, inner
66 |
67 | Query where the matching entities are fragmented over 32 archetypes.
68 |
69 | `N` entities with components `Position` and `Velocity`.
70 | Each of these `N` entities has some combination of components
71 | `C1`, `C2`, ..., `C5`, so entities are fragmented over up to 32 archetypes.
72 |
73 | - Query all `[Position, Velocity]` entities, and add the velocity vector to the position vector.
74 |
75 | 
76 |
77 | | N | Arche | Arche (cached) | Ark | Ark (cached) | Donburi | ggecs | uot | Volt |
78 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
79 | | 1 | 52.68ns | 48.45ns | 51.53ns | 49.66ns | 62.37ns | 43.98ns | 16.29ns | 217.97ns |
80 | | 4 | 24.77ns | 18.36ns | 26.15ns | 22.31ns | 32.76ns | 21.33ns | 14.82ns | 106.30ns |
81 | | 16 | 18.90ns | 12.27ns | 19.36ns | 14.31ns | 25.77ns | 14.81ns | 25.28ns | 74.01ns |
82 | | 64 | 11.91ns | 8.93ns | 11.48ns | 8.90ns | 22.97ns | 11.53ns | 15.70ns | 38.22ns |
83 | | 256 | 7.68ns | 6.87ns | 5.15ns | 4.67ns | 21.25ns | 9.99ns | 6.26ns | 12.99ns |
84 | | 1k | 6.62ns | 6.43ns | 3.88ns | 4.02ns | 21.88ns | 9.68ns | 4.05ns | 7.08ns |
85 | | 16k | 6.37ns | 6.35ns | 3.60ns | 3.85ns | 30.36ns | 9.44ns | 3.26ns | 5.01ns |
86 | | 256k | 6.33ns | 6.30ns | 3.51ns | 3.81ns | 66.34ns | 9.40ns | 3.20ns | 4.78ns |
87 | | 1M | 6.30ns | 6.31ns | 3.49ns | 3.81ns | 92.70ns | 9.41ns | 3.19ns | 4.77ns |
88 |
89 |
90 | ### Query fragmented, outer
91 |
92 | Query where there are 256 non-matching archetypes.
93 |
94 | `N` entities with components `Position` and `Velocity`.
95 | Another `4 * N` entities with `Position` and some combination of 8 components
96 | `C1`, ..., `C8`, so these entities are fragmented over up to 256 archetypes.
97 |
98 | - Query all `[Position, Velocity]` entities, and add the velocity vector to the position vector.
99 |
100 | 
101 |
102 | | N | Arche | Arche (cached) | Ark | Ark (cached) | Donburi | ggecs | uot | Volt |
103 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
104 | | 1 | 63.59ns | 46.57ns | 63.98ns | 49.38ns | 62.35ns | 55.96ns | 16.48ns | 232.22ns |
105 | | 4 | 29.17ns | 16.88ns | 28.14ns | 15.25ns | 29.69ns | 27.31ns | 9.60ns | 88.85ns |
106 | | 16 | 21.40ns | 8.37ns | 19.25ns | 6.45ns | 21.09ns | 23.02ns | 4.64ns | 57.34ns |
107 | | 64 | 19.60ns | 6.89ns | 16.85ns | 4.81ns | 19.13ns | 21.84ns | 3.58ns | 55.82ns |
108 | | 256 | 9.59ns | 6.39ns | 6.81ns | 3.91ns | 20.02ns | 12.62ns | 3.24ns | 17.46ns |
109 | | 1k | 7.11ns | 6.32ns | 4.36ns | 3.86ns | 43.24ns | 10.17ns | 3.17ns | 7.99ns |
110 | | 16k | 6.34ns | 6.28ns | 3.62ns | 3.79ns | 21.12ns | 9.49ns | 3.21ns | 5.02ns |
111 | | 256k | 6.29ns | 6.29ns | 3.49ns | 3.80ns | 22.06ns | 9.39ns | 3.18ns | 4.77ns |
112 | | 1M | 6.31ns | 6.44ns | 3.52ns | 3.80ns | 25.71ns | 9.39ns | 3.20ns | 4.77ns |
113 |
114 |
115 | ### Component random access
116 |
117 | `N` entities with component `Position`.
118 | All entities are collected into a slice, and the slice is shuffled.
119 |
120 | * Iterate the shuffled entities.
121 | * For each entity, get its `Position` and sum up their `X` fields.
122 |
123 | 
124 |
125 | | N | Arche | Ark | Donburi | ggecs | uot | Volt |
126 | | --- | --- | --- | --- | --- | --- | --- |
127 | | 1 | 4.72ns | 4.79ns | 10.37ns | 8.73ns | 36.82ns | 36.45ns |
128 | | 4 | 4.17ns | 4.63ns | 8.94ns | 8.52ns | 36.67ns | 35.94ns |
129 | | 16 | 4.06ns | 4.38ns | 8.89ns | 13.78ns | 37.39ns | 36.42ns |
130 | | 64 | 4.13ns | 4.45ns | 9.88ns | 14.09ns | 38.29ns | 36.10ns |
131 | | 256 | 4.02ns | 4.46ns | 9.52ns | 14.40ns | 39.87ns | 37.10ns |
132 | | 1k | 4.39ns | 4.51ns | 11.71ns | 17.74ns | 40.62ns | 40.04ns |
133 | | 16k | 9.78ns | 8.09ns | 36.30ns | 29.86ns | 59.04ns | 63.02ns |
134 | | 256k | 12.59ns | 13.74ns | 135.82ns | 56.75ns | 141.47ns | 94.72ns |
135 | | 1M | 47.64ns | 38.48ns | 201.54ns | 122.01ns | 189.94ns | 237.34ns |
136 |
137 |
138 | ### Create entities
139 |
140 | - Create `N` entities with components `Position` and `Velocity`.
141 |
142 | The operation is performed once before benchmarking,
143 | to exclude memory allocation, archetype creation etc.
144 | See the benchmark below for entity creation with allocation.
145 |
146 | 
147 |
148 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
149 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
150 | | 1 | 210.25ns | 569.51ns | 293.69ns | 269.05ns | 1.07us | 380.43ns | 431.26ns | 888.39ns |
151 | | 4 | 94.69ns | 148.80ns | 114.81ns | 73.83ns | 471.37ns | 155.48ns | 208.24ns | 391.56ns |
152 | | 16 | 56.45ns | 42.91ns | 79.08ns | 24.54ns | 326.01ns | 128.78ns | 136.54ns | 257.73ns |
153 | | 64 | 42.12ns | 19.43ns | 63.05ns | 14.00ns | 253.93ns | 113.93ns | 113.81ns | 201.77ns |
154 | | 256 | 39.13ns | 10.99ns | 53.07ns | 10.00ns | 191.51ns | 105.62ns | 98.09ns | 175.46ns |
155 | | 1k | 32.25ns | 9.59ns | 47.32ns | 8.86ns | 194.31ns | 104.99ns | 271.51ns | 183.97ns |
156 | | 16k | 27.84ns | 8.32ns | 41.68ns | 7.77ns | 212.04ns | 110.52ns | 277.69ns | 198.49ns |
157 | | 256k | 27.61ns | 8.23ns | 41.51ns | 7.60ns | 200.55ns | 114.65ns | 302.48ns | 220.17ns |
158 | | 1M | 27.99ns | 8.47ns | 41.82ns | 7.75ns | 187.91ns | 181.89ns | 434.34ns | 339.93ns |
159 |
160 |
161 | ### Create entities, allocating
162 |
163 | - Create `N` entities with components `Position` and `Velocity`.
164 |
165 | Each round is performed on a fresh world.
166 | This reflects the creation of the first entities with a certain components set in your game or application.
167 | As soon as things stabilize, the benchmarks for entity creation without allocation (above) apply.
168 |
169 | Low `N` values might be biased by things like archetype creation and memory allocation,
170 | which is handled differently by different implementations.
171 |
172 | 
173 |
174 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
175 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
176 | | 1 | 6.56us | 6.72us | 6.31us | 6.17us | 4.29us | 15.96us | 3.14us | 1.76us |
177 | | 4 | 1.95us | 1.90us | 1.90us | 1.80us | 1.34us | 4.30us | 1.07us | 781.85ns |
178 | | 16 | 468.38ns | 443.91ns | 485.97ns | 453.93ns | 670.05ns | 1.28us | 375.89ns | 407.14ns |
179 | | 64 | 158.23ns | 126.19ns | 169.89ns | 120.99ns | 380.52ns | 567.14ns | 233.28ns | 276.35ns |
180 | | 256 | 66.06ns | 41.89ns | 85.71ns | 43.36ns | 307.33ns | 258.17ns | 160.85ns | 227.67ns |
181 | | 1k | 47.71ns | 26.47ns | 55.69ns | 19.38ns | 287.09ns | 210.61ns | 157.42ns | 214.19ns |
182 | | 16k | 62.16ns | 31.08ns | 68.80ns | 38.75ns | 403.88ns | 235.24ns | 171.55ns | 360.72ns |
183 | | 256k | 111.80ns | 26.93ns | 89.43ns | 37.56ns | 462.99ns | 631.34ns | 271.59ns | 515.97ns |
184 | | 1M | 101.54ns | 19.32ns | 88.47ns | 32.25ns | 424.37ns | 1.46us | 367.81ns | 591.62ns |
185 |
186 |
187 | ### Create large entities
188 |
189 | - Create `N` entities with 10 components `C1`, ..., `C10`.
190 |
191 | The operation is performed once before benchmarking,
192 | to exclude things like archetype creation and memory allocation.
193 |
194 | 
195 |
196 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
197 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
198 | | 1 | 421.20ns | 781.00ns | 409.93ns | 390.66ns | 1.89us | 510.41ns | 688.40ns | 2.95us |
199 | | 4 | 173.78ns | 192.45ns | 192.02ns | 108.28ns | 1.28us | 262.95ns | 398.88ns | 2.06us |
200 | | 16 | 112.27ns | 58.87ns | 129.59ns | 34.90ns | 1.10us | 222.98ns | 311.56ns | 1.52us |
201 | | 64 | 98.13ns | 22.34ns | 105.05ns | 15.74ns | 818.06ns | 171.65ns | 243.23ns | 1.28us |
202 | | 256 | 81.77ns | 12.44ns | 97.84ns | 10.81ns | 719.66ns | 161.36ns | 220.65ns | 1.24us |
203 | | 1k | 79.70ns | 9.67ns | 88.83ns | 9.96ns | 732.77ns | 153.97ns | 460.57ns | 1.27us |
204 | | 16k | 75.86ns | 8.25ns | 85.53ns | 7.94ns | 788.51ns | 172.51ns | 475.62ns | 1.27us |
205 | | 256k | 75.34ns | 8.60ns | 85.42ns | 7.60ns | 695.24ns | 175.36ns | 573.88ns | 1.41us |
206 | | 1M | 76.39ns | 8.25ns | 85.15ns | 7.58ns | 665.61ns | 260.89ns | 676.61ns | 1.46us |
207 |
208 |
209 | ### Add/remove component
210 |
211 | `N` entities with component `Position`.
212 |
213 | - Query all `[Position]` entities and add `Velocity`.
214 | - Query all `[Position, Velocity]` entities and remove `Velocity`.
215 |
216 | One iteration is performed before the benchmarking starts, to exclude memory allocation.
217 |
218 | 
219 |
220 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
221 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
222 | | 1 | 215.27ns | 523.77ns | 214.98ns | 332.42ns | 506.62ns | 576.51ns | 353.84ns | 965.29ns |
223 | | 4 | 152.24ns | 146.12ns | 155.81ns | 93.29ns | 438.58ns | 522.65ns | 333.55ns | 590.35ns |
224 | | 16 | 134.95ns | 47.77ns | 128.00ns | 34.01ns | 426.50ns | 546.52ns | 333.99ns | 483.94ns |
225 | | 64 | 127.92ns | 23.70ns | 121.70ns | 20.52ns | 422.27ns | 537.34ns | 341.97ns | 467.95ns |
226 | | 256 | 134.90ns | 10.31ns | 118.77ns | 10.70ns | 417.71ns | 538.43ns | 352.24ns | 466.26ns |
227 | | 1k | 127.04ns | 7.12ns | 122.28ns | 9.03ns | 420.90ns | 555.24ns | 726.30ns | 473.15ns |
228 | | 16k | 127.53ns | 7.14ns | 119.62ns | 7.16ns | 472.08ns | 588.38ns | 778.43ns | 523.56ns |
229 | | 256k | 126.95ns | 8.20ns | 119.24ns | 8.40ns | 464.54ns | 636.07ns | 862.56ns | 607.03ns |
230 | | 1M | 127.14ns | 9.96ns | 119.74ns | 10.20ns | 475.48ns | 838.44ns | 1.27us | 882.49ns |
231 |
232 |
233 | ### Add/remove component, large entity
234 |
235 | `N` entities with component `Position` and 10 further components `C1`, ..., `C10`.
236 |
237 | - Query all `[Position]` entities and add `Velocity`.
238 | - Query all `[Position, Velocity]` entities and remove `Velocity`.
239 |
240 | One iteration is performed before the benchmarking starts, to exclude memory allocation.
241 |
242 | 
243 |
244 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
245 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
246 | | 1 | 565.09ns | 859.05ns | 435.00ns | 603.83ns | 1.11us | 1.01us | 817.82ns | 2.41us |
247 | | 4 | 513.92ns | 277.74ns | 410.27ns | 218.36ns | 1.06us | 975.66ns | 789.33ns | 1.88us |
248 | | 16 | 489.73ns | 135.82ns | 393.03ns | 118.82ns | 1.03us | 964.11ns | 799.60ns | 1.63us |
249 | | 64 | 478.16ns | 99.52ns | 394.72ns | 95.71ns | 999.54ns | 962.37ns | 871.56ns | 1.59us |
250 | | 256 | 457.27ns | 35.12ns | 381.62ns | 35.26ns | 1.02us | 953.43ns | 872.35ns | 1.64us |
251 | | 1k | 468.96ns | 19.09ns | 422.05ns | 18.81ns | 1.09us | 975.77ns | 1.39us | 1.64us |
252 | | 16k | 470.84ns | 21.24ns | 426.57ns | 25.49ns | 1.18us | 1.03us | 1.43us | 1.73us |
253 | | 256k | 610.91ns | 37.75ns | 536.47ns | 38.01ns | 1.26us | 1.35us | 1.86us | 2.27us |
254 | | 1M | 576.44ns | 41.24ns | 490.42ns | 41.52ns | 1.39us | 1.63us | 1.90us | 2.61us |
255 |
256 |
257 | ### Delete entities
258 |
259 | `N` entities with components `Position` and `Velocity`.
260 |
261 | * Delete all entities
262 |
263 | 
264 |
265 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
266 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
267 | | 1 | 189.07ns | 619.78ns | 160.50ns | 742.44ns | 251.74ns | 352.95ns | 204.79ns | 388.32ns |
268 | | 4 | 86.13ns | 173.14ns | 69.35ns | 195.04ns | 102.08ns | 157.74ns | 84.55ns | 220.65ns |
269 | | 16 | 55.54ns | 56.38ns | 40.67ns | 61.11ns | 69.06ns | 138.45ns | 54.40ns | 184.94ns |
270 | | 64 | 45.13ns | 26.74ns | 35.25ns | 28.29ns | 60.16ns | 126.20ns | 42.64ns | 147.94ns |
271 | | 256 | 39.59ns | 11.62ns | 30.43ns | 9.92ns | 56.99ns | 104.76ns | 46.21ns | 131.52ns |
272 | | 1k | 34.38ns | 7.98ns | 24.75ns | 5.61ns | 50.83ns | 103.12ns | 35.72ns | 128.66ns |
273 | | 16k | 34.24ns | 6.86ns | 23.38ns | 5.34ns | 51.95ns | 115.12ns | 51.79ns | 151.76ns |
274 | | 256k | 35.14ns | 6.91ns | 23.63ns | 5.14ns | 49.54ns | 147.73ns | 84.17ns | 224.51ns |
275 | | 1M | 34.13ns | 7.81ns | 23.83ns | 6.49ns | 50.34ns | 278.67ns | 187.72ns | 379.36ns |
276 |
277 |
278 | ### Delete large entities
279 |
280 | `N` entities with 10 components `C1`, ..., `C10`.
281 |
282 | * Delete all entities
283 |
284 | 
285 |
286 | | N | Arche | Arche (batch) | Ark | Ark (batch) | Donburi | ggecs | uot | Volt |
287 | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
288 | | 1 | 274.85ns | 642.51ns | 233.68ns | 1.05us | 406.11ns | 485.32ns | 180.42ns | 781.95ns |
289 | | 4 | 175.39ns | 168.68ns | 138.78ns | 314.91ns | 202.45ns | 286.39ns | 78.82ns | 594.19ns |
290 | | 16 | 133.46ns | 57.89ns | 113.90ns | 117.53ns | 160.51ns | 204.15ns | 52.09ns | 454.67ns |
291 | | 64 | 135.42ns | 27.05ns | 103.44ns | 72.31ns | 130.84ns | 178.09ns | 40.25ns | 336.12ns |
292 | | 256 | 116.03ns | 12.08ns | 94.24ns | 19.31ns | 117.15ns | 163.50ns | 43.39ns | 321.08ns |
293 | | 1k | 111.18ns | 7.58ns | 74.23ns | 7.88ns | 128.41ns | 164.30ns | 34.99ns | 317.24ns |
294 | | 16k | 105.44ns | 6.61ns | 72.76ns | 7.78ns | 121.93ns | 176.41ns | 51.03ns | 366.70ns |
295 | | 256k | 134.67ns | 7.47ns | 84.41ns | 14.44ns | 197.40ns | 371.86ns | 184.39ns | 565.67ns |
296 | | 1M | 113.36ns | 7.94ns | 82.34ns | 14.85ns | 126.52ns | 431.86ns | 239.32ns | 648.59ns |
297 |
298 |
299 | ### Create world
300 |
301 | - Create a new world
302 |
303 | | N | Arche | Ark | Donburi | ggecs | uot | Volt |
304 | | --- | --- | --- | --- | --- | --- | --- |
305 | | 1 | 8.84us | 13.51us | 2.38us | 180.96us | 2.22us | 40.20us |
306 |
307 |
308 | ### Popularity
309 |
310 | Given that all tested projects are on Github, we can use the star history as a proxy here.
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 | ## Running the benchmarks
319 |
320 | Run the benchmarks using the following command:
321 |
322 | ```shell
323 | go run . -test.benchtime=0.25s
324 | ```
325 |
326 | > On PowerShell use this instead:
327 | > `go run . --% -test.benchtime=0.25s`
328 |
329 | The `benchtime` limit is required for some of the benchmarks that have a high
330 | setup cost which is not timed. They would take forever otherwise.
331 | The benchmarks can take up to one hour to complete.
332 |
333 | To run a selection of benchmarks, add their names as arguments:
334 |
335 | ```shell
336 | go run . query2comp query1in10 query32arch
337 | ```
338 |
339 | To create the plots, run `plot/plot.py`. The following packages are required:
340 | - numpy
341 | - pandas
342 | - matplotlib
343 |
344 | ```
345 | pip install -r ./plot/requirements.txt
346 | python plot/plot.py
347 | ```
348 |
349 | ## Contributing
350 |
351 | Developers of ECS frameworks are welcome to add their implementation to the benchmarks.
352 | However, there are a few (quality) criteria that need to be fulfilled for inclusion:
353 |
354 | - All benchmarks must be implemented, which means that the ECS must have the required features
355 | - The ECS must be working properly and not exhibit serious flaws; it will undergo a basic review by maintainers
356 | - The ECS must be sufficiently documented so that it can be used without reading the code
357 | - There must be at least basic unit tests
358 | - Unit tests must be run in the CI of the repository
359 | - The ECS *must not* be tightly coupled to a particular game engine, particularly graphics stuff
360 | - There must be tagged release versions; only tagged versions will be included here
361 |
362 | Developers of included frameworks are encouraged to review the benchmarks,
363 | and to fix (or point to) misuse or potential optimizations.
364 |
--------------------------------------------------------------------------------
/bench/add_remove/arche.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | ids := []ecs.ID{velID}
16 |
17 | ecs.NewBuilder(&world, posID).NewBatch(n)
18 |
19 | filterPos := ecs.All(posID)
20 | filterPosVel := ecs.All(posID, velID)
21 |
22 | entities := make([]ecs.Entity, 0, n)
23 |
24 | // Iterate once for more fairness
25 | query := world.Query(&filterPos)
26 | for query.Next() {
27 | entities = append(entities, query.Entity())
28 | }
29 |
30 | for _, e := range entities {
31 | world.Add(e, ids...)
32 | }
33 |
34 | entities = entities[:0]
35 | query = world.Query(&filterPosVel)
36 | for query.Next() {
37 | entities = append(entities, query.Entity())
38 | }
39 |
40 | for _, e := range entities {
41 | world.Remove(e, ids...)
42 | }
43 |
44 | entities = entities[:0]
45 |
46 | for b.Loop() {
47 | query := world.Query(&filterPos)
48 | for query.Next() {
49 | entities = append(entities, query.Entity())
50 | }
51 |
52 | for _, e := range entities {
53 | world.Add(e, ids...)
54 | }
55 |
56 | entities = entities[:0]
57 | query = world.Query(&filterPosVel)
58 | for query.Next() {
59 | entities = append(entities, query.Entity())
60 | }
61 |
62 | for _, e := range entities {
63 | world.Remove(e, ids...)
64 | }
65 |
66 | entities = entities[:0]
67 | }
68 | }
69 |
70 | func runArcheBatched(b *testing.B, n int) {
71 | world := ecs.NewWorld(1024)
72 |
73 | posID := ecs.ComponentID[comps.Position](&world)
74 | velID := ecs.ComponentID[comps.Velocity](&world)
75 | ids := []ecs.ID{velID}
76 |
77 | ecs.NewBuilder(&world, posID).NewBatch(n)
78 |
79 | filterPos := ecs.All(posID)
80 | filterPosVel := ecs.All(posID, velID)
81 |
82 | // Iterate once for more fairness
83 | world.Batch().Add(&filterPos, ids...)
84 | world.Batch().Remove(&filterPosVel, ids...)
85 |
86 | for b.Loop() {
87 | world.Batch().Add(&filterPos, ids...)
88 | world.Batch().Remove(&filterPosVel, ids...)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/bench/add_remove/ark.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posMap := ecs.NewMap1[comps.Position](&world)
14 | velMap := ecs.NewMap1[comps.Velocity](&world)
15 |
16 | posMap.NewBatchFn(n, nil)
17 |
18 | filterPos := ecs.NewFilter1[comps.Position](&world)
19 | filterPosVel := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
20 |
21 | entities := make([]ecs.Entity, 0, n)
22 |
23 | // Iterate once for more fairness
24 | query1 := filterPos.Query()
25 | for query1.Next() {
26 | entities = append(entities, query1.Entity())
27 | }
28 |
29 | for _, e := range entities {
30 | velMap.AddFn(e, nil)
31 | }
32 |
33 | entities = entities[:0]
34 | query2 := filterPosVel.Query()
35 | for query2.Next() {
36 | entities = append(entities, query2.Entity())
37 | }
38 |
39 | for _, e := range entities {
40 | velMap.Remove(e)
41 | }
42 |
43 | entities = entities[:0]
44 |
45 | for b.Loop() {
46 | query1 = filterPos.Query()
47 | for query1.Next() {
48 | entities = append(entities, query1.Entity())
49 | }
50 |
51 | for _, e := range entities {
52 | velMap.AddFn(e, nil)
53 | }
54 |
55 | entities = entities[:0]
56 | query2 = filterPosVel.Query()
57 | for query2.Next() {
58 | entities = append(entities, query2.Entity())
59 | }
60 |
61 | for _, e := range entities {
62 | velMap.Remove(e)
63 | }
64 |
65 | entities = entities[:0]
66 | }
67 | }
68 |
69 | func runArkBatched(b *testing.B, n int) {
70 | world := ecs.NewWorld(1024)
71 |
72 | posMap := ecs.NewMap1[comps.Position](&world)
73 | velMap := ecs.NewMap1[comps.Velocity](&world)
74 |
75 | posMap.NewBatchFn(n, nil)
76 |
77 | filterPos := ecs.NewFilter1[comps.Position](&world)
78 | filterPosVel := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
79 |
80 | // Iterate once for more fairness
81 | velMap.AddBatchFn(filterPos.Batch(), nil)
82 | velMap.RemoveBatch(filterPosVel.Batch(), nil)
83 |
84 | for b.Loop() {
85 | velMap.AddBatchFn(filterPos.Batch(), nil)
86 | velMap.RemoveBatch(filterPosVel.Batch(), nil)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/bench/add_remove/doc.go:
--------------------------------------------------------------------------------
1 | // Package addremove benchmarks adding and removing components
2 | //
3 | // Setup:
4 | // - 1000 entities with Position{f64, f64}
5 | //
6 | // Benchmark:
7 | // - Iterate all entities with Position, and add Velocity
8 | // - Iterate all entities with Position and Velocity, and remove Velocity
9 | package addremove
10 |
--------------------------------------------------------------------------------
/bench/add_remove/donburi.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/filter"
9 | )
10 |
11 | func runDonburi(b *testing.B, n int) {
12 | world := donburi.NewWorld()
13 |
14 | var position = donburi.NewComponentType[comps.Position]()
15 | var velocity = donburi.NewComponentType[comps.Velocity]()
16 |
17 | for i := 0; i < n; i++ {
18 | world.Create(position)
19 | }
20 |
21 | queryPos := donburi.NewQuery(filter.Contains(position))
22 | queryPosVel := donburi.NewQuery(filter.Contains(position, velocity))
23 |
24 | // Iterate once for more fairness
25 | queryPos.Each(world, func(entry *donburi.Entry) {
26 | entry.AddComponent(velocity)
27 | })
28 | queryPosVel.Each(world, func(entry *donburi.Entry) {
29 | entry.RemoveComponent(velocity)
30 | })
31 |
32 | for b.Loop() {
33 | queryPos.Each(world, func(entry *donburi.Entry) {
34 | entry.AddComponent(velocity)
35 | })
36 | queryPosVel.Each(world, func(entry *donburi.Entry) {
37 | entry.RemoveComponent(velocity)
38 | })
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/bench/add_remove/ggecs.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | )
15 |
16 | func runGGEcs(b *testing.B, n int) {
17 | world := ecs.NewWorld(1024)
18 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
19 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
20 |
21 | for i := 0; i < n; i++ {
22 | _ = world.NewEntity(PositionComponentID)
23 | }
24 |
25 | posMask := ecs.MakeComponentMask(PositionComponentID)
26 | posVelMask := ecs.MakeComponentMask(PositionComponentID, VelocityComponentID)
27 |
28 | entities := make([]ecs.EntityID, 0, n)
29 |
30 | // Iterate once for more fairness
31 | query := world.Query(posMask)
32 | for query.Next() {
33 | entities = append(entities, query.Entity())
34 | }
35 |
36 | for _, e := range entities {
37 | world.AddComponent(e, VelocityComponentID)
38 | }
39 |
40 | entities = entities[:0]
41 | query = world.Query(posVelMask)
42 | for query.Next() {
43 | entities = append(entities, query.Entity())
44 | }
45 |
46 | for _, e := range entities {
47 | world.RemComponent(e, VelocityComponentID)
48 | }
49 | entities = entities[:0]
50 |
51 | for b.Loop() {
52 | query := world.Query(posMask)
53 | for query.Next() {
54 | entities = append(entities, query.Entity())
55 | }
56 |
57 | for _, e := range entities {
58 | world.AddComponent(e, VelocityComponentID)
59 | }
60 |
61 | entities = entities[:0]
62 | query = world.Query(posVelMask)
63 | for query.Next() {
64 | entities = append(entities, query.Entity())
65 | }
66 |
67 | for _, e := range entities {
68 | world.RemComponent(e, VelocityComponentID)
69 | }
70 | entities = entities[:0]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/bench/add_remove/run.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/add_remove/uot.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | queryPos := ecs.Query1[comps.Position](world)
14 | queryPosVel := ecs.Query2[comps.Position, comps.Velocity](world)
15 | comp := ecs.C(comps.Velocity{})
16 |
17 | for i := 0; i < n; i++ {
18 | id := world.NewId()
19 | ecs.Write(world, id,
20 | ecs.C(comps.Position{}),
21 | )
22 | }
23 |
24 | entities := make([]ecs.Id, 0, n)
25 |
26 | // Iterate once for more fairness
27 | queryPos.MapId(func(id ecs.Id, pos *comps.Position) {
28 | entities = append(entities, id)
29 | })
30 |
31 | for _, e := range entities {
32 | ecs.Write(world, e,
33 | ecs.C(comps.Velocity{}),
34 | )
35 | }
36 |
37 | entities = entities[:0]
38 |
39 | queryPosVel.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
40 | entities = append(entities, id)
41 | })
42 |
43 | for _, e := range entities {
44 | ecs.DeleteComponent(world, e, comp)
45 | }
46 |
47 | entities = entities[:0]
48 |
49 | for b.Loop() {
50 | queryPos.MapId(func(id ecs.Id, pos *comps.Position) {
51 | entities = append(entities, id)
52 | })
53 |
54 | for _, e := range entities {
55 | ecs.Write(world, e,
56 | ecs.C(comps.Velocity{}),
57 | )
58 | }
59 |
60 | entities = entities[:0]
61 |
62 | queryPosVel.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
63 | entities = append(entities, id)
64 | })
65 |
66 | for _, e := range entities {
67 | ecs.DeleteComponent(world, e, comp)
68 | }
69 |
70 | entities = entities[:0]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/bench/add_remove/volt.go:
--------------------------------------------------------------------------------
1 | package addremove
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.Velocity](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 |
19 | for i := 0; i < n; i++ {
20 | e := world.CreateEntity(strconv.Itoa(i))
21 | volt.AddComponent(world, e, comps.Position{})
22 | }
23 |
24 | posMask := volt.CreateQuery1[comps.Position](world, volt.QueryConfiguration{})
25 | posVelMask := volt.CreateQuery2[comps.Position, comps.Velocity](world, volt.QueryConfiguration{})
26 |
27 | entities := make([]volt.EntityId, 0, n)
28 |
29 | // Iterate once for more fairness
30 | for result := range posMask.Foreach(nil) {
31 | entities = append(entities, result.EntityId)
32 | }
33 |
34 | for _, e := range entities {
35 | volt.AddComponent(world, e, comps.Velocity{})
36 | }
37 |
38 | entities = entities[:0]
39 | for result := range posVelMask.Foreach(nil) {
40 | entities = append(entities, result.EntityId)
41 | }
42 |
43 | for _, e := range entities {
44 | volt.RemoveComponent[comps.Velocity](world, e)
45 | }
46 | entities = entities[:0]
47 |
48 | for b.Loop() {
49 | for result := range posMask.Foreach(nil) {
50 | entities = append(entities, result.EntityId)
51 | }
52 |
53 | for _, e := range entities {
54 | volt.AddComponent(world, e, comps.Velocity{})
55 | }
56 |
57 | entities = entities[:0]
58 | for result := range posVelMask.Foreach(nil) {
59 | entities = append(entities, result.EntityId)
60 | }
61 |
62 | for _, e := range entities {
63 | volt.RemoveComponent[comps.Velocity](world, e)
64 | }
65 | entities = entities[:0]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/bench/add_remove_large/arche.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | ids := []ecs.ID{velID}
16 |
17 | allIDs := []ecs.ID{
18 | posID,
19 | ecs.ComponentID[comps.C1](&world),
20 | ecs.ComponentID[comps.C2](&world),
21 | ecs.ComponentID[comps.C3](&world),
22 | ecs.ComponentID[comps.C4](&world),
23 | ecs.ComponentID[comps.C5](&world),
24 | ecs.ComponentID[comps.C6](&world),
25 | ecs.ComponentID[comps.C7](&world),
26 | ecs.ComponentID[comps.C8](&world),
27 | ecs.ComponentID[comps.C9](&world),
28 | ecs.ComponentID[comps.C10](&world),
29 | }
30 |
31 | world.Batch().New(n, allIDs...)
32 |
33 | filterPos := ecs.All(posID)
34 | filterPosVel := ecs.All(posID, velID)
35 |
36 | entities := make([]ecs.Entity, 0, n)
37 |
38 | // Iterate once for more fairness
39 | query := world.Query(&filterPos)
40 | for query.Next() {
41 | entities = append(entities, query.Entity())
42 | }
43 |
44 | for _, e := range entities {
45 | world.Add(e, ids...)
46 | }
47 |
48 | entities = entities[:0]
49 | query = world.Query(&filterPosVel)
50 | for query.Next() {
51 | entities = append(entities, query.Entity())
52 | }
53 |
54 | for _, e := range entities {
55 | world.Remove(e, ids...)
56 | }
57 |
58 | entities = entities[:0]
59 |
60 | for b.Loop() {
61 | query := world.Query(&filterPos)
62 | for query.Next() {
63 | entities = append(entities, query.Entity())
64 | }
65 |
66 | for _, e := range entities {
67 | world.Add(e, ids...)
68 | }
69 |
70 | entities = entities[:0]
71 | query = world.Query(&filterPosVel)
72 | for query.Next() {
73 | entities = append(entities, query.Entity())
74 | }
75 |
76 | for _, e := range entities {
77 | world.Remove(e, ids...)
78 | }
79 |
80 | entities = entities[:0]
81 | }
82 | }
83 |
84 | func runArcheBatched(b *testing.B, n int) {
85 | world := ecs.NewWorld(1024)
86 |
87 | posID := ecs.ComponentID[comps.Position](&world)
88 | velID := ecs.ComponentID[comps.Velocity](&world)
89 | ids := []ecs.ID{velID}
90 |
91 | allIDs := []ecs.ID{
92 | posID,
93 | ecs.ComponentID[comps.C1](&world),
94 | ecs.ComponentID[comps.C2](&world),
95 | ecs.ComponentID[comps.C3](&world),
96 | ecs.ComponentID[comps.C4](&world),
97 | ecs.ComponentID[comps.C5](&world),
98 | ecs.ComponentID[comps.C6](&world),
99 | ecs.ComponentID[comps.C7](&world),
100 | ecs.ComponentID[comps.C8](&world),
101 | ecs.ComponentID[comps.C9](&world),
102 | ecs.ComponentID[comps.C10](&world),
103 | }
104 |
105 | world.Batch().New(n, allIDs...)
106 |
107 | filterPos := ecs.All(posID)
108 | filterPosVel := ecs.All(posID, velID)
109 |
110 | // Iterate once for more fairness
111 | world.Batch().Add(&filterPos, ids...)
112 | world.Batch().Remove(&filterPosVel, ids...)
113 |
114 | for b.Loop() {
115 | world.Batch().Add(&filterPos, ids...)
116 | world.Batch().Remove(&filterPosVel, ids...)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/bench/add_remove_large/ark.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | velMap := ecs.NewMap1[comps.Velocity](&world)
14 |
15 | mapper := ecs.NewMap11[
16 | comps.Position,
17 | comps.C1, comps.C2, comps.C3, comps.C4, comps.C5,
18 | comps.C6, comps.C7, comps.C8, comps.C9, comps.C10,
19 | ](&world)
20 |
21 | mapper.NewBatchFn(n, nil)
22 |
23 | filterPos := ecs.NewFilter1[comps.Position](&world)
24 | filterPosVel := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
25 |
26 | entities := make([]ecs.Entity, 0, n)
27 |
28 | // Iterate once for more fairness
29 | query1 := filterPos.Query()
30 | for query1.Next() {
31 | entities = append(entities, query1.Entity())
32 | }
33 |
34 | for _, e := range entities {
35 | velMap.AddFn(e, nil)
36 | }
37 |
38 | entities = entities[:0]
39 | query2 := filterPosVel.Query()
40 | for query2.Next() {
41 | entities = append(entities, query2.Entity())
42 | }
43 |
44 | for _, e := range entities {
45 | velMap.Remove(e)
46 | }
47 |
48 | entities = entities[:0]
49 |
50 | for b.Loop() {
51 | query1 = filterPos.Query()
52 | for query1.Next() {
53 | entities = append(entities, query1.Entity())
54 | }
55 |
56 | for _, e := range entities {
57 | velMap.AddFn(e, nil)
58 | }
59 |
60 | entities = entities[:0]
61 | query2 = filterPosVel.Query()
62 | for query2.Next() {
63 | entities = append(entities, query2.Entity())
64 | }
65 |
66 | for _, e := range entities {
67 | velMap.Remove(e)
68 | }
69 |
70 | entities = entities[:0]
71 | }
72 | }
73 |
74 | func runArkBatched(b *testing.B, n int) {
75 | world := ecs.NewWorld(1024)
76 |
77 | velMap := ecs.NewMap1[comps.Velocity](&world)
78 |
79 | mapper := ecs.NewMap11[
80 | comps.Position,
81 | comps.C1, comps.C2, comps.C3, comps.C4, comps.C5,
82 | comps.C6, comps.C7, comps.C8, comps.C9, comps.C10,
83 | ](&world)
84 |
85 | mapper.NewBatchFn(n, nil)
86 |
87 | filterPos := ecs.NewFilter1[comps.Position](&world)
88 | filterPosVel := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
89 |
90 | // Iterate once for more fairness
91 | velMap.AddBatchFn(filterPos.Batch(), nil)
92 | velMap.RemoveBatch(filterPosVel.Batch(), nil)
93 |
94 | for b.Loop() {
95 | velMap.AddBatchFn(filterPos.Batch(), nil)
96 | velMap.RemoveBatch(filterPosVel.Batch(), nil)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/bench/add_remove_large/donburi.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | "github.com/yohamta/donburi/filter"
10 | )
11 |
12 | func runDonburi(b *testing.B, n int) {
13 | world := donburi.NewWorld()
14 |
15 | var position = donburi.NewComponentType[comps.Position]()
16 | var velocity = donburi.NewComponentType[comps.Velocity]()
17 |
18 | allIDs := []component.IComponentType{
19 | position,
20 | donburi.NewComponentType[comps.C1](),
21 | donburi.NewComponentType[comps.C2](),
22 | donburi.NewComponentType[comps.C3](),
23 | donburi.NewComponentType[comps.C4](),
24 | donburi.NewComponentType[comps.C5](),
25 | donburi.NewComponentType[comps.C6](),
26 | donburi.NewComponentType[comps.C7](),
27 | donburi.NewComponentType[comps.C8](),
28 | donburi.NewComponentType[comps.C9](),
29 | donburi.NewComponentType[comps.C10](),
30 | }
31 |
32 | for i := 0; i < n; i++ {
33 | world.Create(allIDs...)
34 | }
35 |
36 | queryPos := donburi.NewQuery(filter.Contains(position))
37 | queryPosVel := donburi.NewQuery(filter.Contains(position, velocity))
38 |
39 | // Iterate once for more fairness
40 | queryPos.Each(world, func(entry *donburi.Entry) {
41 | entry.AddComponent(velocity)
42 | })
43 | queryPosVel.Each(world, func(entry *donburi.Entry) {
44 | entry.RemoveComponent(velocity)
45 | })
46 |
47 | for b.Loop() {
48 | queryPos.Each(world, func(entry *donburi.Entry) {
49 | entry.AddComponent(velocity)
50 | })
51 | queryPosVel.Each(world, func(entry *donburi.Entry) {
52 | entry.RemoveComponent(velocity)
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/bench/add_remove_large/ggecs.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | C1ID
15 | C2ID
16 | C3ID
17 | C4ID
18 | C5ID
19 | C6ID
20 | C7ID
21 | C8ID
22 | C9ID
23 | C10ID
24 | )
25 |
26 | func runGGEcs(b *testing.B, n int) {
27 | world := ecs.NewWorld(1024)
28 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
29 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
30 |
31 | world.Register(ecs.NewComponentRegistry[comps.C1](C1ID))
32 | world.Register(ecs.NewComponentRegistry[comps.C2](C2ID))
33 | world.Register(ecs.NewComponentRegistry[comps.C3](C3ID))
34 | world.Register(ecs.NewComponentRegistry[comps.C4](C4ID))
35 | world.Register(ecs.NewComponentRegistry[comps.C5](C5ID))
36 | world.Register(ecs.NewComponentRegistry[comps.C6](C6ID))
37 | world.Register(ecs.NewComponentRegistry[comps.C7](C7ID))
38 | world.Register(ecs.NewComponentRegistry[comps.C8](C8ID))
39 | world.Register(ecs.NewComponentRegistry[comps.C9](C9ID))
40 | world.Register(ecs.NewComponentRegistry[comps.C10](C10ID))
41 |
42 | for i := 0; i < n; i++ {
43 | _ = world.NewEntity(
44 | PositionComponentID,
45 | C1ID, C2ID, C3ID, C4ID, C5ID,
46 | C6ID, C7ID, C8ID, C9ID, C10ID,
47 | )
48 | }
49 |
50 | posMask := ecs.MakeComponentMask(PositionComponentID)
51 | posVelMask := ecs.MakeComponentMask(PositionComponentID, VelocityComponentID)
52 |
53 | entities := make([]ecs.EntityID, 0, n)
54 |
55 | // Iterate once for more fairness
56 | query := world.Query(posMask)
57 | for query.Next() {
58 | entities = append(entities, query.Entity())
59 | }
60 |
61 | for _, e := range entities {
62 | world.AddComponent(e, VelocityComponentID)
63 | }
64 |
65 | entities = entities[:0]
66 | query = world.Query(posVelMask)
67 | for query.Next() {
68 | entities = append(entities, query.Entity())
69 | }
70 |
71 | for _, e := range entities {
72 | world.RemComponent(e, VelocityComponentID)
73 | }
74 | entities = entities[:0]
75 |
76 | for b.Loop() {
77 | query := world.Query(posMask)
78 | for query.Next() {
79 | entities = append(entities, query.Entity())
80 | }
81 |
82 | for _, e := range entities {
83 | world.AddComponent(e, VelocityComponentID)
84 | }
85 |
86 | entities = entities[:0]
87 | query = world.Query(posVelMask)
88 | for query.Next() {
89 | entities = append(entities, query.Entity())
90 | }
91 |
92 | for _, e := range entities {
93 | world.RemComponent(e, VelocityComponentID)
94 | }
95 | entities = entities[:0]
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/bench/add_remove_large/run.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/add_remove_large/uot.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | queryPos := ecs.Query1[comps.Position](world)
14 | queryPosVel := ecs.Query2[comps.Position, comps.Velocity](world)
15 | comp := ecs.C(comps.Velocity{})
16 |
17 | for i := 0; i < n; i++ {
18 | id := world.NewId()
19 | ecs.Write(world, id,
20 | ecs.C(comps.Position{}),
21 | ecs.C(comps.C1{}),
22 | ecs.C(comps.C2{}),
23 | ecs.C(comps.C3{}),
24 | ecs.C(comps.C4{}),
25 | ecs.C(comps.C5{}),
26 | ecs.C(comps.C6{}),
27 | ecs.C(comps.C7{}),
28 | ecs.C(comps.C8{}),
29 | ecs.C(comps.C9{}),
30 | ecs.C(comps.C10{}),
31 | )
32 | }
33 |
34 | entities := make([]ecs.Id, 0, n)
35 |
36 | // Iterate once for more fairness
37 | queryPos.MapId(func(id ecs.Id, pos *comps.Position) {
38 | entities = append(entities, id)
39 | })
40 |
41 | for _, e := range entities {
42 | ecs.Write(world, e,
43 | ecs.C(comps.Velocity{}),
44 | )
45 | }
46 |
47 | entities = entities[:0]
48 |
49 | queryPosVel.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
50 | entities = append(entities, id)
51 | })
52 |
53 | for _, e := range entities {
54 | ecs.DeleteComponent(world, e, comp)
55 | }
56 |
57 | entities = entities[:0]
58 |
59 | for b.Loop() {
60 | queryPos.MapId(func(id ecs.Id, pos *comps.Position) {
61 | entities = append(entities, id)
62 | })
63 |
64 | for _, e := range entities {
65 | ecs.Write(world, e,
66 | ecs.C(comps.Velocity{}),
67 | )
68 | }
69 |
70 | entities = entities[:0]
71 |
72 | queryPosVel.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
73 | entities = append(entities, id)
74 | })
75 |
76 | for _, e := range entities {
77 | ecs.DeleteComponent(world, e, comp)
78 | }
79 |
80 | entities = entities[:0]
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/bench/add_remove_large/volt.go:
--------------------------------------------------------------------------------
1 | package addremovelarge
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.Velocity](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 |
19 | volt.RegisterComponent[comps.C1](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
20 | volt.RegisterComponent[comps.C2](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
21 | volt.RegisterComponent[comps.C3](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
22 | volt.RegisterComponent[comps.C4](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
23 | volt.RegisterComponent[comps.C5](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
24 | volt.RegisterComponent[comps.C6](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
25 | volt.RegisterComponent[comps.C7](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
26 | volt.RegisterComponent[comps.C8](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
27 | volt.RegisterComponent[comps.C9](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
28 | volt.RegisterComponent[comps.C10](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
29 |
30 | for i := 0; i < n; i++ {
31 | e, err := volt.CreateEntityWithComponents8(world, strconv.Itoa(i), comps.Position{},
32 | comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{},
33 | comps.C5{}, comps.C6{}, comps.C7{},
34 | )
35 | if err != nil {
36 | panic("Volt crashed")
37 | }
38 | volt.AddComponents3(world, e, comps.C8{}, comps.C9{}, comps.C10{})
39 | }
40 |
41 | posMask := volt.CreateQuery1[comps.Position](world, volt.QueryConfiguration{})
42 | posVelMask := volt.CreateQuery2[comps.Position, comps.Velocity](world, volt.QueryConfiguration{})
43 |
44 | entities := make([]volt.EntityId, 0, n)
45 |
46 | // Iterate once for more fairness
47 | for result := range posMask.Foreach(nil) {
48 | entities = append(entities, result.EntityId)
49 | }
50 |
51 | for _, e := range entities {
52 | volt.AddComponent(world, e, comps.Velocity{})
53 | }
54 |
55 | entities = entities[:0]
56 | for result := range posVelMask.Foreach(nil) {
57 | entities = append(entities, result.EntityId)
58 | }
59 |
60 | for _, e := range entities {
61 | volt.RemoveComponent[comps.Velocity](world, e)
62 | }
63 | entities = entities[:0]
64 |
65 | for b.Loop() {
66 | for result := range posMask.Foreach(nil) {
67 | entities = append(entities, result.EntityId)
68 | }
69 |
70 | for _, e := range entities {
71 | volt.AddComponent(world, e, comps.Velocity{})
72 | }
73 |
74 | entities = entities[:0]
75 | for result := range posVelMask.Foreach(nil) {
76 | entities = append(entities, result.EntityId)
77 | }
78 |
79 | for _, e := range entities {
80 | volt.RemoveComponent[comps.Velocity](world, e)
81 | }
82 | entities = entities[:0]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/bench/comps/comps.go:
--------------------------------------------------------------------------------
1 | package comps
2 |
3 | import "github.com/akmonengine/volt"
4 |
5 | const (
6 | PositionId = iota
7 | VelocityId
8 | C1Id
9 | C2Id
10 | C3Id
11 | C4Id
12 | C5Id
13 | C6Id
14 | C7Id
15 | C8Id
16 | C9Id
17 | C10Id
18 | )
19 |
20 | // Position component
21 | type Position struct {
22 | X float64
23 | Y float64
24 | }
25 |
26 | func (c Position) GetComponentId() volt.ComponentId {
27 | return PositionId
28 | }
29 |
30 | // Velocity component
31 | type Velocity struct {
32 | X float64
33 | Y float64
34 | }
35 |
36 | func (c Velocity) GetComponentId() volt.ComponentId {
37 | return VelocityId
38 | }
39 |
40 | type C1 struct {
41 | X float64
42 | Y float64
43 | }
44 |
45 | func (c C1) GetComponentId() volt.ComponentId {
46 | return C1Id
47 | }
48 |
49 | type C2 struct {
50 | X float64
51 | Y float64
52 | }
53 |
54 | func (c C2) GetComponentId() volt.ComponentId {
55 | return C2Id
56 | }
57 |
58 | type C3 struct {
59 | X float64
60 | Y float64
61 | }
62 |
63 | func (c C3) GetComponentId() volt.ComponentId {
64 | return C3Id
65 | }
66 |
67 | type C4 struct {
68 | X float64
69 | Y float64
70 | }
71 |
72 | func (c C4) GetComponentId() volt.ComponentId {
73 | return C4Id
74 | }
75 |
76 | type C5 struct {
77 | X float64
78 | Y float64
79 | }
80 |
81 | func (c C5) GetComponentId() volt.ComponentId {
82 | return C5Id
83 | }
84 |
85 | type C6 struct {
86 | X float64
87 | Y float64
88 | }
89 |
90 | func (c C6) GetComponentId() volt.ComponentId {
91 | return C6Id
92 | }
93 |
94 | type C7 struct {
95 | X float64
96 | Y float64
97 | }
98 |
99 | func (c C7) GetComponentId() volt.ComponentId {
100 | return C7Id
101 | }
102 |
103 | type C8 struct {
104 | X float64
105 | Y float64
106 | }
107 |
108 | func (c C8) GetComponentId() volt.ComponentId {
109 | return C8Id
110 | }
111 |
112 | type C9 struct {
113 | X float64
114 | Y float64
115 | }
116 |
117 | func (c C9) GetComponentId() volt.ComponentId {
118 | return C9Id
119 | }
120 |
121 | type C10 struct {
122 | X float64
123 | Y float64
124 | }
125 |
126 | func (c C10) GetComponentId() volt.ComponentId {
127 | return C10Id
128 | }
129 |
--------------------------------------------------------------------------------
/bench/create10comp/arche.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | ids := []ecs.ID{
14 | ecs.ComponentID[comps.C1](&world),
15 | ecs.ComponentID[comps.C2](&world),
16 | ecs.ComponentID[comps.C3](&world),
17 | ecs.ComponentID[comps.C4](&world),
18 | ecs.ComponentID[comps.C5](&world),
19 | ecs.ComponentID[comps.C6](&world),
20 | ecs.ComponentID[comps.C7](&world),
21 | ecs.ComponentID[comps.C8](&world),
22 | ecs.ComponentID[comps.C9](&world),
23 | ecs.ComponentID[comps.C10](&world),
24 | }
25 |
26 | ecs.NewBuilder(&world, ids...).NewBatch(n)
27 | world.Batch().RemoveEntities(ecs.All(ids...))
28 |
29 | entities := make([]ecs.Entity, 0, n)
30 |
31 | for b.Loop() {
32 | for range n {
33 | e := world.NewEntity(ids...)
34 | // Just for fairness, because the others need to do that, too.
35 | entities = append(entities, e)
36 | }
37 | b.StopTimer()
38 |
39 | if n < 64 {
40 | // Speed up cleanup for low entity counts
41 | for i := len(entities) - 1; i >= 0; i-- {
42 | world.RemoveEntity(entities[i])
43 | }
44 | } else {
45 | world.Batch().RemoveEntities(ecs.All(ids...))
46 | }
47 |
48 | entities = entities[:0]
49 | b.StartTimer()
50 | }
51 | }
52 |
53 | func runArcheBatched(b *testing.B, n int) {
54 | world := ecs.NewWorld(1024)
55 |
56 | ids := []ecs.ID{
57 | ecs.ComponentID[comps.C1](&world),
58 | ecs.ComponentID[comps.C2](&world),
59 | ecs.ComponentID[comps.C3](&world),
60 | ecs.ComponentID[comps.C4](&world),
61 | ecs.ComponentID[comps.C5](&world),
62 | ecs.ComponentID[comps.C6](&world),
63 | ecs.ComponentID[comps.C7](&world),
64 | ecs.ComponentID[comps.C8](&world),
65 | ecs.ComponentID[comps.C9](&world),
66 | ecs.ComponentID[comps.C10](&world),
67 | }
68 |
69 | ecs.NewBuilder(&world, ids...).NewBatch(n)
70 | world.Batch().RemoveEntities(ecs.All(ids...))
71 |
72 | for b.Loop() {
73 | ecs.NewBuilder(&world, ids...).NewBatch(n)
74 | b.StopTimer()
75 | world.Batch().RemoveEntities(ecs.All(ids...))
76 | b.StartTimer()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/bench/create10comp/ark.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | mapper := ecs.NewMap10[
14 | comps.C1, comps.C2, comps.C3, comps.C4, comps.C5,
15 | comps.C6, comps.C7, comps.C8, comps.C9, comps.C10,
16 | ](&world)
17 | filter := ecs.NewFilter0(&world)
18 |
19 | mapper.NewBatchFn(n, nil)
20 | world.RemoveEntities(filter.Batch(), nil)
21 |
22 | entities := make([]ecs.Entity, 0, n)
23 |
24 | for b.Loop() {
25 | for range n {
26 | e := mapper.NewEntityFn(nil)
27 | // Just for fairness, because the others need to do that, too.
28 | entities = append(entities, e)
29 | }
30 | b.StopTimer()
31 |
32 | if n < 64 {
33 | // Speed up cleanup for low entity counts
34 | for i := len(entities) - 1; i >= 0; i-- {
35 | world.RemoveEntity(entities[i])
36 | }
37 | } else {
38 | world.RemoveEntities(filter.Batch(), nil)
39 | }
40 |
41 | entities = entities[:0]
42 | b.StartTimer()
43 | }
44 | }
45 |
46 | func runArkBatched(b *testing.B, n int) {
47 | world := ecs.NewWorld(1024)
48 |
49 | mapper := ecs.NewMap10[
50 | comps.C1, comps.C2, comps.C3, comps.C4, comps.C5,
51 | comps.C6, comps.C7, comps.C8, comps.C9, comps.C10,
52 | ](&world)
53 | filter := ecs.NewFilter0(&world)
54 |
55 | mapper.NewBatchFn(n, nil)
56 | world.RemoveEntities(filter.Batch(), nil)
57 |
58 | for b.Loop() {
59 | mapper.NewBatchFn(n, nil)
60 | b.StopTimer()
61 | world.RemoveEntities(filter.Batch(), nil)
62 | b.StartTimer()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/bench/create10comp/donburi.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | )
10 |
11 | func runDonburi(b *testing.B, n int) {
12 | allIDs := []component.IComponentType{
13 | donburi.NewComponentType[comps.C1](),
14 | donburi.NewComponentType[comps.C2](),
15 | donburi.NewComponentType[comps.C3](),
16 | donburi.NewComponentType[comps.C4](),
17 | donburi.NewComponentType[comps.C5](),
18 | donburi.NewComponentType[comps.C6](),
19 | donburi.NewComponentType[comps.C7](),
20 | donburi.NewComponentType[comps.C8](),
21 | donburi.NewComponentType[comps.C9](),
22 | donburi.NewComponentType[comps.C10](),
23 | }
24 |
25 | world := donburi.NewWorld()
26 |
27 | entities := make([]donburi.Entity, 0, n)
28 | for range n {
29 | e := world.Create(allIDs...)
30 | entities = append(entities, e)
31 | }
32 | for _, e := range entities {
33 | world.Remove(e)
34 | }
35 | entities = entities[:0]
36 |
37 | for b.Loop() {
38 | for range n {
39 | e := world.Create(allIDs...)
40 | entities = append(entities, e)
41 | }
42 | b.StopTimer()
43 | for _, e := range entities {
44 | world.Remove(e)
45 | }
46 | entities = entities[:0]
47 | b.StartTimer()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/bench/create10comp/ggecs.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | C1ID ecs.ComponentID = iota
13 | C2ID
14 | C3ID
15 | C4ID
16 | C5ID
17 | C6ID
18 | C7ID
19 | C8ID
20 | C9ID
21 | C10ID
22 | )
23 |
24 | func runGGEcs(b *testing.B, n int) {
25 | world := ecs.NewWorld(1024)
26 | world.Register(ecs.NewComponentRegistry[comps.C1](C1ID))
27 | world.Register(ecs.NewComponentRegistry[comps.C2](C2ID))
28 | world.Register(ecs.NewComponentRegistry[comps.C3](C3ID))
29 | world.Register(ecs.NewComponentRegistry[comps.C4](C4ID))
30 | world.Register(ecs.NewComponentRegistry[comps.C5](C5ID))
31 | world.Register(ecs.NewComponentRegistry[comps.C6](C6ID))
32 | world.Register(ecs.NewComponentRegistry[comps.C7](C7ID))
33 | world.Register(ecs.NewComponentRegistry[comps.C8](C8ID))
34 | world.Register(ecs.NewComponentRegistry[comps.C9](C9ID))
35 | world.Register(ecs.NewComponentRegistry[comps.C10](C10ID))
36 |
37 | allIDs := []ecs.ComponentID{
38 | C1ID, C2ID, C3ID, C4ID, C5ID,
39 | C6ID, C7ID, C8ID, C9ID, C10ID,
40 | }
41 |
42 | entities := make([]ecs.EntityID, 0, n)
43 | for range n {
44 | e := world.NewEntity(allIDs...)
45 | entities = append(entities, e)
46 | }
47 | for _, e := range entities {
48 | world.RemEntity(e)
49 | }
50 | entities = entities[:0]
51 |
52 | for b.Loop() {
53 | for range n {
54 | e := world.NewEntity(allIDs...)
55 | entities = append(entities, e)
56 | }
57 | b.StopTimer()
58 | for _, e := range entities {
59 | world.RemEntity(e)
60 | }
61 | entities = entities[:0]
62 | b.StartTimer()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/bench/create10comp/run.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/create10comp/uot.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | allIDs := []ecs.Component{
14 | ecs.C(comps.C1{}),
15 | ecs.C(comps.C2{}),
16 | ecs.C(comps.C3{}),
17 | ecs.C(comps.C4{}),
18 | ecs.C(comps.C5{}),
19 | ecs.C(comps.C6{}),
20 | ecs.C(comps.C7{}),
21 | ecs.C(comps.C8{}),
22 | ecs.C(comps.C9{}),
23 | ecs.C(comps.C10{}),
24 | }
25 |
26 | entities := make([]ecs.Id, 0, n)
27 | for range n {
28 | id := world.NewId()
29 | ecs.Write(world, id, allIDs...)
30 | entities = append(entities, id)
31 | }
32 | for _, e := range entities {
33 | ecs.Delete(world, e)
34 | }
35 | entities = entities[:0]
36 |
37 | for b.Loop() {
38 | for range n {
39 | id := world.NewId()
40 | ecs.Write(world, id, allIDs...)
41 | entities = append(entities, id)
42 | }
43 | b.StopTimer()
44 | for _, e := range entities {
45 | ecs.Delete(world, e)
46 | }
47 | entities = entities[:0]
48 | b.StartTimer()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/bench/create10comp/volt.go:
--------------------------------------------------------------------------------
1 | package create10comp
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.C1](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.C2](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 | volt.RegisterComponent[comps.C3](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
19 | volt.RegisterComponent[comps.C4](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
20 | volt.RegisterComponent[comps.C5](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
21 | volt.RegisterComponent[comps.C6](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
22 | volt.RegisterComponent[comps.C7](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
23 | volt.RegisterComponent[comps.C8](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
24 | volt.RegisterComponent[comps.C9](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
25 | volt.RegisterComponent[comps.C10](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
26 |
27 | entities := make([]volt.EntityId, 0, n)
28 | for id := range n {
29 | e, err := volt.CreateEntityWithComponents8(world, strconv.Itoa(id),
30 | comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{},
31 | comps.C5{}, comps.C6{}, comps.C7{}, comps.C8{},
32 | )
33 | if err != nil {
34 | panic("Volt crashed")
35 | }
36 | volt.AddComponents2(world, e, comps.C9{}, comps.C10{})
37 | entities = append(entities, e)
38 | }
39 | for _, e := range entities {
40 | world.RemoveEntity(e)
41 | }
42 | entities = entities[:0]
43 |
44 | for b.Loop() {
45 | for id := range n {
46 | e, err := volt.CreateEntityWithComponents8(world, strconv.Itoa(id),
47 | comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{},
48 | comps.C5{}, comps.C6{}, comps.C7{}, comps.C8{},
49 | )
50 | if err != nil {
51 | panic("Volt crashed")
52 | }
53 | volt.AddComponents2(world, e, comps.C9{}, comps.C10{})
54 | entities = append(entities, e)
55 | }
56 | b.StopTimer()
57 | for _, e := range entities {
58 | world.RemoveEntity(e)
59 | }
60 | entities = entities[:0]
61 | b.StartTimer()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/bench/create2comp/arche.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | ids := []ecs.ID{posID, velID}
16 |
17 | ecs.NewBuilder(&world, ids...).NewBatch(n)
18 | world.Batch().RemoveEntities(ecs.All(ids...))
19 |
20 | entities := make([]ecs.Entity, 0, n)
21 |
22 | for b.Loop() {
23 | for range n {
24 | e := world.NewEntity(ids...)
25 | // Just for fairness, because the others need to do that, too.
26 | entities = append(entities, e)
27 | }
28 | b.StopTimer()
29 |
30 | if n < 64 {
31 | // Speed up cleanup for low entity counts
32 | for i := len(entities) - 1; i >= 0; i-- {
33 | world.RemoveEntity(entities[i])
34 | }
35 | } else {
36 | world.Batch().RemoveEntities(ecs.All(ids...))
37 | }
38 |
39 | entities = entities[:0]
40 | b.StartTimer()
41 | }
42 | }
43 |
44 | func runArcheBatched(b *testing.B, n int) {
45 | world := ecs.NewWorld(1024)
46 |
47 | posID := ecs.ComponentID[comps.Position](&world)
48 | velID := ecs.ComponentID[comps.Velocity](&world)
49 | ids := []ecs.ID{posID, velID}
50 |
51 | ecs.NewBuilder(&world, ids...).NewBatch(n)
52 | world.Batch().RemoveEntities(ecs.All(ids...))
53 |
54 | for b.Loop() {
55 | ecs.NewBuilder(&world, ids...).NewBatch(n)
56 | b.StopTimer()
57 | world.Batch().RemoveEntities(ecs.All(ids...))
58 | b.StartTimer()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/bench/create2comp/ark.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
14 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
15 |
16 | mapper.NewBatchFn(n, nil)
17 | world.RemoveEntities(filter.Batch(), nil)
18 |
19 | entities := make([]ecs.Entity, 0, n)
20 |
21 | for b.Loop() {
22 | for range n {
23 | e := mapper.NewEntityFn(nil)
24 | // Just for fairness, because the others need to do that, too.
25 | entities = append(entities, e)
26 | }
27 | b.StopTimer()
28 |
29 | if n < 64 {
30 | // Speed up cleanup for low entity counts
31 | for i := len(entities) - 1; i >= 0; i-- {
32 | world.RemoveEntity(entities[i])
33 | }
34 | } else {
35 | world.RemoveEntities(filter.Batch(), nil)
36 | }
37 |
38 | entities = entities[:0]
39 | b.StartTimer()
40 | }
41 | }
42 |
43 | func runArkBatched(b *testing.B, n int) {
44 | world := ecs.NewWorld(1024)
45 |
46 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
47 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
48 |
49 | mapper.NewBatchFn(n, nil)
50 | world.RemoveEntities(filter.Batch(), nil)
51 |
52 | for b.Loop() {
53 | mapper.NewBatchFn(n, nil)
54 | b.StopTimer()
55 | world.RemoveEntities(filter.Batch(), nil)
56 | b.StartTimer()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/bench/create2comp/donburi.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | )
10 |
11 | func runDonburi(b *testing.B, n int) {
12 | allIDs := []component.IComponentType{
13 | donburi.NewComponentType[comps.Position](),
14 | donburi.NewComponentType[comps.Velocity](),
15 | }
16 |
17 | world := donburi.NewWorld()
18 |
19 | entities := make([]donburi.Entity, 0, n)
20 | for range n {
21 | e := world.Create(allIDs...)
22 | entities = append(entities, e)
23 | }
24 | for _, e := range entities {
25 | world.Remove(e)
26 | }
27 | entities = entities[:0]
28 |
29 | for b.Loop() {
30 | for range n {
31 | e := world.Create(allIDs...)
32 | entities = append(entities, e)
33 | }
34 | b.StopTimer()
35 | for _, e := range entities {
36 | world.Remove(e)
37 | }
38 | entities = entities[:0]
39 | b.StartTimer()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/bench/create2comp/ggecs.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | )
15 |
16 | func runGGEcs(b *testing.B, n int) {
17 | world := ecs.NewWorld(1024)
18 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
19 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
20 |
21 | allIDs := []ecs.ComponentID{
22 | PositionComponentID,
23 | VelocityComponentID,
24 | }
25 |
26 | entities := make([]ecs.EntityID, 0, n)
27 | for range n {
28 | e := world.NewEntity(allIDs...)
29 | entities = append(entities, e)
30 | }
31 | for _, e := range entities {
32 | world.RemEntity(e)
33 | }
34 | entities = entities[:0]
35 |
36 | for b.Loop() {
37 | for range n {
38 | e := world.NewEntity(allIDs...)
39 | entities = append(entities, e)
40 | }
41 | b.StopTimer()
42 | for _, e := range entities {
43 | world.RemEntity(e)
44 | }
45 | entities = entities[:0]
46 | b.StartTimer()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/bench/create2comp/run.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/create2comp/uot.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | allIDs := []ecs.Component{
14 | ecs.C(comps.Position{}),
15 | ecs.C(comps.Velocity{}),
16 | }
17 |
18 | entities := make([]ecs.Id, 0, n)
19 | for range n {
20 | id := world.NewId()
21 | ecs.Write(world, id, allIDs...)
22 | entities = append(entities, id)
23 | }
24 | for _, e := range entities {
25 | ecs.Delete(world, e)
26 | }
27 | entities = entities[:0]
28 |
29 | for b.Loop() {
30 | for range n {
31 | id := world.NewId()
32 | ecs.Write(world, id, allIDs...)
33 | entities = append(entities, id)
34 | }
35 | b.StopTimer()
36 | for _, e := range entities {
37 | ecs.Delete(world, e)
38 | }
39 | entities = entities[:0]
40 | b.StartTimer()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/bench/create2comp/volt.go:
--------------------------------------------------------------------------------
1 | package create2comp
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.Velocity](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 |
19 | entities := make([]volt.EntityId, 0, n)
20 | for id := range n {
21 | e, err := volt.CreateEntityWithComponents2(world, strconv.Itoa(id), comps.Position{}, comps.Velocity{})
22 | if err != nil {
23 | panic("Volt crashed")
24 | }
25 | entities = append(entities, e)
26 | }
27 | for _, e := range entities {
28 | world.RemoveEntity(e)
29 | }
30 | entities = entities[:0]
31 |
32 | for b.Loop() {
33 | for id := range n {
34 | e, err := volt.CreateEntityWithComponents2(world, strconv.Itoa(id), comps.Position{}, comps.Velocity{})
35 | if err != nil {
36 | panic("Volt crashed")
37 | }
38 | entities = append(entities, e)
39 | }
40 | b.StopTimer()
41 | for _, e := range entities {
42 | world.RemoveEntity(e)
43 | }
44 | entities = entities[:0]
45 | b.StartTimer()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/arche.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | for b.Loop() {
12 | b.StopTimer()
13 | world := ecs.NewWorld(1024)
14 |
15 | posID := ecs.ComponentID[comps.Position](&world)
16 | velID := ecs.ComponentID[comps.Velocity](&world)
17 | ids := []ecs.ID{posID, velID}
18 |
19 | b.StartTimer()
20 | for range n {
21 | world.NewEntity(ids...)
22 | }
23 | }
24 | }
25 |
26 | func runArcheBatched(b *testing.B, n int) {
27 | for b.Loop() {
28 | b.StopTimer()
29 | world := ecs.NewWorld(1024)
30 |
31 | posID := ecs.ComponentID[comps.Position](&world)
32 | velID := ecs.ComponentID[comps.Velocity](&world)
33 | ids := []ecs.ID{posID, velID}
34 |
35 | b.StartTimer()
36 | ecs.NewBuilder(&world, ids...).NewBatch(n)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/ark.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | for b.Loop() {
12 | b.StopTimer()
13 | world := ecs.NewWorld(1024)
14 |
15 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
16 |
17 | b.StartTimer()
18 | for range n {
19 | mapper.NewEntityFn(nil)
20 | }
21 | }
22 | }
23 |
24 | func runArkBatched(b *testing.B, n int) {
25 | for b.Loop() {
26 | b.StopTimer()
27 | world := ecs.NewWorld(1024)
28 |
29 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
30 |
31 | b.StartTimer()
32 | mapper.NewBatchFn(n, nil)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/donburi.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | )
10 |
11 | func runDonburi(b *testing.B, n int) {
12 | allIDs := []component.IComponentType{
13 | donburi.NewComponentType[comps.Position](),
14 | donburi.NewComponentType[comps.Velocity](),
15 | }
16 |
17 | for b.Loop() {
18 | b.StopTimer()
19 | world := donburi.NewWorld()
20 |
21 | b.StartTimer()
22 | for range n {
23 | world.Create(allIDs...)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/ggecs.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | )
15 |
16 | func runGGEcs(b *testing.B, n int) {
17 | for b.Loop() {
18 | b.StopTimer()
19 | world := ecs.NewWorld(1024)
20 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
21 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
22 |
23 | allIDs := []ecs.ComponentID{
24 | PositionComponentID,
25 | VelocityComponentID,
26 | }
27 |
28 | b.StartTimer()
29 | for range n {
30 | _ = world.NewEntity(allIDs...)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/run.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/uot.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | for b.Loop() {
12 | b.StopTimer()
13 | world := ecs.NewWorld()
14 |
15 | allIDs := []ecs.Component{
16 | ecs.C(comps.Position{}),
17 | ecs.C(comps.Velocity{}),
18 | }
19 |
20 | b.StartTimer()
21 | for range n {
22 | id := world.NewId()
23 | ecs.Write(world, id, allIDs...)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/bench/create2comp_alloc/volt.go:
--------------------------------------------------------------------------------
1 | package create2compalloc
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | for b.Loop() {
15 | b.StopTimer()
16 | world := volt.CreateWorld(1024)
17 |
18 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
19 | volt.RegisterComponent[comps.Velocity](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
20 |
21 | b.StartTimer()
22 | for id := range n {
23 | _, err := volt.CreateEntityWithComponents2(world, strconv.Itoa(id), comps.Position{}, comps.Velocity{})
24 | if err != nil {
25 | panic("Volt crashed")
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/bench/delete10comp/arche.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | ids := []ecs.ID{
14 | ecs.ComponentID[comps.C1](&world),
15 | ecs.ComponentID[comps.C2](&world),
16 | ecs.ComponentID[comps.C3](&world),
17 | ecs.ComponentID[comps.C4](&world),
18 | ecs.ComponentID[comps.C5](&world),
19 | ecs.ComponentID[comps.C6](&world),
20 | ecs.ComponentID[comps.C7](&world),
21 | ecs.ComponentID[comps.C8](&world),
22 | ecs.ComponentID[comps.C9](&world),
23 | ecs.ComponentID[comps.C10](&world),
24 | }
25 |
26 | entities := make([]ecs.Entity, 0, n)
27 |
28 | query := world.Batch().NewQ(n, ids...)
29 | for query.Next() {
30 | entities = append(entities, query.Entity())
31 | }
32 |
33 | for b.Loop() {
34 | for _, e := range entities {
35 | world.RemoveEntity(e)
36 | }
37 | b.StopTimer()
38 | entities = entities[:0]
39 |
40 | if n < 64 {
41 | // Speed up entity creation for low entity counts
42 | for range n {
43 | e := world.NewEntity(ids...)
44 | entities = append(entities, e)
45 | }
46 | } else {
47 | query := world.Batch().NewQ(n, ids...)
48 | for query.Next() {
49 | entities = append(entities, query.Entity())
50 | }
51 | }
52 | b.StartTimer()
53 | }
54 | }
55 |
56 | func runArcheBatched(b *testing.B, n int) {
57 | world := ecs.NewWorld(1024)
58 |
59 | posID := ecs.ComponentID[comps.Position](&world)
60 | velID := ecs.ComponentID[comps.Velocity](&world)
61 | ids := []ecs.ID{posID, velID}
62 |
63 | filter := ecs.All(ids...)
64 |
65 | world.Batch().New(n, ids...)
66 |
67 | for b.Loop() {
68 | world.Batch().RemoveEntities(&filter)
69 | b.StopTimer()
70 | world.Batch().New(n, ids...)
71 | b.StartTimer()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/bench/delete10comp/ark.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | mapper := ecs.NewMap10[
14 | comps.C1, comps.C2, comps.C3, comps.C4, comps.C5,
15 | comps.C6, comps.C7, comps.C8, comps.C9, comps.C10,
16 | ](&world)
17 | filter := ecs.NewFilter0(&world)
18 |
19 | entities := make([]ecs.Entity, 0, n)
20 |
21 | mapper.NewBatchFn(n, nil)
22 | query := filter.Query()
23 | for query.Next() {
24 | entities = append(entities, query.Entity())
25 | }
26 |
27 | for b.Loop() {
28 | for _, e := range entities {
29 | world.RemoveEntity(e)
30 | }
31 | b.StopTimer()
32 | entities = entities[:0]
33 |
34 | if n < 64 {
35 | // Speed up entity creation for low entity counts
36 | for range n {
37 | e := mapper.NewEntityFn(nil)
38 | entities = append(entities, e)
39 | }
40 | } else {
41 | mapper.NewBatchFn(n, nil)
42 | query := filter.Query()
43 | for query.Next() {
44 | entities = append(entities, query.Entity())
45 | }
46 | }
47 | b.StartTimer()
48 | }
49 | }
50 |
51 | func runArkBatched(b *testing.B, n int) {
52 | world := ecs.NewWorld(1024)
53 |
54 | mapper := ecs.NewMap10[
55 | comps.C1, comps.C2, comps.C3, comps.C4, comps.C5,
56 | comps.C6, comps.C7, comps.C8, comps.C9, comps.C10,
57 | ](&world)
58 | filter := ecs.NewFilter0(&world)
59 |
60 | mapper.NewBatchFn(n, nil)
61 |
62 | for b.Loop() {
63 | world.RemoveEntities(filter.Batch(), nil)
64 | b.StopTimer()
65 | mapper.NewBatchFn(n, nil)
66 | b.StartTimer()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/bench/delete10comp/donburi.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | )
10 |
11 | func runDonburi(b *testing.B, n int) {
12 | allIDs := []component.IComponentType{
13 | donburi.NewComponentType[comps.C1](),
14 | donburi.NewComponentType[comps.C2](),
15 | donburi.NewComponentType[comps.C3](),
16 | donburi.NewComponentType[comps.C4](),
17 | donburi.NewComponentType[comps.C5](),
18 | donburi.NewComponentType[comps.C6](),
19 | donburi.NewComponentType[comps.C7](),
20 | donburi.NewComponentType[comps.C8](),
21 | donburi.NewComponentType[comps.C9](),
22 | donburi.NewComponentType[comps.C10](),
23 | }
24 |
25 | world := donburi.NewWorld()
26 |
27 | entities := make([]donburi.Entity, 0, n)
28 | for range n {
29 | e := world.Create(allIDs...)
30 | entities = append(entities, e)
31 | }
32 |
33 | for b.Loop() {
34 | for _, e := range entities {
35 | world.Remove(e)
36 | }
37 | b.StopTimer()
38 | entities = entities[:0]
39 | for range n {
40 | e := world.Create(allIDs...)
41 | entities = append(entities, e)
42 | }
43 | b.StartTimer()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/bench/delete10comp/ggecs.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | C1ID ecs.ComponentID = iota
13 | C2ID
14 | C3ID
15 | C4ID
16 | C5ID
17 | C6ID
18 | C7ID
19 | C8ID
20 | C9ID
21 | C10ID
22 | )
23 |
24 | func runGGEcs(b *testing.B, n int) {
25 | world := ecs.NewWorld(1024)
26 | world.Register(ecs.NewComponentRegistry[comps.C1](C1ID))
27 | world.Register(ecs.NewComponentRegistry[comps.C2](C2ID))
28 | world.Register(ecs.NewComponentRegistry[comps.C3](C3ID))
29 | world.Register(ecs.NewComponentRegistry[comps.C4](C4ID))
30 | world.Register(ecs.NewComponentRegistry[comps.C5](C5ID))
31 | world.Register(ecs.NewComponentRegistry[comps.C6](C6ID))
32 | world.Register(ecs.NewComponentRegistry[comps.C7](C7ID))
33 | world.Register(ecs.NewComponentRegistry[comps.C8](C8ID))
34 | world.Register(ecs.NewComponentRegistry[comps.C9](C9ID))
35 | world.Register(ecs.NewComponentRegistry[comps.C10](C10ID))
36 |
37 | allIDs := []ecs.ComponentID{
38 | C1ID, C2ID, C3ID, C4ID, C5ID,
39 | C6ID, C7ID, C8ID, C9ID, C10ID,
40 | }
41 |
42 | entities := make([]ecs.EntityID, 0, n)
43 | for range n {
44 | e := world.NewEntity(allIDs...)
45 | entities = append(entities, e)
46 | }
47 |
48 | for b.Loop() {
49 | for _, e := range entities {
50 | world.RemEntity(e)
51 | }
52 | b.StopTimer()
53 | entities = entities[:0]
54 | for range n {
55 | e := world.NewEntity(allIDs...)
56 | entities = append(entities, e)
57 | }
58 | b.StartTimer()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/bench/delete10comp/run.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/delete10comp/uot.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | allIDs := []ecs.Component{
14 | ecs.C(comps.C1{}),
15 | ecs.C(comps.C2{}),
16 | ecs.C(comps.C3{}),
17 | ecs.C(comps.C4{}),
18 | ecs.C(comps.C5{}),
19 | ecs.C(comps.C6{}),
20 | ecs.C(comps.C7{}),
21 | ecs.C(comps.C8{}),
22 | ecs.C(comps.C9{}),
23 | ecs.C(comps.C10{}),
24 | }
25 |
26 | entities := make([]ecs.Id, 0, n)
27 | for range n {
28 | id := world.NewId()
29 | ecs.Write(world, id, allIDs...)
30 | entities = append(entities, id)
31 | }
32 |
33 | for b.Loop() {
34 | for _, e := range entities {
35 | ecs.Delete(world, e)
36 | }
37 | b.StopTimer()
38 | entities = entities[:0]
39 | for range n {
40 | id := world.NewId()
41 | ecs.Write(world, id, allIDs...)
42 | entities = append(entities, id)
43 | }
44 | b.StartTimer()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/bench/delete10comp/volt.go:
--------------------------------------------------------------------------------
1 | package delete10comp
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.C1](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.C2](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 | volt.RegisterComponent[comps.C3](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
19 | volt.RegisterComponent[comps.C4](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
20 | volt.RegisterComponent[comps.C5](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
21 | volt.RegisterComponent[comps.C6](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
22 | volt.RegisterComponent[comps.C7](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
23 | volt.RegisterComponent[comps.C8](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
24 | volt.RegisterComponent[comps.C9](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
25 | volt.RegisterComponent[comps.C10](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
26 |
27 | entities := make([]volt.EntityId, 0, n)
28 | for id := range n {
29 | e, err := volt.CreateEntityWithComponents8(world, strconv.Itoa(id),
30 | comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{},
31 | comps.C5{}, comps.C6{}, comps.C7{}, comps.C8{},
32 | )
33 | if err != nil {
34 | panic("Volt crashed")
35 | }
36 | volt.AddComponents2(world, e, comps.C9{}, comps.C10{})
37 | entities = append(entities, e)
38 | }
39 |
40 | for b.Loop() {
41 | for _, e := range entities {
42 | world.RemoveEntity(e)
43 | }
44 | b.StopTimer()
45 | entities = entities[:0]
46 | for id := range n {
47 | e, err := volt.CreateEntityWithComponents8(world, strconv.Itoa(id),
48 | comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{},
49 | comps.C5{}, comps.C6{}, comps.C7{}, comps.C8{},
50 | )
51 | if err != nil {
52 | panic("Volt crashed")
53 | }
54 | volt.AddComponents2(world, e, comps.C9{}, comps.C10{})
55 | entities = append(entities, e)
56 | }
57 | b.StartTimer()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/bench/delete2comp/arche.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | ids := []ecs.ID{posID, velID}
16 |
17 | entities := make([]ecs.Entity, 0, n)
18 |
19 | query := world.Batch().NewQ(n, ids...)
20 | for query.Next() {
21 | entities = append(entities, query.Entity())
22 | }
23 |
24 | for b.Loop() {
25 | for _, e := range entities {
26 | world.RemoveEntity(e)
27 | }
28 | b.StopTimer()
29 |
30 | entities = entities[:0]
31 |
32 | if n < 64 {
33 | // Speed up entity creation for low entity counts
34 | for range n {
35 | e := world.NewEntity(ids...)
36 | entities = append(entities, e)
37 | }
38 | } else {
39 | query := world.Batch().NewQ(n, ids...)
40 | for query.Next() {
41 | entities = append(entities, query.Entity())
42 | }
43 | }
44 | b.StartTimer()
45 | }
46 | }
47 |
48 | func runArcheBatched(b *testing.B, n int) {
49 | world := ecs.NewWorld(1024)
50 |
51 | posID := ecs.ComponentID[comps.Position](&world)
52 | velID := ecs.ComponentID[comps.Velocity](&world)
53 | ids := []ecs.ID{posID, velID}
54 |
55 | filter := ecs.All(ids...)
56 |
57 | world.Batch().New(n, ids...)
58 |
59 | for b.Loop() {
60 | world.Batch().RemoveEntities(&filter)
61 | b.StopTimer()
62 | world.Batch().New(n, ids...)
63 | b.StartTimer()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/bench/delete2comp/ark.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
14 |
15 | entities := make([]ecs.Entity, 0, n)
16 | mapper.NewBatchFn(n, func(entity ecs.Entity, _ *comps.Position, _ *comps.Velocity) {
17 | entities = append(entities, entity)
18 | })
19 |
20 | for b.Loop() {
21 | for _, e := range entities {
22 | world.RemoveEntity(e)
23 | }
24 | b.StopTimer()
25 |
26 | entities = entities[:0]
27 |
28 | if n < 64 {
29 | // Speed up entity creation for low entity counts
30 | for range n {
31 | e := mapper.NewEntityFn(nil)
32 | entities = append(entities, e)
33 | }
34 | } else {
35 | mapper.NewBatchFn(n, func(entity ecs.Entity, _ *comps.Position, _ *comps.Velocity) {
36 | entities = append(entities, entity)
37 | })
38 | }
39 | b.StartTimer()
40 | }
41 | }
42 |
43 | func runArkBatched(b *testing.B, n int) {
44 | world := ecs.NewWorld(1024)
45 |
46 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
47 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
48 |
49 | mapper.NewBatchFn(n, nil)
50 |
51 | for b.Loop() {
52 | world.RemoveEntities(filter.Batch(), nil)
53 | b.StopTimer()
54 | mapper.NewBatchFn(n, nil)
55 | b.StartTimer()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/bench/delete2comp/donburi.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | )
9 |
10 | func runDonburi(b *testing.B, n int) {
11 | var position = donburi.NewComponentType[comps.Position]()
12 | var velocity = donburi.NewComponentType[comps.Velocity]()
13 |
14 | world := donburi.NewWorld()
15 |
16 | entities := make([]donburi.Entity, 0, n)
17 | for range n {
18 | e := world.Create(position, velocity)
19 | entities = append(entities, e)
20 | }
21 |
22 | for b.Loop() {
23 | for _, e := range entities {
24 | world.Remove(e)
25 | }
26 | b.StopTimer()
27 |
28 | entities = entities[:0]
29 | for range n {
30 | e := world.Create(position, velocity)
31 | entities = append(entities, e)
32 | }
33 | b.StartTimer()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/bench/delete2comp/ggecs.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | )
15 |
16 | func runGGEcs(b *testing.B, n int) {
17 | world := ecs.NewWorld(1024)
18 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
19 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
20 |
21 | entities := make([]ecs.EntityID, 0, n)
22 | for range n {
23 | e := world.NewEntity(PositionComponentID, VelocityComponentID)
24 | entities = append(entities, e)
25 | }
26 |
27 | for b.Loop() {
28 | for _, e := range entities {
29 | world.RemEntity(e)
30 | }
31 | b.StopTimer()
32 |
33 | entities = entities[:0]
34 | for range n {
35 | e := world.NewEntity(PositionComponentID, VelocityComponentID)
36 | entities = append(entities, e)
37 | }
38 | b.StartTimer()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/bench/delete2comp/run.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (batch)", F: runArcheBatched},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (batch)", F: runArkBatched},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/delete2comp/uot.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | entities := make([]ecs.Id, 0, n)
14 | for range n {
15 | id := world.NewId()
16 | ecs.Write(world, id,
17 | ecs.C(comps.Position{}),
18 | ecs.C(comps.Velocity{}),
19 | )
20 | entities = append(entities, id)
21 | }
22 |
23 | for b.Loop() {
24 | for _, e := range entities {
25 | ecs.Delete(world, e)
26 | }
27 | b.StopTimer()
28 |
29 | entities = entities[:0]
30 | for range n {
31 | id := world.NewId()
32 | ecs.Write(world, id,
33 | ecs.C(comps.Position{}),
34 | ecs.C(comps.Velocity{}),
35 | )
36 | entities = append(entities, id)
37 | }
38 | b.StartTimer()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/bench/delete2comp/volt.go:
--------------------------------------------------------------------------------
1 | package delete2comp
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.Velocity](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 |
19 | entities := make([]volt.EntityId, 0, n)
20 | for id := range n {
21 | e, err := volt.CreateEntityWithComponents2(world, strconv.Itoa(id), comps.Position{}, comps.Velocity{})
22 | if err != nil {
23 | panic("Volt crashed")
24 | }
25 | entities = append(entities, e)
26 | }
27 |
28 | for b.Loop() {
29 | for _, e := range entities {
30 | world.RemoveEntity(e)
31 | }
32 | b.StopTimer()
33 | entities = entities[:0]
34 | for id := range n {
35 | e, err := volt.CreateEntityWithComponents2(world, strconv.Itoa(id), comps.Position{}, comps.Velocity{})
36 | if err != nil {
37 | panic("Volt crashed")
38 | }
39 | entities = append(entities, e)
40 | }
41 | b.StartTimer()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/bench/new_world/arche.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | var world ecs.World
12 | for b.Loop() {
13 | world = ecs.NewWorld()
14 | }
15 | assert.False(b, world.IsLocked())
16 | }
17 |
--------------------------------------------------------------------------------
/bench/new_world/ark.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | var world ecs.World
12 | for b.Loop() {
13 | world = ecs.NewWorld()
14 | }
15 | assert.False(b, world.IsLocked())
16 | }
17 |
--------------------------------------------------------------------------------
/bench/new_world/donburi.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/yohamta/donburi"
8 | )
9 |
10 | func runDonburi(b *testing.B, n int) {
11 | var world donburi.World
12 | for b.Loop() {
13 | world = donburi.NewWorld()
14 | }
15 | assert.Equal(b, 0, world.Len())
16 | }
17 |
--------------------------------------------------------------------------------
/bench/new_world/ggecs.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func runGGEcs(b *testing.B, n int) {
11 | var world ecs.World
12 | for b.Loop() {
13 | world = ecs.NewWorld(1024)
14 | }
15 | e := world.NewEntity()
16 | assert.True(b, world.IsAlive(e))
17 | }
18 |
--------------------------------------------------------------------------------
/bench/new_world/run.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Ark", F: runArk},
13 | {Name: "Donburi", F: runDonburi},
14 | {Name: "ggecs", F: runGGEcs},
15 | {Name: "uot", F: runUot},
16 | {Name: "Volt", F: runVolt},
17 | },
18 | N: []int{
19 | 1,
20 | },
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/bench/new_world/uot.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | var world *ecs.World
12 | for b.Loop() {
13 | world = ecs.NewWorld()
14 | }
15 | assert.NotNil(b, world)
16 | }
17 |
--------------------------------------------------------------------------------
/bench/new_world/volt.go:
--------------------------------------------------------------------------------
1 | package newworld
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/akmonengine/volt"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
11 |
12 | func runVolt(b *testing.B, n int) {
13 | var world *volt.World
14 | for b.Loop() {
15 | world = volt.CreateWorld(1024)
16 | }
17 | assert.NotNil(b, world)
18 | }
19 |
--------------------------------------------------------------------------------
/bench/query256arch/arche.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | c1ID := ecs.ComponentID[comps.C1](&world)
16 | c2ID := ecs.ComponentID[comps.C2](&world)
17 | c3ID := ecs.ComponentID[comps.C3](&world)
18 | c4ID := ecs.ComponentID[comps.C4](&world)
19 | c5ID := ecs.ComponentID[comps.C5](&world)
20 | c6ID := ecs.ComponentID[comps.C6](&world)
21 | c7ID := ecs.ComponentID[comps.C7](&world)
22 | c8ID := ecs.ComponentID[comps.C8](&world)
23 |
24 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID, c6ID, c7ID, c8ID}
25 |
26 | world.Batch().New(n, posID, velID)
27 |
28 | ids := []ecs.ID{}
29 | for i := range n * 4 {
30 | ids = append(ids, posID)
31 | for j, id := range extraIDs {
32 | m := 1 << j
33 | if i&m == m {
34 | ids = append(ids, id)
35 | }
36 | }
37 | world.NewEntity(ids...)
38 |
39 | ids = ids[:0]
40 | }
41 |
42 | filter := ecs.All(posID, velID)
43 |
44 | for b.Loop() {
45 | query := world.Query(&filter)
46 | for query.Next() {
47 | pos := (*comps.Position)(query.Get(posID))
48 | vel := (*comps.Velocity)(query.Get(velID))
49 | pos.X += vel.X
50 | pos.Y += vel.Y
51 | }
52 | }
53 | }
54 |
55 | func runArcheRegistered(b *testing.B, n int) {
56 | world := ecs.NewWorld(1024)
57 |
58 | posID := ecs.ComponentID[comps.Position](&world)
59 | velID := ecs.ComponentID[comps.Velocity](&world)
60 | c1ID := ecs.ComponentID[comps.C1](&world)
61 | c2ID := ecs.ComponentID[comps.C2](&world)
62 | c3ID := ecs.ComponentID[comps.C3](&world)
63 | c4ID := ecs.ComponentID[comps.C4](&world)
64 | c5ID := ecs.ComponentID[comps.C5](&world)
65 | c6ID := ecs.ComponentID[comps.C6](&world)
66 | c7ID := ecs.ComponentID[comps.C7](&world)
67 | c8ID := ecs.ComponentID[comps.C8](&world)
68 |
69 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID, c6ID, c7ID, c8ID}
70 |
71 | world.Batch().New(n, posID, velID)
72 |
73 | ids := []ecs.ID{}
74 | for i := range n * 4 {
75 | ids = append(ids, posID)
76 | for j, id := range extraIDs {
77 | m := 1 << j
78 | if i&m == m {
79 | ids = append(ids, id)
80 | }
81 | }
82 | world.NewEntity(ids...)
83 |
84 | ids = ids[:0]
85 | }
86 |
87 | filter := ecs.All(posID, velID)
88 | cf := world.Cache().Register(&filter)
89 |
90 | for b.Loop() {
91 | query := world.Query(&cf)
92 | for query.Next() {
93 | pos := (*comps.Position)(query.Get(posID))
94 | vel := (*comps.Velocity)(query.Get(velID))
95 | pos.X += vel.X
96 | pos.Y += vel.Y
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/bench/query256arch/ark.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | _ = ecs.ComponentID[comps.Velocity](&world)
15 | c1ID := ecs.ComponentID[comps.C1](&world)
16 | c2ID := ecs.ComponentID[comps.C2](&world)
17 | c3ID := ecs.ComponentID[comps.C3](&world)
18 | c4ID := ecs.ComponentID[comps.C4](&world)
19 | c5ID := ecs.ComponentID[comps.C5](&world)
20 | c6ID := ecs.ComponentID[comps.C6](&world)
21 | c7ID := ecs.ComponentID[comps.C7](&world)
22 | c8ID := ecs.ComponentID[comps.C8](&world)
23 |
24 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID, c6ID, c7ID, c8ID}
25 |
26 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
27 | mapper.NewBatchFn(n, nil)
28 |
29 | ids := []ecs.ID{}
30 | for i := range n * 4 {
31 | ids = append(ids, posID)
32 | for j, id := range extraIDs {
33 | m := 1 << j
34 | if i&m == m {
35 | ids = append(ids, id)
36 | }
37 | }
38 | world.Unsafe().NewEntity(ids...)
39 |
40 | ids = ids[:0]
41 | }
42 |
43 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
44 |
45 | for b.Loop() {
46 | query := filter.Query()
47 | for query.Next() {
48 | pos, vel := query.Get()
49 | pos.X += vel.X
50 | pos.Y += vel.Y
51 | }
52 | }
53 | }
54 |
55 | func runArkRegistered(b *testing.B, n int) {
56 | world := ecs.NewWorld(1024)
57 |
58 | posID := ecs.ComponentID[comps.Position](&world)
59 | _ = ecs.ComponentID[comps.Velocity](&world)
60 | c1ID := ecs.ComponentID[comps.C1](&world)
61 | c2ID := ecs.ComponentID[comps.C2](&world)
62 | c3ID := ecs.ComponentID[comps.C3](&world)
63 | c4ID := ecs.ComponentID[comps.C4](&world)
64 | c5ID := ecs.ComponentID[comps.C5](&world)
65 | c6ID := ecs.ComponentID[comps.C6](&world)
66 | c7ID := ecs.ComponentID[comps.C7](&world)
67 | c8ID := ecs.ComponentID[comps.C8](&world)
68 |
69 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID, c6ID, c7ID, c8ID}
70 |
71 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
72 | mapper.NewBatchFn(n, nil)
73 |
74 | ids := []ecs.ID{}
75 | for i := range n * 4 {
76 | ids = append(ids, posID)
77 | for j, id := range extraIDs {
78 | m := 1 << j
79 | if i&m == m {
80 | ids = append(ids, id)
81 | }
82 | }
83 | world.Unsafe().NewEntity(ids...)
84 |
85 | ids = ids[:0]
86 | }
87 |
88 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world).Register()
89 |
90 | for b.Loop() {
91 | query := filter.Query()
92 | for query.Next() {
93 | pos, vel := query.Get()
94 | pos.X += vel.X
95 | pos.Y += vel.Y
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/bench/query256arch/donburi.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | "github.com/yohamta/donburi/filter"
10 | )
11 |
12 | func runDonburi(b *testing.B, n int) {
13 | world := donburi.NewWorld()
14 |
15 | var position = donburi.NewComponentType[comps.Position]()
16 | var velocity = donburi.NewComponentType[comps.Velocity]()
17 | var c1 = donburi.NewComponentType[comps.C1]()
18 | var c2 = donburi.NewComponentType[comps.C2]()
19 | var c3 = donburi.NewComponentType[comps.C3]()
20 | var c4 = donburi.NewComponentType[comps.C4]()
21 | var c5 = donburi.NewComponentType[comps.C5]()
22 | var c6 = donburi.NewComponentType[comps.C6]()
23 | var c7 = donburi.NewComponentType[comps.C7]()
24 | var c8 = donburi.NewComponentType[comps.C8]()
25 |
26 | extraIDs := []component.IComponentType{c1, c2, c3, c4, c5, c6, c7, c8}
27 |
28 | for range n {
29 | world.Create(position, velocity)
30 | }
31 |
32 | ids := []component.IComponentType{}
33 | for i := range n * 4 {
34 | ids = append(ids, position)
35 | for j, id := range extraIDs {
36 | m := 1 << j
37 | if i&m == m {
38 | ids = append(ids, id)
39 | }
40 | }
41 | world.Create(ids...)
42 | ids = ids[:0]
43 | }
44 |
45 | query := donburi.NewQuery(filter.Contains(position, velocity))
46 |
47 | for b.Loop() {
48 | query.Each(world, func(entry *donburi.Entry) {
49 | pos := position.Get(entry)
50 | vel := velocity.Get(entry)
51 |
52 | pos.X += vel.X
53 | pos.Y += vel.Y
54 | })
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/bench/query256arch/ggecs.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | C1ID
15 | C2ID
16 | C3ID
17 | C4ID
18 | C5ID
19 | C6ID
20 | C7ID
21 | C8ID
22 | )
23 |
24 | func runGGEcs(b *testing.B, n int) {
25 | world := ecs.NewWorld(1024)
26 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
27 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
28 |
29 | world.Register(ecs.NewComponentRegistry[comps.C1](C1ID))
30 | world.Register(ecs.NewComponentRegistry[comps.C2](C2ID))
31 | world.Register(ecs.NewComponentRegistry[comps.C3](C3ID))
32 | world.Register(ecs.NewComponentRegistry[comps.C4](C4ID))
33 | world.Register(ecs.NewComponentRegistry[comps.C5](C5ID))
34 | world.Register(ecs.NewComponentRegistry[comps.C5](C6ID))
35 | world.Register(ecs.NewComponentRegistry[comps.C5](C7ID))
36 | world.Register(ecs.NewComponentRegistry[comps.C5](C8ID))
37 |
38 | extraIDs := []ecs.ComponentID{C1ID, C2ID, C3ID, C4ID, C5ID, C6ID, C7ID, C8ID}
39 |
40 | for range n {
41 | _ = world.NewEntity(PositionComponentID, VelocityComponentID)
42 | }
43 |
44 | ids := []ecs.ComponentID{}
45 | for i := range n * 4 {
46 | ids = append(ids, PositionComponentID)
47 | for j, id := range extraIDs {
48 | m := 1 << j
49 | if i&m == m {
50 | ids = append(ids, id)
51 | }
52 | }
53 | _ = world.NewEntity(ids...)
54 |
55 | ids = ids[:0]
56 | }
57 |
58 | mask := ecs.MakeComponentMask(PositionComponentID, VelocityComponentID)
59 |
60 | for b.Loop() {
61 | query := world.Query(mask)
62 | for query.Next() {
63 | pos := (*comps.Position)(query.Component(PositionComponentID))
64 | vel := (*comps.Velocity)(query.Component(VelocityComponentID))
65 | pos.X += vel.X
66 | pos.Y += vel.Y
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/bench/query256arch/run.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (cached)", F: runArcheRegistered},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (cached)", F: runArkRegistered},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/query256arch/uot.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | extraComps := []any{comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{}, comps.C5{}, comps.C6{}, comps.C7{}, comps.C8{}}
14 |
15 | for range n {
16 | id := world.NewId()
17 | ecs.Write(world, id, ecs.C(comps.Position{}), ecs.C(comps.Velocity{}))
18 | }
19 |
20 | ids := []ecs.Component{}
21 | for i := range n * 4 {
22 | ids = append(ids, ecs.C(comps.Position{}))
23 | for j, id := range extraComps {
24 | m := 1 << j
25 | if i&m == m {
26 | ids = append(ids, ecs.C(id))
27 | }
28 | }
29 |
30 | id := world.NewId()
31 | ecs.Write(world, id, ids...)
32 |
33 | ids = ids[:0]
34 | }
35 |
36 | query := ecs.Query2[comps.Position, comps.Velocity](world)
37 |
38 | for b.Loop() {
39 | query.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
40 | pos.X += vel.X
41 | pos.Y += vel.Y
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/bench/query256arch/volt.go:
--------------------------------------------------------------------------------
1 | package query256arch
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | func runVolt(b *testing.B, n int) {
12 | world := volt.CreateWorld(1024)
13 |
14 | volt.RegisterComponent[comps.Position](world, &volt.ComponentConfig[comps.Position]{BuilderFn: func(component any, configuration any) {}})
15 | volt.RegisterComponent[comps.Velocity](world, &volt.ComponentConfig[comps.Velocity]{BuilderFn: func(component any, configuration any) {}})
16 | volt.RegisterComponent[comps.C1](world, &volt.ComponentConfig[comps.C1]{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.C2](world, &volt.ComponentConfig[comps.C2]{BuilderFn: func(component any, configuration any) {}})
18 | volt.RegisterComponent[comps.C3](world, &volt.ComponentConfig[comps.C3]{BuilderFn: func(component any, configuration any) {}})
19 | volt.RegisterComponent[comps.C4](world, &volt.ComponentConfig[comps.C4]{BuilderFn: func(component any, configuration any) {}})
20 | volt.RegisterComponent[comps.C5](world, &volt.ComponentConfig[comps.C5]{BuilderFn: func(component any, configuration any) {}})
21 | volt.RegisterComponent[comps.C6](world, &volt.ComponentConfig[comps.C6]{BuilderFn: func(component any, configuration any) {}})
22 | volt.RegisterComponent[comps.C7](world, &volt.ComponentConfig[comps.C7]{BuilderFn: func(component any, configuration any) {}})
23 | volt.RegisterComponent[comps.C8](world, &volt.ComponentConfig[comps.C8]{BuilderFn: func(component any, configuration any) {}})
24 | extraComps := []volt.ComponentId{comps.C1Id, comps.C2Id, comps.C3Id, comps.C4Id, comps.C5Id, comps.C6Id, comps.C7Id, comps.C8Id}
25 |
26 | for r := range n {
27 | volt.CreateEntityWithComponents2(world, strconv.Itoa(r), comps.Position{}, comps.Velocity{})
28 | }
29 |
30 | var ids []volt.ComponentIdConf
31 | for i := range n * 4 {
32 | ids = append(ids, volt.ComponentIdConf{ComponentId: comps.PositionId})
33 | for j, id := range extraComps {
34 | m := 1 << j
35 | if i&m == m {
36 | ids = append(ids, volt.ComponentIdConf{ComponentId: id})
37 | }
38 | }
39 |
40 | e := world.CreateEntity(strconv.Itoa(n + i))
41 | world.AddComponents(e, ids...)
42 | ids = ids[:0]
43 | }
44 |
45 | query := volt.CreateQuery2[comps.Position, comps.Velocity](world, volt.QueryConfiguration{})
46 |
47 | for b.Loop() {
48 | for result := range query.Foreach(nil) {
49 | pos := result.A
50 | vel := result.B
51 | pos.X += vel.X
52 | pos.Y += vel.Y
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/bench/query2comp/arche.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 |
16 | query := ecs.NewBuilder(&world, posID, velID).NewBatchQ(n)
17 | for query.Next() {
18 | vel := (*comps.Velocity)(query.Get(velID))
19 | vel.X = 1
20 | vel.Y = 1
21 | }
22 |
23 | filter := ecs.All(posID, velID)
24 |
25 | for b.Loop() {
26 | query := world.Query(&filter)
27 | for query.Next() {
28 | pos := (*comps.Position)(query.Get(posID))
29 | vel := (*comps.Velocity)(query.Get(velID))
30 | pos.X += vel.X
31 | pos.Y += vel.Y
32 | }
33 | }
34 | }
35 |
36 | func runArcheRegistered(b *testing.B, n int) {
37 | world := ecs.NewWorld(1024)
38 |
39 | posID := ecs.ComponentID[comps.Position](&world)
40 | velID := ecs.ComponentID[comps.Velocity](&world)
41 |
42 | query := ecs.NewBuilder(&world, posID, velID).NewBatchQ(n)
43 | for query.Next() {
44 | vel := (*comps.Velocity)(query.Get(velID))
45 | vel.X = 1
46 | vel.Y = 1
47 | }
48 |
49 | filter := ecs.All(posID, velID)
50 | cf := world.Cache().Register(&filter)
51 |
52 | for b.Loop() {
53 | query := world.Query(&cf)
54 | for query.Next() {
55 | pos := (*comps.Position)(query.Get(posID))
56 | vel := (*comps.Velocity)(query.Get(velID))
57 | pos.X += vel.X
58 | pos.Y += vel.Y
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/bench/query2comp/ark.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
14 | for range n {
15 | _ = mapper.NewEntity(&comps.Position{}, &comps.Velocity{X: 1, Y: 1})
16 | }
17 |
18 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
19 |
20 | for b.Loop() {
21 | query := filter.Query()
22 | for query.Next() {
23 | pos, vel := query.Get()
24 | pos.X += vel.X
25 | pos.Y += vel.Y
26 | }
27 | }
28 | }
29 |
30 | func runArkRegistered(b *testing.B, n int) {
31 | world := ecs.NewWorld(1024)
32 |
33 | mapper := ecs.NewMap2[comps.Position, comps.Velocity](&world)
34 | for range n {
35 | _ = mapper.NewEntity(&comps.Position{}, &comps.Velocity{X: 1, Y: 1})
36 | }
37 |
38 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world).Register()
39 |
40 | for b.Loop() {
41 | query := filter.Query()
42 | for query.Next() {
43 | pos, vel := query.Get()
44 | pos.X += vel.X
45 | pos.Y += vel.Y
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/bench/query2comp/donburi.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/filter"
9 | )
10 |
11 | func runDonburi(b *testing.B, n int) {
12 | world := donburi.NewWorld()
13 |
14 | var position = donburi.NewComponentType[comps.Position]()
15 | var velocity = donburi.NewComponentType[comps.Velocity]()
16 |
17 | for i := 0; i < n; i++ {
18 | e := world.Create(position, velocity)
19 | entry := world.Entry(e)
20 | vel := (*comps.Position)(entry.Component(velocity))
21 | vel.X = 1
22 | vel.Y = 1
23 | }
24 |
25 | query := donburi.NewQuery(filter.Contains(position, velocity))
26 |
27 | for b.Loop() {
28 | query.Each(world, func(entry *donburi.Entry) {
29 | pos := position.Get(entry)
30 | vel := velocity.Get(entry)
31 |
32 | pos.X += vel.X
33 | pos.Y += vel.Y
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/bench/query2comp/ggecs.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | positionComponentID ecs.ComponentID = iota
13 | velocityComponentID
14 | )
15 |
16 | func runGGEcs(b *testing.B, n int) {
17 | world := ecs.NewWorld(1024)
18 | world.Register(ecs.NewComponentRegistry[comps.Position](positionComponentID))
19 | world.Register(ecs.NewComponentRegistry[comps.Velocity](velocityComponentID))
20 |
21 | for i := 0; i < n; i++ {
22 | e := world.NewEntity(positionComponentID, velocityComponentID)
23 | vel := (*comps.Velocity)(world.Component(e, velocityComponentID))
24 | vel.X = 1
25 | vel.Y = 1
26 | }
27 |
28 | mask := ecs.MakeComponentMask(positionComponentID, velocityComponentID)
29 |
30 | for b.Loop() {
31 | query := world.Query(mask)
32 | for query.Next() {
33 | pos := (*comps.Position)(query.Component(positionComponentID))
34 | vel := (*comps.Velocity)(query.Component(velocityComponentID))
35 | pos.X += vel.X
36 | pos.Y += vel.Y
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/bench/query2comp/run.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (cached)", F: runArcheRegistered},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (cached)", F: runArkRegistered},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/query2comp/uot.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | for i := 0; i < n; i++ {
14 | id := world.NewId()
15 | ecs.Write(world, id,
16 | ecs.C(comps.Position{}),
17 | ecs.C(comps.Velocity{X: 1, Y: 1}),
18 | )
19 | }
20 | query := ecs.Query2[comps.Position, comps.Velocity](world)
21 |
22 | for b.Loop() {
23 | query.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
24 | pos.X += vel.X
25 | pos.Y += vel.Y
26 | })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/bench/query2comp/volt.go:
--------------------------------------------------------------------------------
1 | package query2comp
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
12 |
13 | func runVolt(b *testing.B, n int) {
14 | world := volt.CreateWorld(1024)
15 |
16 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.Velocity](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
18 |
19 | for i := 0; i < n; i++ {
20 | e := world.CreateEntity(strconv.Itoa(i))
21 | volt.AddComponents2(world, e, comps.Position{}, comps.Velocity{})
22 | }
23 |
24 | query := volt.CreateQuery2[comps.Position, comps.Velocity](world, volt.QueryConfiguration{})
25 |
26 | for b.Loop() {
27 | for result := range query.Foreach(nil) {
28 | pos := result.A
29 | vel := result.B
30 | pos.X += vel.X
31 | pos.Y += vel.Y
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bench/query32arch/arche.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/arche/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArche(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | c1ID := ecs.ComponentID[comps.C1](&world)
16 | c2ID := ecs.ComponentID[comps.C2](&world)
17 | c3ID := ecs.ComponentID[comps.C3](&world)
18 | c4ID := ecs.ComponentID[comps.C4](&world)
19 | c5ID := ecs.ComponentID[comps.C5](&world)
20 |
21 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID}
22 |
23 | ids := []ecs.ID{}
24 | for i := range n {
25 | ids = append(ids, posID, velID)
26 | for j, id := range extraIDs {
27 | m := 1 << j
28 | if i&m == m {
29 | ids = append(ids, id)
30 | }
31 | }
32 | world.NewEntity(ids...)
33 |
34 | ids = ids[:0]
35 | }
36 |
37 | filter := ecs.All(posID, velID)
38 |
39 | for b.Loop() {
40 | query := world.Query(&filter)
41 | for query.Next() {
42 | pos := (*comps.Position)(query.Get(posID))
43 | vel := (*comps.Velocity)(query.Get(velID))
44 | pos.X += vel.X
45 | pos.Y += vel.Y
46 | }
47 | }
48 | }
49 |
50 | func runArcheRegistered(b *testing.B, n int) {
51 | world := ecs.NewWorld(1024)
52 |
53 | posID := ecs.ComponentID[comps.Position](&world)
54 | velID := ecs.ComponentID[comps.Velocity](&world)
55 | c1ID := ecs.ComponentID[comps.C1](&world)
56 | c2ID := ecs.ComponentID[comps.C2](&world)
57 | c3ID := ecs.ComponentID[comps.C3](&world)
58 | c4ID := ecs.ComponentID[comps.C4](&world)
59 | c5ID := ecs.ComponentID[comps.C5](&world)
60 |
61 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID}
62 |
63 | ids := []ecs.ID{}
64 | for i := range n {
65 | ids = append(ids, posID, velID)
66 | for j, id := range extraIDs {
67 | m := 1 << j
68 | if i&m == m {
69 | ids = append(ids, id)
70 | }
71 | }
72 | world.NewEntity(ids...)
73 |
74 | ids = ids[:0]
75 | }
76 |
77 | filter := ecs.All(posID, velID)
78 | cf := world.Cache().Register(&filter)
79 |
80 | for b.Loop() {
81 | query := world.Query(&cf)
82 | for query.Next() {
83 | pos := (*comps.Position)(query.Get(posID))
84 | vel := (*comps.Velocity)(query.Get(velID))
85 | pos.X += vel.X
86 | pos.Y += vel.Y
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/bench/query32arch/ark.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | func runArk(b *testing.B, n int) {
11 | world := ecs.NewWorld(1024)
12 |
13 | posID := ecs.ComponentID[comps.Position](&world)
14 | velID := ecs.ComponentID[comps.Velocity](&world)
15 | c1ID := ecs.ComponentID[comps.C1](&world)
16 | c2ID := ecs.ComponentID[comps.C2](&world)
17 | c3ID := ecs.ComponentID[comps.C3](&world)
18 | c4ID := ecs.ComponentID[comps.C4](&world)
19 | c5ID := ecs.ComponentID[comps.C5](&world)
20 |
21 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID}
22 |
23 | ids := []ecs.ID{}
24 | for i := range n {
25 | ids = append(ids, posID, velID)
26 | for j, id := range extraIDs {
27 | m := 1 << j
28 | if i&m == m {
29 | ids = append(ids, id)
30 | }
31 | }
32 | world.Unsafe().NewEntity(ids...)
33 |
34 | ids = ids[:0]
35 | }
36 |
37 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world)
38 |
39 | for b.Loop() {
40 | query := filter.Query()
41 | for query.Next() {
42 | pos, vel := query.Get()
43 | pos.X += vel.X
44 | pos.Y += vel.Y
45 | }
46 | }
47 | }
48 |
49 | func runArkRegistered(b *testing.B, n int) {
50 | world := ecs.NewWorld(1024)
51 |
52 | posID := ecs.ComponentID[comps.Position](&world)
53 | velID := ecs.ComponentID[comps.Velocity](&world)
54 | c1ID := ecs.ComponentID[comps.C1](&world)
55 | c2ID := ecs.ComponentID[comps.C2](&world)
56 | c3ID := ecs.ComponentID[comps.C3](&world)
57 | c4ID := ecs.ComponentID[comps.C4](&world)
58 | c5ID := ecs.ComponentID[comps.C5](&world)
59 |
60 | extraIDs := []ecs.ID{c1ID, c2ID, c3ID, c4ID, c5ID}
61 |
62 | ids := []ecs.ID{}
63 | for i := range n {
64 | ids = append(ids, posID, velID)
65 | for j, id := range extraIDs {
66 | m := 1 << j
67 | if i&m == m {
68 | ids = append(ids, id)
69 | }
70 | }
71 | world.Unsafe().NewEntity(ids...)
72 |
73 | ids = ids[:0]
74 | }
75 |
76 | filter := ecs.NewFilter2[comps.Position, comps.Velocity](&world).Register()
77 |
78 | for b.Loop() {
79 | query := filter.Query()
80 | for query.Next() {
81 | pos, vel := query.Get()
82 | pos.X += vel.X
83 | pos.Y += vel.Y
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/bench/query32arch/donburi.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/yohamta/donburi"
8 | "github.com/yohamta/donburi/component"
9 | "github.com/yohamta/donburi/filter"
10 | )
11 |
12 | func runDonburi(b *testing.B, n int) {
13 | world := donburi.NewWorld()
14 |
15 | var position = donburi.NewComponentType[comps.Position]()
16 | var velocity = donburi.NewComponentType[comps.Velocity]()
17 | var c1 = donburi.NewComponentType[comps.C1]()
18 | var c2 = donburi.NewComponentType[comps.C2]()
19 | var c3 = donburi.NewComponentType[comps.C3]()
20 | var c4 = donburi.NewComponentType[comps.C4]()
21 | var c5 = donburi.NewComponentType[comps.C5]()
22 |
23 | extraIDs := []component.IComponentType{c1, c2, c3, c4, c5}
24 | ids := []component.IComponentType{}
25 |
26 | for i := range n {
27 | ids = append(ids, position, velocity)
28 | for j, id := range extraIDs {
29 | m := 1 << j
30 | if i&m == m {
31 | ids = append(ids, id)
32 | }
33 | }
34 |
35 | world.Create(ids...)
36 |
37 | ids = ids[:0]
38 | }
39 |
40 | query := donburi.NewQuery(filter.Contains(position, velocity))
41 |
42 | for b.Loop() {
43 | query.Each(world, func(entry *donburi.Entry) {
44 | pos := position.Get(entry)
45 | vel := velocity.Get(entry)
46 |
47 | pos.X += vel.X
48 | pos.Y += vel.Y
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/bench/query32arch/ggecs.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "testing"
5 |
6 | ecs "github.com/marioolofo/go-gameengine-ecs"
7 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
8 | )
9 |
10 | // Component IDs
11 | const (
12 | PositionComponentID ecs.ComponentID = iota
13 | VelocityComponentID
14 | C1ID
15 | C2ID
16 | C3ID
17 | C4ID
18 | C5ID
19 | )
20 |
21 | func runGGEcs(b *testing.B, n int) {
22 | world := ecs.NewWorld(1024)
23 | world.Register(ecs.NewComponentRegistry[comps.Position](PositionComponentID))
24 | world.Register(ecs.NewComponentRegistry[comps.Velocity](VelocityComponentID))
25 |
26 | world.Register(ecs.NewComponentRegistry[comps.C1](C1ID))
27 | world.Register(ecs.NewComponentRegistry[comps.C2](C2ID))
28 | world.Register(ecs.NewComponentRegistry[comps.C3](C3ID))
29 | world.Register(ecs.NewComponentRegistry[comps.C4](C4ID))
30 | world.Register(ecs.NewComponentRegistry[comps.C5](C5ID))
31 |
32 | extraIDs := []ecs.ComponentID{C1ID, C2ID, C3ID, C4ID, C5ID}
33 |
34 | ids := []ecs.ComponentID{}
35 | for i := range n {
36 | ids = append(ids, PositionComponentID, VelocityComponentID)
37 | for j, id := range extraIDs {
38 | m := 1 << j
39 | if i&m == m {
40 | ids = append(ids, id)
41 | }
42 | }
43 | _ = world.NewEntity(ids...)
44 |
45 | ids = ids[:0]
46 | }
47 |
48 | mask := ecs.MakeComponentMask(PositionComponentID, VelocityComponentID)
49 |
50 | for b.Loop() {
51 | query := world.Query(mask)
52 | for query.Next() {
53 | pos := (*comps.Position)(query.Component(PositionComponentID))
54 | vel := (*comps.Velocity)(query.Component(VelocityComponentID))
55 | pos.X += vel.X
56 | pos.Y += vel.Y
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/bench/query32arch/run.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Arche (cached)", F: runArcheRegistered},
13 | {Name: "Ark", F: runArk},
14 | {Name: "Ark (cached)", F: runArkRegistered},
15 | {Name: "Donburi", F: runDonburi},
16 | {Name: "ggecs", F: runGGEcs},
17 | {Name: "uot", F: runUot},
18 | {Name: "Volt", F: runVolt},
19 | },
20 | N: []int{
21 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bench/query32arch/uot.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
7 | "github.com/unitoftime/ecs"
8 | )
9 |
10 | func runUot(b *testing.B, n int) {
11 | world := ecs.NewWorld()
12 |
13 | extraComps := []any{comps.C1{}, comps.C2{}, comps.C3{}, comps.C4{}, comps.C5{}}
14 |
15 | ids := []ecs.Component{}
16 | for i := range n {
17 | ids = append(ids, ecs.C(comps.Position{}), ecs.C(comps.Velocity{}))
18 | for j, id := range extraComps {
19 | m := 1 << j
20 | if i&m == m {
21 | ids = append(ids, ecs.C(id))
22 | }
23 | }
24 |
25 | id := world.NewId()
26 | ecs.Write(world, id, ids...)
27 |
28 | ids = ids[:0]
29 | }
30 | query := ecs.Query2[comps.Position, comps.Velocity](world)
31 |
32 | for b.Loop() {
33 | query.MapId(func(id ecs.Id, pos *comps.Position, vel *comps.Velocity) {
34 | pos.X += vel.X
35 | pos.Y += vel.Y
36 | })
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/bench/query32arch/volt.go:
--------------------------------------------------------------------------------
1 | package query32arch
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 |
7 | "github.com/akmonengine/volt"
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | )
10 |
11 | func runVolt(b *testing.B, n int) {
12 | world := volt.CreateWorld(1024)
13 |
14 | volt.RegisterComponent[comps.Position](world, &volt.ComponentConfig[comps.Position]{BuilderFn: func(component any, configuration any) {}})
15 | volt.RegisterComponent[comps.Velocity](world, &volt.ComponentConfig[comps.Velocity]{BuilderFn: func(component any, configuration any) {}})
16 | volt.RegisterComponent[comps.C1](world, &volt.ComponentConfig[comps.C1]{BuilderFn: func(component any, configuration any) {}})
17 | volt.RegisterComponent[comps.C2](world, &volt.ComponentConfig[comps.C2]{BuilderFn: func(component any, configuration any) {}})
18 | volt.RegisterComponent[comps.C3](world, &volt.ComponentConfig[comps.C3]{BuilderFn: func(component any, configuration any) {}})
19 | volt.RegisterComponent[comps.C4](world, &volt.ComponentConfig[comps.C4]{BuilderFn: func(component any, configuration any) {}})
20 | volt.RegisterComponent[comps.C5](world, &volt.ComponentConfig[comps.C5]{BuilderFn: func(component any, configuration any) {}})
21 | extraComps := []volt.ComponentId{comps.C1Id, comps.C2Id, comps.C3Id, comps.C4Id, comps.C5Id}
22 |
23 | var ids []volt.ComponentIdConf
24 | for i := 0; i < n; i++ {
25 | e := world.CreateEntity(strconv.Itoa(i))
26 |
27 | ids = append(ids, volt.ComponentIdConf{ComponentId: comps.PositionId}, volt.ComponentIdConf{ComponentId: comps.VelocityId})
28 | for j, id := range extraComps {
29 | m := 1 << j
30 | if i&m == m {
31 | ids = append(ids, volt.ComponentIdConf{ComponentId: id})
32 | }
33 | }
34 |
35 | world.AddComponents(e, ids...)
36 | ids = ids[:0]
37 | }
38 |
39 | query := volt.CreateQuery2[comps.Position, comps.Velocity](world, volt.QueryConfiguration{})
40 |
41 | for b.Loop() {
42 | for result := range query.Foreach(nil) {
43 | pos := result.A
44 | vel := result.B
45 | pos.X += vel.X
46 | pos.Y += vel.Y
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/bench/random/arche.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "log"
5 | "math/rand/v2"
6 | "testing"
7 |
8 | "github.com/mlange-42/arche/ecs"
9 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
10 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
11 | )
12 |
13 | func runArche(b *testing.B, n int) {
14 | world := ecs.NewWorld(1024)
15 |
16 | posID := ecs.ComponentID[comps.Position](&world)
17 |
18 | entities := make([]ecs.Entity, 0, n)
19 | query := world.Batch().NewQ(n, posID)
20 | for query.Next() {
21 | entities = append(entities, query.Entity())
22 | }
23 | rand.Shuffle(n, util.Swap(entities))
24 |
25 | sum := 0.0
26 | for b.Loop() {
27 | for _, e := range entities {
28 | pos := (*comps.Position)(world.Get(e, posID))
29 | sum += pos.X
30 | }
31 | }
32 | if sum > 0 {
33 | log.Fatal("error")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/bench/random/ark.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "log"
5 | "math/rand/v2"
6 | "testing"
7 |
8 | "github.com/mlange-42/ark/ecs"
9 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
10 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
11 | )
12 |
13 | func runArk(b *testing.B, n int) {
14 | world := ecs.NewWorld(1024)
15 |
16 | mapper := ecs.NewMap[comps.Position](&world)
17 | entities := make([]ecs.Entity, 0, n)
18 | for range n {
19 | e := mapper.NewEntity(&comps.Position{})
20 | entities = append(entities, e)
21 | }
22 | rand.Shuffle(n, util.Swap(entities))
23 |
24 | sum := 0.0
25 | for b.Loop() {
26 | for _, e := range entities {
27 | pos := mapper.Get(e)
28 | sum += pos.X
29 | }
30 | }
31 | if sum > 0 {
32 | log.Fatal("error")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bench/random/donburi.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "log"
5 | "math/rand/v2"
6 | "testing"
7 |
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
10 | "github.com/yohamta/donburi"
11 | )
12 |
13 | func runDonburi(b *testing.B, n int) {
14 | world := donburi.NewWorld()
15 |
16 | var position = donburi.NewComponentType[comps.Position]()
17 |
18 | entities := make([]donburi.Entity, 0, n)
19 | for i := 0; i < n; i++ {
20 | e := world.Create(position)
21 | entities = append(entities, e)
22 | }
23 | rand.Shuffle(n, util.Swap(entities))
24 |
25 | sum := 0.0
26 | for b.Loop() {
27 | for _, e := range entities {
28 | entry := world.Entry(e)
29 | pos := (*comps.Position)(entry.Component(position))
30 | sum += pos.X
31 | }
32 | }
33 | if sum > 0 {
34 | log.Fatal("error")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/bench/random/ggecs.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "log"
5 | "math/rand/v2"
6 | "testing"
7 |
8 | ecs "github.com/marioolofo/go-gameengine-ecs"
9 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
10 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
11 | )
12 |
13 | // Component IDs
14 | const (
15 | positionComponentID ecs.ComponentID = iota
16 | velocityComponentID
17 | )
18 |
19 | func runGGEcs(b *testing.B, n int) {
20 | world := ecs.NewWorld(1024)
21 | world.Register(ecs.NewComponentRegistry[comps.Position](positionComponentID))
22 |
23 | entities := make([]ecs.EntityID, 0, n)
24 | for i := 0; i < n; i++ {
25 | e := world.NewEntity(positionComponentID)
26 | entities = append(entities, e)
27 | }
28 | rand.Shuffle(n, util.Swap(entities))
29 |
30 | sum := 0.0
31 | for b.Loop() {
32 | for _, e := range entities {
33 | pos := (*comps.Position)(world.Component(e, positionComponentID))
34 | sum += pos.X
35 | }
36 | }
37 | if sum > 0 {
38 | log.Fatal("error")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/bench/random/run.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
5 | )
6 |
7 | // Benchmarks runs the benchmarks.
8 | func Benchmarks() util.Benchmarks {
9 | return util.Benchmarks{
10 | Benches: []util.Benchmark{
11 | {Name: "Arche", F: runArche},
12 | {Name: "Ark", F: runArk},
13 | {Name: "Donburi", F: runDonburi},
14 | {Name: "ggecs", F: runGGEcs},
15 | {Name: "uot", F: runUot},
16 | {Name: "Volt", F: runVolt},
17 | },
18 | N: []int{
19 | 1, 4, 16, 64, 256, 1024, 16_000, 256_000, 1_000_000,
20 | },
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/bench/random/uot.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "log"
5 | "math/rand/v2"
6 | "testing"
7 |
8 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
9 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
10 | "github.com/unitoftime/ecs"
11 | )
12 |
13 | func runUot(b *testing.B, n int) {
14 | world := ecs.NewWorld()
15 |
16 | entities := make([]ecs.Id, 0, n)
17 | for i := 0; i < n; i++ {
18 | id := world.NewId()
19 | ecs.Write(world, id,
20 | ecs.C(comps.Position{}),
21 | ecs.C(comps.Velocity{}),
22 | )
23 | entities = append(entities, id)
24 | }
25 | rand.Shuffle(n, util.Swap(entities))
26 |
27 | sum := 0.0
28 | for b.Loop() {
29 | for _, e := range entities {
30 | pos := ecs.ReadPtr[comps.Position](world, e)
31 | sum += pos.X
32 | }
33 | }
34 | if sum > 0 {
35 | log.Fatal("error")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/bench/random/volt.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "log"
5 | "math/rand/v2"
6 | "strconv"
7 | "testing"
8 |
9 | "github.com/akmonengine/volt"
10 | "github.com/mlange-42/go-ecs-benchmarks/bench/comps"
11 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
12 | )
13 |
14 | type voltConfig = volt.ComponentConfig[volt.ComponentInterface]
15 |
16 | func runVolt(b *testing.B, n int) {
17 | world := volt.CreateWorld(1024)
18 |
19 | volt.RegisterComponent[comps.Position](world, &voltConfig{BuilderFn: func(component any, configuration any) {}})
20 |
21 | entities := make([]volt.EntityId, 0, n)
22 | for i := 0; i < n; i++ {
23 | e := world.CreateEntity(strconv.Itoa(i))
24 | volt.AddComponent(world, e, comps.Position{})
25 | entities = append(entities, e)
26 | }
27 | rand.Shuffle(n, util.Swap(entities))
28 |
29 | sum := 0.0
30 | for b.Loop() {
31 | for _, e := range entities {
32 | pos := volt.GetComponent[comps.Position](world, e)
33 | sum += pos.X
34 | }
35 | }
36 | if sum > 0 {
37 | log.Fatal("error")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/bench/run.go:
--------------------------------------------------------------------------------
1 | package bench
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path"
8 | "strings"
9 | "time"
10 |
11 | addremove "github.com/mlange-42/go-ecs-benchmarks/bench/add_remove"
12 | addremovelarge "github.com/mlange-42/go-ecs-benchmarks/bench/add_remove_large"
13 | "github.com/mlange-42/go-ecs-benchmarks/bench/create10comp"
14 | "github.com/mlange-42/go-ecs-benchmarks/bench/create2comp"
15 | create2compalloc "github.com/mlange-42/go-ecs-benchmarks/bench/create2comp_alloc"
16 | "github.com/mlange-42/go-ecs-benchmarks/bench/delete10comp"
17 | "github.com/mlange-42/go-ecs-benchmarks/bench/delete2comp"
18 | newworld "github.com/mlange-42/go-ecs-benchmarks/bench/new_world"
19 | "github.com/mlange-42/go-ecs-benchmarks/bench/query256arch"
20 | "github.com/mlange-42/go-ecs-benchmarks/bench/query2comp"
21 | "github.com/mlange-42/go-ecs-benchmarks/bench/query32arch"
22 | "github.com/mlange-42/go-ecs-benchmarks/bench/random"
23 | "github.com/mlange-42/go-ecs-benchmarks/bench/util"
24 | "github.com/shirou/gopsutil/v4/cpu"
25 | )
26 |
27 | var benchmarks = map[string]func() util.Benchmarks{
28 | "query2comp": query2comp.Benchmarks,
29 | "query32arch": query32arch.Benchmarks,
30 | "query256arch": query256arch.Benchmarks,
31 |
32 | "random": random.Benchmarks,
33 |
34 | "create2comp": create2comp.Benchmarks,
35 | "create2comp_alloc": create2compalloc.Benchmarks,
36 | "create10comp": create10comp.Benchmarks,
37 | "add_remove": addremove.Benchmarks,
38 | "add_remove_large": addremovelarge.Benchmarks,
39 |
40 | "delete2comp": delete2comp.Benchmarks,
41 | "delete10comp": delete10comp.Benchmarks,
42 |
43 | "new_world": newworld.Benchmarks,
44 | }
45 |
46 | // RunAll runs all benchmarks.
47 | func RunAll() {
48 | if err := os.Mkdir("results", os.ModePerm); err != nil && !os.IsExist(err) {
49 | log.Fatal(err)
50 | }
51 | if err := writeInfo(); err != nil {
52 | log.Fatal(err)
53 | }
54 | for name, fn := range benchmarks {
55 | util.RunBenchmarks(name, fn(), util.ToCSV)
56 | }
57 | }
58 |
59 | // Run runs selected benchmarks.
60 | func Run(benches []string) {
61 | if err := os.Mkdir("results", os.ModePerm); err != nil && !os.IsExist(err) {
62 | log.Fatal(err)
63 | }
64 | if err := writeInfo(); err != nil {
65 | log.Fatal(err)
66 | }
67 |
68 | for _, b := range benches {
69 | if _, ok := benchmarks[b]; !ok {
70 | log.Fatalf("benchmark %s not found", b)
71 | }
72 | }
73 |
74 | for _, b := range benches {
75 | util.RunBenchmarks(b, benchmarks[b](), util.ToCSV)
76 | }
77 | }
78 |
79 | func writeInfo() error {
80 | text := strings.Builder{}
81 | fmt.Fprintf(&text, "Last run: %s \n", time.Now().Format(time.RFC1123))
82 | infos, err := cpu.Info()
83 | if err != nil {
84 | return err
85 | }
86 | for _, info := range infos {
87 | fmt.Fprintf(&text, "CPU: %s\n", info.ModelName)
88 | break
89 | }
90 | err = os.WriteFile(path.Join("results", "info.md"), []byte(text.String()), 0666)
91 | if err != nil {
92 | return err
93 | }
94 |
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/bench/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | type Benchmarks struct {
12 | Benches []Benchmark
13 | N []int
14 | time [][]float64
15 | }
16 |
17 | type Benchmark struct {
18 | Name string
19 | F func(b *testing.B, n int)
20 | }
21 |
22 | func RunBenchmarks(name string, benchmarks Benchmarks, format func(Benchmarks) string) {
23 | fmt.Println("Running", name)
24 |
25 | benchmarks.time = make([][]float64, len(benchmarks.Benches))
26 |
27 | for i := range benchmarks.time {
28 | benchmarks.time[i] = make([]float64, len(benchmarks.N))
29 | }
30 |
31 | for i, n := range benchmarks.N {
32 | fmt.Println(" N =", n)
33 | for j := range benchmarks.Benches {
34 | bench := &benchmarks.Benches[j]
35 | fmt.Printf(" %-20s", bench.Name)
36 | res := testing.Benchmark(func(b *testing.B) {
37 | bench.F(b, n)
38 | })
39 | nanos := float64(res.T.Nanoseconds()) / float64(res.N*n)
40 | benchmarks.time[j][i] = nanos
41 | fmt.Printf("%12s\n", toTime(nanos))
42 | }
43 | }
44 |
45 | os.WriteFile(path.Join("results", fmt.Sprintf("%s.csv", name)), []byte(format(benchmarks)), 0666)
46 | }
47 |
48 | func ToCSV(benchmarks Benchmarks) string {
49 | b := strings.Builder{}
50 |
51 | b.WriteString("N,")
52 | for j := range benchmarks.Benches {
53 | bench := &benchmarks.Benches[j]
54 | b.WriteString(bench.Name)
55 | if j < len(benchmarks.Benches)-1 {
56 | b.WriteString(",")
57 | }
58 | }
59 | b.WriteString("\n")
60 |
61 | for i, n := range benchmarks.N {
62 | b.WriteString(fmt.Sprintf("%d,", n))
63 | for j := range benchmarks.Benches {
64 | t := benchmarks.time[j][i]
65 |
66 | tStr := fmt.Sprintf("%f", t)
67 | b.WriteString(tStr)
68 | if j < len(benchmarks.Benches)-1 {
69 | b.WriteString(",")
70 | }
71 | }
72 | b.WriteString("\n")
73 | }
74 |
75 | return b.String()
76 | }
77 |
78 | func Swap[T any](slice []T) func(i, j int) {
79 | return func(i, j int) {
80 | slice[i], slice[j] = slice[j], slice[i]
81 | }
82 | }
83 |
84 | func toTime(v float64) string {
85 | if v < 1_000 {
86 | return fmt.Sprintf("%.2fns", v)
87 | }
88 | if v < 1_000_000 {
89 | return fmt.Sprintf("%.2fus", v/1000)
90 | }
91 | return fmt.Sprintf("%.2fms", v/1_000_000)
92 | }
93 |
--------------------------------------------------------------------------------
/docs/README-template.md:
--------------------------------------------------------------------------------
1 | # Go ECS Benchmarks
2 |
3 | Comparative benchmarks for Go Entity Component System (ECS) implementations.
4 |
5 | > Disclaimer: This repository is maintained by the author of
6 | > [Arche](https://github.com/mlange-42/arche) and [Ark](https://github.com/mlange-42/ark).
7 |
8 | ## Benchmark candidates
9 |
10 | | ECS | Version |
11 | |-----|---------|
12 | | [Arche](https://github.com/mlange-42/arche) | v0.15.3 |
13 | | [Ark](https://github.com/mlange-42/ark) | v0.4.2 |
14 | | [Donburi](https://github.com/yottahmd/donburi) | v1.15.7 |
15 | | [go-gameengine-ecs](https://github.com/marioolofo/go-gameengine-ecs) | v0.9.0 |
16 | | [unitoftime/ecs](https://github.com/unitoftime/ecs) | v0.0.3 |
17 | | [Volt](https://github.com/akmonengine/volt) | v1.6.0 |
18 |
19 | Candidates are always displayed in alphabetical order.
20 |
21 | In case you develop or use a Go ECS that is not in the list and that want to see here,
22 | please open an issue or make a pull request.
23 | See the section on [Contributing](#contributing) for details.
24 |
25 | In case you are a developer or user of an implementation included here,
26 | feel free to check the benchmarked code for any possible improvements.
27 | Open an issue if you want a version update.
28 |
29 | ## Benchmarks
30 |
31 | ${info}
32 |
33 | For each benchmark, the left plot panel and the table show the time spent per entity,
34 | while the right panel shows the total time.
35 |
36 | Note that the Y axis has logarithmic scale in all plots.
37 | So doubled bar or line height is not doubled time!
38 |
39 | All components used in the benchmarks have two `float64` fields.
40 | The initial capacity of the world is set to 1024 where this is supported.
41 |
42 | ### Query
43 |
44 | `N` entities with components `Position` and `Velocity`.
45 |
46 | - Query all `[Position, Velocity]` entities, and add the velocity vector to the position vector.
47 |
48 | 
49 |
50 | ${query2comp}
51 |
52 | ### Query fragmented, inner
53 |
54 | Query where the matching entities are fragmented over 32 archetypes.
55 |
56 | `N` entities with components `Position` and `Velocity`.
57 | Each of these `N` entities has some combination of components
58 | `C1`, `C2`, ..., `C5`, so entities are fragmented over up to 32 archetypes.
59 |
60 | - Query all `[Position, Velocity]` entities, and add the velocity vector to the position vector.
61 |
62 | 
63 |
64 | ${query32arch}
65 |
66 | ### Query fragmented, outer
67 |
68 | Query where there are 256 non-matching archetypes.
69 |
70 | `N` entities with components `Position` and `Velocity`.
71 | Another `4 * N` entities with `Position` and some combination of 8 components
72 | `C1`, ..., `C8`, so these entities are fragmented over up to 256 archetypes.
73 |
74 | - Query all `[Position, Velocity]` entities, and add the velocity vector to the position vector.
75 |
76 | 
77 |
78 | ${query256arch}
79 |
80 | ### Component random access
81 |
82 | `N` entities with component `Position`.
83 | All entities are collected into a slice, and the slice is shuffled.
84 |
85 | * Iterate the shuffled entities.
86 | * For each entity, get its `Position` and sum up their `X` fields.
87 |
88 | 
89 |
90 | ${random}
91 |
92 | ### Create entities
93 |
94 | - Create `N` entities with components `Position` and `Velocity`.
95 |
96 | The operation is performed once before benchmarking,
97 | to exclude memory allocation, archetype creation etc.
98 | See the benchmark below for entity creation with allocation.
99 |
100 | 
101 |
102 | ${create2comp}
103 |
104 | ### Create entities, allocating
105 |
106 | - Create `N` entities with components `Position` and `Velocity`.
107 |
108 | Each round is performed on a fresh world.
109 | This reflects the creation of the first entities with a certain components set in your game or application.
110 | As soon as things stabilize, the benchmarks for entity creation without allocation (above) apply.
111 |
112 | Low `N` values might be biased by things like archetype creation and memory allocation,
113 | which is handled differently by different implementations.
114 |
115 | 
116 |
117 | ${create2comp_alloc}
118 |
119 | ### Create large entities
120 |
121 | - Create `N` entities with 10 components `C1`, ..., `C10`.
122 |
123 | The operation is performed once before benchmarking,
124 | to exclude things like archetype creation and memory allocation.
125 |
126 | 
127 |
128 | ${create10comp}
129 |
130 | ### Add/remove component
131 |
132 | `N` entities with component `Position`.
133 |
134 | - Query all `[Position]` entities and add `Velocity`.
135 | - Query all `[Position, Velocity]` entities and remove `Velocity`.
136 |
137 | One iteration is performed before the benchmarking starts, to exclude memory allocation.
138 |
139 | 
140 |
141 | ${add_remove}
142 |
143 | ### Add/remove component, large entity
144 |
145 | `N` entities with component `Position` and 10 further components `C1`, ..., `C10`.
146 |
147 | - Query all `[Position]` entities and add `Velocity`.
148 | - Query all `[Position, Velocity]` entities and remove `Velocity`.
149 |
150 | One iteration is performed before the benchmarking starts, to exclude memory allocation.
151 |
152 | 
153 |
154 | ${add_remove_large}
155 |
156 | ### Delete entities
157 |
158 | `N` entities with components `Position` and `Velocity`.
159 |
160 | * Delete all entities
161 |
162 | 
163 |
164 | ${delete2comp}
165 |
166 | ### Delete large entities
167 |
168 | `N` entities with 10 components `C1`, ..., `C10`.
169 |
170 | * Delete all entities
171 |
172 | 
173 |
174 | ${delete10comp}
175 |
176 | ### Create world
177 |
178 | - Create a new world
179 |
180 | ${new_world}
181 |
182 | ### Popularity
183 |
184 | Given that all tested projects are on Github, we can use the star history as a proxy here.
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | ## Running the benchmarks
193 |
194 | Run the benchmarks using the following command:
195 |
196 | ```shell
197 | go run . -test.benchtime=0.25s
198 | ```
199 |
200 | > On PowerShell use this instead:
201 | > `go run . --% -test.benchtime=0.25s`
202 |
203 | The `benchtime` limit is required for some of the benchmarks that have a high
204 | setup cost which is not timed. They would take forever otherwise.
205 | The benchmarks can take up to one hour to complete.
206 |
207 | To run a selection of benchmarks, add their names as arguments:
208 |
209 | ```shell
210 | go run . query2comp query1in10 query32arch
211 | ```
212 |
213 | To create the plots, run `plot/plot.py`. The following packages are required:
214 | - numpy
215 | - pandas
216 | - matplotlib
217 |
218 | ```
219 | pip install -r ./plot/requirements.txt
220 | python plot/plot.py
221 | ```
222 |
223 | ## Contributing
224 |
225 | Developers of ECS frameworks are welcome to add their implementation to the benchmarks.
226 | However, there are a few (quality) criteria that need to be fulfilled for inclusion:
227 |
228 | - All benchmarks must be implemented, which means that the ECS must have the required features
229 | - The ECS must be working properly and not exhibit serious flaws; it will undergo a basic review by maintainers
230 | - The ECS must be sufficiently documented so that it can be used without reading the code
231 | - There must be at least basic unit tests
232 | - Unit tests must be run in the CI of the repository
233 | - The ECS *must not* be tightly coupled to a particular game engine, particularly graphics stuff
234 | - There must be tagged release versions; only tagged versions will be included here
235 |
236 | Developers of included frameworks are encouraged to review the benchmarks,
237 | and to fix (or point to) misuse or potential optimizations.
238 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mlange-42/go-ecs-benchmarks
2 |
3 | go 1.24.2
4 |
5 | require (
6 | github.com/akmonengine/volt v1.6.0
7 | github.com/marioolofo/go-gameengine-ecs v0.9.0
8 | github.com/mlange-42/arche v0.15.3
9 | github.com/mlange-42/ark v0.4.2
10 | github.com/shirou/gopsutil/v4 v4.25.2
11 | github.com/stretchr/testify v1.10.0
12 | github.com/unitoftime/ecs v0.0.3
13 | github.com/yohamta/donburi v1.15.7
14 | )
15 |
16 | require (
17 | github.com/davecgh/go-spew v1.1.1 // indirect
18 | github.com/ebitengine/purego v0.8.2 // indirect
19 | github.com/go-ole/go-ole v1.2.6 // indirect
20 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
21 | github.com/pmezard/go-difflib v1.0.0 // indirect
22 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
23 | github.com/tklauser/go-sysconf v0.3.14 // indirect
24 | github.com/tklauser/numcpus v0.8.0 // indirect
25 | github.com/unitoftime/cod v0.0.0-20240909130117-f553b1d09d22 // indirect
26 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
27 | golang.org/x/sys v0.29.0 // indirect
28 | gopkg.in/yaml.v3 v3.0.1 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/akmonengine/volt v1.6.0 h1:iIKXjmsg3qXcFIs7IpnAogHsAf1zVOEsl1fuH46k9Ds=
2 | github.com/akmonengine/volt v1.6.0/go.mod h1:likSLaycVpILyqN47YGK6URulliNqwxHXeqc55FyfCo=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
6 | github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
7 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
8 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
9 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
10 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
11 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
12 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
13 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
14 | github.com/marioolofo/go-gameengine-ecs v0.9.0 h1:J0awhWBm7CVmtDa62ouA3X7uuUTS+zcENIeBwSRumvE=
15 | github.com/marioolofo/go-gameengine-ecs v0.9.0/go.mod h1:oigfVORaCpMEaktM5fmk/jScla/M7dEMCnJlyZmYiLo=
16 | github.com/mlange-42/arche v0.15.3 h1:vonT1aCIHmu3nnCopzmhzOP5+J2NswVTexa2UDyek34=
17 | github.com/mlange-42/arche v0.15.3/go.mod h1:bX5PDzTbf2pEIlwnjRu3IpVqduLsxve4rUblXV0Byj0=
18 | github.com/mlange-42/ark v0.4.2 h1:C6TvQZ9QbY39XPUK6Zk3YNQYHyvQD8KglwodnGzTh5Q=
19 | github.com/mlange-42/ark v0.4.2/go.mod h1:47KXHr5HLftLn4iyL8w04iv7KJUNUoDymEIotD41f3o=
20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
22 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
23 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
24 | github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
25 | github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
26 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
27 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
28 | github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
29 | github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
30 | github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
31 | github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
32 | github.com/unitoftime/cod v0.0.0-20240909130117-f553b1d09d22 h1:81V+30jfwHH0wAsOVZ6gFCTWf1+XRdeJUMf0OgI0JG4=
33 | github.com/unitoftime/cod v0.0.0-20240909130117-f553b1d09d22/go.mod h1:Iufibv9gn5GJb4Qzkf8e8xaXOV77OgkrB5kkBZTEN+M=
34 | github.com/unitoftime/ecs v0.0.3 h1:W3AfvJg04u1njFa7Q1GtFTSEuLnlevKkhzUC8aMEpww=
35 | github.com/unitoftime/ecs v0.0.3/go.mod h1:YmsHdpPA1CcSd7i6o2W3NgTwFJYI8G68GeGqKimMb3c=
36 | github.com/yohamta/donburi v1.15.7 h1:so/vHf1L133d0SFVrCUzMMueh2ko39wRkrcpNLdzvz8=
37 | github.com/yohamta/donburi v1.15.7/go.mod h1:FdjU9hpwAsAs1qRvqsSTJimPJ0dipvdnr9hMJXYc1Rk=
38 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
39 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
40 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
41 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
43 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
44 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
47 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
48 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
49 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "testing"
6 |
7 | "github.com/mlange-42/go-ecs-benchmarks/bench"
8 | )
9 |
10 | func main() {
11 | testing.Init()
12 | flag.Parse()
13 |
14 | args := flag.Args()
15 |
16 | if len(args) == 0 {
17 | bench.RunAll()
18 | return
19 | }
20 |
21 | bench.Run(args)
22 | }
23 |
--------------------------------------------------------------------------------
/plot/plot.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | from string import Template
4 |
5 | import numpy as np
6 | import pandas as pd
7 | from matplotlib import pyplot as plt
8 | from matplotlib.figure import Figure
9 |
10 | results_dir = "results"
11 | template = "docs/README-template.md"
12 |
13 | default_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
14 |
15 | colors = {
16 | "Arche": default_colors[0],
17 | "Arche (batch)": default_colors[1],
18 | "Arche (cached)": default_colors[1],
19 | "Ark": default_colors[7],
20 | "Ark (batch)": default_colors[8],
21 | "Ark (cached)": default_colors[8],
22 | "Donburi": default_colors[2],
23 | "Ento": default_colors[3], # TODO: remove
24 | "ggecs": default_colors[4],
25 | "uot": default_colors[5],
26 | "Volt": default_colors[6],
27 | }
28 |
29 |
30 | def plot_all():
31 | template_vars = {
32 | "info": "",
33 | }
34 |
35 | with open(os.path.join(results_dir, "info.md"), "r") as f:
36 | template_vars["info"] = f.read()
37 |
38 | files = glob.glob(os.path.join(results_dir, "*.csv"))
39 | files = [os.path.split(f)[-1][:-4] for f in files]
40 | print(f"{len(files)} files to plot: {' '.join(files)}")
41 |
42 | for f in files:
43 | data = pd.read_csv(os.path.join(results_dir, f"{f}.csv"))
44 | fig = plot(data)
45 | fig.savefig(os.path.join(results_dir, f"{f}.svg"))
46 | fig.savefig(os.path.join(results_dir, f"{f}.png"))
47 | plt.close(fig)
48 |
49 | md = to_markdown(data)
50 | template_vars[f] = md
51 | with open(os.path.join(results_dir, f"{f}.md"), "w") as f:
52 | f.write(md)
53 |
54 | readme = update_readme(template, template_vars)
55 | with open(os.path.join(results_dir, "README.md"), "w") as f:
56 | f.write(readme)
57 |
58 |
59 | def plot(data: pd.DataFrame) -> Figure:
60 | multi = len(data.index) > 1
61 | if multi:
62 | fig, ax = plt.subplots(ncols=2, figsize=(10, 4))
63 | plot_bars(data, ax[0], legend=False)
64 | plot_lines(data, ax[1], legend=True)
65 | else:
66 | fig, ax = plt.subplots(ncols=1, figsize=(5, 4))
67 | plot_bars(data, ax, legend=True)
68 |
69 | fig.tight_layout()
70 | return fig
71 |
72 |
73 | def plot_bars(data: pd.DataFrame, ax, legend: bool):
74 | cols = data.columns[1:]
75 | width = 1.0 / (1.5 * len(cols))
76 | max_value = data.max()[1:].max()
77 |
78 | for i, col in enumerate(cols):
79 | col_data = data[col]
80 | x = np.arange(len(col_data)) + i * width - 0.375
81 | ax.bar(x, col_data, width=width, color=colors[col], label=col)
82 |
83 | ax.set_ylabel("Time per entity")
84 | ax.set_yscale("log")
85 |
86 | if max_value > 400:
87 | ax.set_yticks([1, 10, 100, 1000])
88 | ax.set_yticklabels(["1ns", "10ns", "100ns", "1μs"])
89 | else:
90 | ax.set_yticks([1, 10, 100])
91 | ax.set_yticklabels(["1ns", "10ns", "100ns"])
92 |
93 | ax.set_xlabel("#Entities")
94 |
95 | ax.set_xticks(range(len(data.index)))
96 |
97 | labels = [
98 | str(n) if n < 1000 else f"{n//1000}k" if n < 1000000 else f"{n//1000000}M"
99 | for n in data.N
100 | ]
101 | ax.set_xticklabels(labels)
102 |
103 | if legend:
104 | ax.legend(framealpha=0.5)
105 |
106 |
107 | def plot_lines(data: pd.DataFrame, ax, legend: bool):
108 | cols = data.columns[1:]
109 | width = 1.0 / (1.5 * len(cols))
110 |
111 | for i, col in enumerate(cols):
112 | col_data = data[col]
113 | ax.plot(data.N, col_data * data.N, color=colors[col], label=col)
114 |
115 | ax.set_ylabel("Total time")
116 | ax.set_xscale("log")
117 | ax.set_yscale("log")
118 |
119 | ax.set_yticks([1, 1000, 1000_000])
120 | ax.set_yticklabels(["1ns", "1μs", "1ms"])
121 |
122 | ax.set_xlabel("#Entities")
123 |
124 | ax.set_xticks(data.N)
125 |
126 | labels = [
127 | str(n) if n < 1000 else f"{n//1000}k" if n < 1000000 else f"{n//1000000}M"
128 | for n in data.N
129 | ]
130 | ax.set_xticklabels(labels)
131 |
132 | if legend:
133 | ax.legend(framealpha=0.5)
134 |
135 |
136 | def to_markdown(data: pd.DataFrame) -> str:
137 | s = ""
138 | s += "| " + " | ".join(data.columns) + " |\n"
139 | s += "| " + " | ".join(["---"] * len(data.columns)) + " |\n"
140 |
141 | for i, row in data.iterrows():
142 | if row.N < 1000:
143 | n = "%d" % (row.N)
144 | elif row.N < 1_000_000:
145 | n = "%dk" % (row.N // 1000)
146 | else:
147 | n = "%dM" % (row.N // 1000000)
148 |
149 | vals = [to_time(v) for v in row.iloc[1:]]
150 | s += "| " + " | ".join([n] + vals) + " |\n"
151 |
152 | return s
153 |
154 |
155 | def to_time(v: float) -> str:
156 | if v < 1_000:
157 | return f"{v:.2f}ns"
158 | if v < 1_000_000:
159 | return f"{(v/1_000):.2f}us"
160 | return f"{(v/1_000_000):.2f}ms"
161 |
162 |
163 | def update_readme(template_file: str, values: dict) -> str:
164 | with open(template_file, "r") as file:
165 | file_content = file.read()
166 |
167 | s = Template(file_content)
168 | return s.substitute(values)
169 |
170 |
171 | if __name__ == "__main__":
172 | plot_all()
173 |
--------------------------------------------------------------------------------
/plot/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | pandas
3 | matplotlib
4 |
--------------------------------------------------------------------------------