microecs 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microecs-0.3.1 → microecs-0.3.2}/PKG-INFO +36 -1
- {microecs-0.3.1 → microecs-0.3.2}/README.md +35 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs/pool.py +1 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs.egg-info/PKG-INFO +36 -1
- {microecs-0.3.1 → microecs-0.3.2}/setup.py +1 -1
- {microecs-0.3.1 → microecs-0.3.2}/LICENSE.TXT +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs/__init__.py +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs/component.py +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs/query_result.py +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs/utils.py +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs/world.py +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs.egg-info/SOURCES.txt +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs.egg-info/dependency_links.txt +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs.egg-info/requires.txt +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/microecs.egg-info/top_level.txt +0 -0
- {microecs-0.3.1 → microecs-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: microecs
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: MicroECS: Minimal Entity Component System (ECS) in python and numpy
|
|
5
5
|
Home-page: https://gitlab.com/meehai/microecs
|
|
6
6
|
License: MIT
|
|
@@ -117,3 +117,38 @@ Edge cases worth knowing:
|
|
|
117
117
|
longer matches the pool's row count.
|
|
118
118
|
- **Operands must come from the same query.** Alignment is per-pool, not by flat index, so don't
|
|
119
119
|
mix a `_Field` from one `world.query(...)` into an op on another.
|
|
120
|
+
|
|
121
|
+
## Benchmark: ECS vs OOP
|
|
122
|
+
|
|
123
|
+
The same physics step -- `pos += vel*dt` over N=100k entities split across 2 pools -- computed 8
|
|
124
|
+
ways, all verified to produce the identical result. Reproduce with
|
|
125
|
+
`python test/manual/perf/bench_ecs_vs_oop.py` (it prints `{mode: avg_seconds_per_step}`).
|
|
126
|
+
|
|
127
|
+
| pattern | ns/entity | vs OOP-scalar |
|
|
128
|
+
|---|---:|---:|
|
|
129
|
+
| `micro-ecs-pool-vectorized` — `for pool: pool.f[:] = pool.f + …` | 0.9 | **52× faster** |
|
|
130
|
+
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `_Field`) | 1.8 | **27× faster** |
|
|
131
|
+
| **`oop-scalar`** — `for o: o.x += o.vx*dt` (python floats) | 48 | 1× (baseline) |
|
|
132
|
+
| `oop-numpy` — objects holding `(2,)` numpy arrays | 605 | 13× slower |
|
|
133
|
+
| `micro-ecs-zip-rows` — `for p, v in zip(qr.pos, qr.vel)` | 744 | 15× slower |
|
|
134
|
+
| `micro-ecs-pool-loop` — `for pool: for i: pool.f[i]` | 870 | 18× slower |
|
|
135
|
+
| `micro-ecs-get-entity` — `world.get_entity(eid)` per entity | 1674 | 35× slower |
|
|
136
|
+
| `micro-ecs-index` — `qr.f[i]` per entity (an `np.searchsorted` each) | 4573 | 95× slower |
|
|
137
|
+
|
|
138
|
+
*Intel Core Ultra 7 165H, single-threaded, python 3.12 / numpy 2.2. ns/entity = seconds/step ÷ N.*
|
|
139
|
+
|
|
140
|
+
Three things to take from it:
|
|
141
|
+
|
|
142
|
+
1. **Vectorized wins big.** Batched ops (`_Field` or per-pool) run at 1–2 ns/entity — **27–52×
|
|
143
|
+
faster** than the *fastest* OOP loop. Same for data-parallel branches: an `np.where` clamp or
|
|
144
|
+
bounce is ~34× faster than a per-entity `if`.
|
|
145
|
+
2. **Per-entity loops are a cliff, not a tie.** Every per-entity microecs path is **15–95× slower**
|
|
146
|
+
than idiomatic float-based OOP — because microecs is numpy-backed, so a per-entity step pays
|
|
147
|
+
numpy's tiny-array overhead (`oop-numpy` shows the same ~13× tax). One unavoidable per-entity
|
|
148
|
+
pass (~750 ns/entity) costs ~500× a vectorized op (~1.5 ns) and will dominate the frame.
|
|
149
|
+
3. **If you must loop, loop right.** `zip`-rows (15×) < pool-loop (18×) < `get_entity` (35×) <
|
|
150
|
+
`qr.f[i]` (95× — never in a hot loop; `qr.f[i] += …` also raises, you must bind the row first).
|
|
151
|
+
|
|
152
|
+
**Rule of thumb:** keep systems vectorized and push branches into `np.where` / `np.clip`. If a
|
|
153
|
+
workload is *irreducibly* per-entity (data-dependent control flow), plain python objects beat
|
|
154
|
+
microecs ~15× — use them there. microecs is the right tool for **vectorizable** simulation.
|
|
@@ -105,3 +105,38 @@ Edge cases worth knowing:
|
|
|
105
105
|
longer matches the pool's row count.
|
|
106
106
|
- **Operands must come from the same query.** Alignment is per-pool, not by flat index, so don't
|
|
107
107
|
mix a `_Field` from one `world.query(...)` into an op on another.
|
|
108
|
+
|
|
109
|
+
## Benchmark: ECS vs OOP
|
|
110
|
+
|
|
111
|
+
The same physics step -- `pos += vel*dt` over N=100k entities split across 2 pools -- computed 8
|
|
112
|
+
ways, all verified to produce the identical result. Reproduce with
|
|
113
|
+
`python test/manual/perf/bench_ecs_vs_oop.py` (it prints `{mode: avg_seconds_per_step}`).
|
|
114
|
+
|
|
115
|
+
| pattern | ns/entity | vs OOP-scalar |
|
|
116
|
+
|---|---:|---:|
|
|
117
|
+
| `micro-ecs-pool-vectorized` — `for pool: pool.f[:] = pool.f + …` | 0.9 | **52× faster** |
|
|
118
|
+
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `_Field`) | 1.8 | **27× faster** |
|
|
119
|
+
| **`oop-scalar`** — `for o: o.x += o.vx*dt` (python floats) | 48 | 1× (baseline) |
|
|
120
|
+
| `oop-numpy` — objects holding `(2,)` numpy arrays | 605 | 13× slower |
|
|
121
|
+
| `micro-ecs-zip-rows` — `for p, v in zip(qr.pos, qr.vel)` | 744 | 15× slower |
|
|
122
|
+
| `micro-ecs-pool-loop` — `for pool: for i: pool.f[i]` | 870 | 18× slower |
|
|
123
|
+
| `micro-ecs-get-entity` — `world.get_entity(eid)` per entity | 1674 | 35× slower |
|
|
124
|
+
| `micro-ecs-index` — `qr.f[i]` per entity (an `np.searchsorted` each) | 4573 | 95× slower |
|
|
125
|
+
|
|
126
|
+
*Intel Core Ultra 7 165H, single-threaded, python 3.12 / numpy 2.2. ns/entity = seconds/step ÷ N.*
|
|
127
|
+
|
|
128
|
+
Three things to take from it:
|
|
129
|
+
|
|
130
|
+
1. **Vectorized wins big.** Batched ops (`_Field` or per-pool) run at 1–2 ns/entity — **27–52×
|
|
131
|
+
faster** than the *fastest* OOP loop. Same for data-parallel branches: an `np.where` clamp or
|
|
132
|
+
bounce is ~34× faster than a per-entity `if`.
|
|
133
|
+
2. **Per-entity loops are a cliff, not a tie.** Every per-entity microecs path is **15–95× slower**
|
|
134
|
+
than idiomatic float-based OOP — because microecs is numpy-backed, so a per-entity step pays
|
|
135
|
+
numpy's tiny-array overhead (`oop-numpy` shows the same ~13× tax). One unavoidable per-entity
|
|
136
|
+
pass (~750 ns/entity) costs ~500× a vectorized op (~1.5 ns) and will dominate the frame.
|
|
137
|
+
3. **If you must loop, loop right.** `zip`-rows (15×) < pool-loop (18×) < `get_entity` (35×) <
|
|
138
|
+
`qr.f[i]` (95× — never in a hot loop; `qr.f[i] += …` also raises, you must bind the row first).
|
|
139
|
+
|
|
140
|
+
**Rule of thumb:** keep systems vectorized and push branches into `np.where` / `np.clip`. If a
|
|
141
|
+
workload is *irreducibly* per-entity (data-dependent control flow), plain python objects beat
|
|
142
|
+
microecs ~15× — use them there. microecs is the right tool for **vectorizable** simulation.
|
|
@@ -31,6 +31,7 @@ class Pool:
|
|
|
31
31
|
|
|
32
32
|
for _field, field_shape, field_dtype in zip(self.fields, self.shapes, self.dtypes):
|
|
33
33
|
new_item: np.ndarray = entity_fields[_field] # checked in World._get_entity_pool(entity).
|
|
34
|
+
assert isinstance(new_item, np.ndarray), f"{_field=} {new_item=} {type(new_item)}"
|
|
34
35
|
assert new_item.shape == field_shape, f"{_field=} {new_item=}, {new_item.shape=}, {field_shape=}"
|
|
35
36
|
assert np.issubdtype(new_item.dtype, field_dtype), f"{_field=} {new_item=} {new_item.dtype=} {field_dtype=}"
|
|
36
37
|
self.data[_field][self.size] = new_item
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: microecs
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: MicroECS: Minimal Entity Component System (ECS) in python and numpy
|
|
5
5
|
Home-page: https://gitlab.com/meehai/microecs
|
|
6
6
|
License: MIT
|
|
@@ -117,3 +117,38 @@ Edge cases worth knowing:
|
|
|
117
117
|
longer matches the pool's row count.
|
|
118
118
|
- **Operands must come from the same query.** Alignment is per-pool, not by flat index, so don't
|
|
119
119
|
mix a `_Field` from one `world.query(...)` into an op on another.
|
|
120
|
+
|
|
121
|
+
## Benchmark: ECS vs OOP
|
|
122
|
+
|
|
123
|
+
The same physics step -- `pos += vel*dt` over N=100k entities split across 2 pools -- computed 8
|
|
124
|
+
ways, all verified to produce the identical result. Reproduce with
|
|
125
|
+
`python test/manual/perf/bench_ecs_vs_oop.py` (it prints `{mode: avg_seconds_per_step}`).
|
|
126
|
+
|
|
127
|
+
| pattern | ns/entity | vs OOP-scalar |
|
|
128
|
+
|---|---:|---:|
|
|
129
|
+
| `micro-ecs-pool-vectorized` — `for pool: pool.f[:] = pool.f + …` | 0.9 | **52× faster** |
|
|
130
|
+
| `micro-ecs-vectorized` — `qr.f[:] = qr.f + …` (the `_Field`) | 1.8 | **27× faster** |
|
|
131
|
+
| **`oop-scalar`** — `for o: o.x += o.vx*dt` (python floats) | 48 | 1× (baseline) |
|
|
132
|
+
| `oop-numpy` — objects holding `(2,)` numpy arrays | 605 | 13× slower |
|
|
133
|
+
| `micro-ecs-zip-rows` — `for p, v in zip(qr.pos, qr.vel)` | 744 | 15× slower |
|
|
134
|
+
| `micro-ecs-pool-loop` — `for pool: for i: pool.f[i]` | 870 | 18× slower |
|
|
135
|
+
| `micro-ecs-get-entity` — `world.get_entity(eid)` per entity | 1674 | 35× slower |
|
|
136
|
+
| `micro-ecs-index` — `qr.f[i]` per entity (an `np.searchsorted` each) | 4573 | 95× slower |
|
|
137
|
+
|
|
138
|
+
*Intel Core Ultra 7 165H, single-threaded, python 3.12 / numpy 2.2. ns/entity = seconds/step ÷ N.*
|
|
139
|
+
|
|
140
|
+
Three things to take from it:
|
|
141
|
+
|
|
142
|
+
1. **Vectorized wins big.** Batched ops (`_Field` or per-pool) run at 1–2 ns/entity — **27–52×
|
|
143
|
+
faster** than the *fastest* OOP loop. Same for data-parallel branches: an `np.where` clamp or
|
|
144
|
+
bounce is ~34× faster than a per-entity `if`.
|
|
145
|
+
2. **Per-entity loops are a cliff, not a tie.** Every per-entity microecs path is **15–95× slower**
|
|
146
|
+
than idiomatic float-based OOP — because microecs is numpy-backed, so a per-entity step pays
|
|
147
|
+
numpy's tiny-array overhead (`oop-numpy` shows the same ~13× tax). One unavoidable per-entity
|
|
148
|
+
pass (~750 ns/entity) costs ~500× a vectorized op (~1.5 ns) and will dominate the frame.
|
|
149
|
+
3. **If you must loop, loop right.** `zip`-rows (15×) < pool-loop (18×) < `get_entity` (35×) <
|
|
150
|
+
`qr.f[i]` (95× — never in a hot loop; `qr.f[i] += …` also raises, you must bind the row first).
|
|
151
|
+
|
|
152
|
+
**Rule of thumb:** keep systems vectorized and push branches into `np.where` / `np.clip`. If a
|
|
153
|
+
workload is *irreducibly* per-entity (data-dependent control flow), plain python objects beat
|
|
154
|
+
microecs ~15× — use them there. microecs is the right tool for **vectorizable** simulation.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|