Data Locality¶
Intent: Accelerate memory access by arranging data to take advantage of CPU caching.
Acron Mapping¶
Need |
Acron / Python approach |
|---|---|
Contiguous position data |
|
Bulk geometry |
|
Avoiding per-object overhead |
Single manager task iterating a flat list instead of per-entity tasks |
Cache-friendly transforms |
Cache world positions in a numpy array; sync back to |
Python’s GC-managed object model makes true data locality harder than in C++, but the key wins are: avoid pointer chasing in hot loops and use numpy/array.array for bulk numeric data.
How to Implement¶
Identify the hot loop: the code that runs for every entity every frame (positions, velocities, health ticks).
Replace a list of objects with parallel numpy arrays: one for x, one for y, one for z, etc.
Perform math on the arrays (vectorised numpy ops) rather than looping in Python.
Write results back to
NodePathpositions only for visible/active entities.For geometry updates use
GeomVertexWriterto write directly into Acron’s vertex buffer.
Code Sketch (Python)¶
import numpy as np
class BulletSystem:
MAX = 512
def __init__(self):
self.count = 0
# Contiguous arrays — cache-friendly in numpy
self.px = np.zeros(self.MAX, dtype=np.float32)
self.py = np.zeros(self.MAX, dtype=np.float32)
self.vx = np.zeros(self.MAX, dtype=np.float32)
self.vy = np.zeros(self.MAX, dtype=np.float32)
self.alive = np.zeros(self.MAX, dtype=bool)
# Pre-instantiated NodePaths (hidden until active)
proto = loader.loadModel('assets/bullet.bam')
self._nodes = []
for i in range(self.MAX):
np_ = proto.copyTo(render)
np_.hide()
self._nodes.append(np_)
taskMgr.add(self._update, 'bulletSystem', sort=10)
def spawn(self, x, y, vx, vy):
if self.count >= self.MAX:
return
i = self.count
self.px[i], self.py[i] = x, y
self.vx[i], self.vy[i] = vx, vy
self.alive[i] = True
self._nodes[i].show()
self.count += 1
def _update(self, task):
dt = globalClock.getDt()
# Vectorised update — no Python loop over individual bullets
mask = self.alive[:self.count]
self.px[:self.count][mask] += self.vx[:self.count][mask] * dt
self.py[:self.count][mask] += self.vy[:self.count][mask] * dt
# Sync to scene graph only for alive bullets
for i in range(self.count):
if self.alive[i]:
self._nodes[i].setPos(float(self.px[i]), float(self.py[i]), 0)
return task.cont
Pitfalls¶
The bottleneck is usually the Python loop, not cache misses: in Python, vectorised numpy ops beat manual loops regardless of cache effects. Profile first with
PStatsbefore restructuring data.NodePath sync cost:
setPos()on hundreds ofNodePathobjects per frame is expensive. For truly massive counts (particles) use Acron’s particle system or instanced geometry with a custom shader instead of individual nodes.GeomVertexData for geometry: when updating mesh vertex positions (water ripples, cloth), use
GeomVertexWriterrather than moving many childNodePathnodes — it writes directly to the GPU buffer.