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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: microecs
3
- Version: 0.3.1
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.1
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.
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from setuptools import setup, find_packages
4
4
 
5
5
  NAME = "microecs"
6
- VERSION = "0.3.1"
6
+ VERSION = "0.3.2"
7
7
  DESCRIPTION = "MicroECS: Minimal Entity Component System (ECS) in python and numpy"
8
8
  URL = "https://gitlab.com/meehai/microecs"
9
9
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes