Fast path for hot property getters: stop importing mcrfpy per call; closes #331
Replace the PyImport_ImportModule("mcrfpy") + PyObject_GetAttrString +
full type-call ceremony with direct &mcrfpydef::PyXType references
(safe since all PyTypeObjects are inline C++17 definitions) in every
non-init call site:
- UIDrawable: get_pos/get_origin/get_global_pos now allocate via
PyVector(...).pyObject(); set_pos/set_origin check the Vector type
directly.
- GridLayers: ColorLayer at/subscript wrap via PyColor(...).pyObject()
(also fixes a module refcount leak per read); set/fill/fill_rect,
draw_fov/apply_perspective color parsing, Entity/Texture/HeightMap
instance checks, and TileLayer texture get/set use direct type refs.
- UIGrid init layers=, add_layer/remove_layer/layer(), Grid.layers
getter: direct ColorLayer/TileLayer type refs.
- PyTileSetFile.to_texture, PyLdtkProject.tileset: direct type refs.
Benchmark (tests/benchmarks/issue_331_property_read_bench.py, 200k
reads, release build):
frame.pos 559 ns -> 65 ns/read (8.6x)
frame.origin 571 ns -> 64 ns/read (8.9x)
frame.global_position 589 ns -> 66 ns/read (8.9x)
ColorLayer.at 608 ns -> 262 ns/read (2.3x)
Test suite: 304/304 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DK5DHgRDNGmpN54vnEHAot
This commit is contained in:
parent
80908f771c
commit
182da6230d
8 changed files with 113 additions and 342 deletions
74
tests/benchmarks/issue_331_property_read_bench.py
Normal file
74
tests/benchmarks/issue_331_property_read_bench.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Issue #331: micro-benchmark for hot property getters.
|
||||
|
||||
Times tight-loop reads of properties that (pre-fix) executed
|
||||
PyImport_ImportModule("mcrfpy") + PyObject_GetAttrString + full type call
|
||||
per read, versus the direct tp_alloc fast path (post-fix).
|
||||
|
||||
Direct-execution benchmark: no game loop needed, property reads work
|
||||
immediately after object construction.
|
||||
|
||||
Usage:
|
||||
./mcrogueface --headless --exec tests/benchmarks/issue_331_property_read_bench.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import time
|
||||
|
||||
N = 200_000
|
||||
|
||||
|
||||
def bench(label, fn):
|
||||
start = time.perf_counter()
|
||||
fn()
|
||||
elapsed = time.perf_counter() - start
|
||||
per_read_ns = elapsed / N * 1e9
|
||||
print(f"{label:24s} {elapsed*1000:8.1f} ms total {per_read_ns:8.0f} ns/read")
|
||||
return per_read_ns
|
||||
|
||||
|
||||
def main():
|
||||
frame = mcrfpy.Frame(pos=(10, 20), size=(100, 100))
|
||||
layer = mcrfpy.ColorLayer(name="bench", z_index=0)
|
||||
grid = mcrfpy.Grid(grid_size=(8, 8), layers=[layer])
|
||||
layer.set((3, 3), (10, 20, 30, 255))
|
||||
|
||||
def read_pos():
|
||||
for _ in range(N):
|
||||
frame.pos
|
||||
|
||||
def read_origin():
|
||||
for _ in range(N):
|
||||
frame.origin
|
||||
|
||||
def read_global_pos():
|
||||
for _ in range(N):
|
||||
frame.global_position
|
||||
|
||||
def read_layer_at():
|
||||
for _ in range(N):
|
||||
layer.at((3, 3))
|
||||
|
||||
print(f"issue #331 property-read benchmark, N={N}")
|
||||
results = {
|
||||
"pos": bench("frame.pos", read_pos),
|
||||
"origin": bench("frame.origin", read_origin),
|
||||
"global_pos": bench("frame.global_position", read_global_pos),
|
||||
"layer_at": bench("ColorLayer.at", read_layer_at),
|
||||
}
|
||||
|
||||
# Sanity: values must be correct regardless of construction path
|
||||
p = frame.pos
|
||||
assert (p.x, p.y) == (10.0, 20.0), f"pos wrong: {p}"
|
||||
c = layer.at((3, 3))
|
||||
assert (c.r, c.g, c.b) == (10, 20, 30), f"color wrong: {c}"
|
||||
|
||||
print("PASS")
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue