├── .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 | ![query2comp](https://github.com/user-attachments/assets/3e20bc10-5f23-4f14-af03-6000871e34aa) 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 | ![query32arch](https://github.com/user-attachments/assets/89dfda41-36a4-422c-984e-6ba004df4327) 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 | ![query256arch](https://github.com/user-attachments/assets/e1dd0db3-3da3-4313-96c7-196324705c07) 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 | ![random](https://github.com/user-attachments/assets/2948302b-5bd0-465f-a703-1c1270dce783) 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 | ![create2comp](https://github.com/user-attachments/assets/51de492e-febe-4ea7-9fde-2afbb94dd721) 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 | ![create2comp_alloc](https://github.com/user-attachments/assets/5d07c86c-1fc3-49fd-83e9-ebbfc912c67f) 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 | ![create10comp](https://github.com/user-attachments/assets/c5f6cf29-424a-4959-9afb-a9805930d5c2) 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 | ![add_remove](https://github.com/user-attachments/assets/238f4b07-dd34-47af-8ec6-8a2799ae24d8) 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 | ![add_remove_large](https://github.com/user-attachments/assets/c737b90a-2889-4ad6-b22e-397c4189fc1f) 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 | ![delete2comp](https://github.com/user-attachments/assets/5a1061d0-21aa-4644-bd7f-8aad17102f96) 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 | ![delete10comp](https://github.com/user-attachments/assets/b9f8cd14-0e23-48d3-94e9-6943401c4d51) 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 | Star History Chart 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 | ![query2comp](query2comp.svg) 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 | ![query32arch](query32arch.svg) 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 | ![query256arch](query256arch.svg) 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 | ![random](random.svg) 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 | ![create2comp](create2comp.svg) 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 | ![create2comp_alloc](create2comp_alloc.svg) 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 | ![create10comp](create10comp.svg) 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 | ![add_remove](add_remove.svg) 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 | ![add_remove_large](add_remove_large.svg) 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 | ![delete2comp](delete2comp.svg) 163 | 164 | ${delete2comp} 165 | 166 | ### Delete large entities 167 | 168 | `N` entities with 10 components `C1`, ..., `C10`. 169 | 170 | * Delete all entities 171 | 172 | ![delete10comp](delete10comp.svg) 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 | Star History Chart 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 | --------------------------------------------------------------------------------