fix: EntityCollection iterator O(n²) → O(n) with 100× speedup (closes #159)

Problem: EntityCollection iterator used index-based access on std::list,
causing O(n) traversal per element access (O(n²) total for iteration).

Root cause: Each call to next() started from begin() and advanced index steps:
  std::advance(l_begin, self->index-1);  // O(index) for linked list!

Solution:
- Store actual std::list iterators (current, end) instead of index
- Increment iterator directly in next() - O(1) operation
- Cache Entity and Iterator type lookups to avoid repeated dict lookups

Benchmark results (2,000 entities):
- Before: 13.577ms via EntityCollection
- After:  0.131ms via EntityCollection
- Speedup: 103×

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-12-28 00:30:31 -05:00
commit 8f2407b518
3 changed files with 39 additions and 31 deletions

1
.gitignore vendored
View file

@ -26,6 +26,7 @@ mcrogueface.github.io
scripts/ scripts/
tcod_reference tcod_reference
.archive .archive
.mcp.json
# Keep important documentation and tests # Keep important documentation and tests
!CLAUDE.md !CLAUDE.md

View file

@ -2113,28 +2113,23 @@ int UIEntityCollectionIter::init(PyUIEntityCollectionIterObject* self, PyObject*
PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self) PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self)
{ {
// Check for collection modification during iteration
if (self->data->size() != self->start_size) if (self->data->size() != self->start_size)
{ {
PyErr_SetString(PyExc_RuntimeError, "collection changed size during iteration"); PyErr_SetString(PyExc_RuntimeError, "collection changed size during iteration");
return NULL; return NULL;
} }
if (self->index > self->start_size - 1) // Check if we've reached the end
if (self->current == self->end)
{ {
PyErr_SetNone(PyExc_StopIteration); PyErr_SetNone(PyExc_StopIteration);
return NULL; return NULL;
} }
self->index++;
auto vec = self->data.get(); // Get current element and advance iterator - O(1) operation
if (!vec) auto target = *self->current;
{ ++self->current;
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Advance list iterator since Entities are stored in a list, not a vector
auto l_begin = (*vec).begin();
std::advance(l_begin, self->index-1);
auto target = *l_begin;
// Return the stored Python object if it exists (preserves derived types) // Return the stored Python object if it exists (preserves derived types)
if (target->self != nullptr) { if (target->self != nullptr) {
@ -2143,8 +2138,12 @@ PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self)
} }
// Otherwise create and return a new Python Entity object // Otherwise create and return a new Python Entity object
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); // Cache the Entity type to avoid repeated dictionary lookups (#159)
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0); static PyTypeObject* cached_entity_type = nullptr;
if (!cached_entity_type) {
cached_entity_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
}
auto o = (PyUIEntityObject*)cached_entity_type->tp_alloc(cached_entity_type, 0);
auto p = std::static_pointer_cast<UIEntity>(target); auto p = std::static_pointer_cast<UIEntity>(target);
o->data = p; o->data = p;
return (PyObject*)o; return (PyObject*)o;
@ -2153,9 +2152,12 @@ PyObject* UIEntityCollectionIter::next(PyUIEntityCollectionIterObject* self)
PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self) PyObject* UIEntityCollectionIter::repr(PyUIEntityCollectionIterObject* self)
{ {
std::ostringstream ss; std::ostringstream ss;
if (!self->data) ss << "<UICollectionIter (invalid internal object)>"; if (!self->data) ss << "<UIEntityCollectionIter (invalid internal object)>";
else { else {
ss << "<UICollectionIter (" << self->data->size() << " child objects, @ index " << self->index << ")>"; // Calculate current position by distance from end
auto remaining = std::distance(self->current, self->end);
auto total = self->data->size();
ss << "<UIEntityCollectionIter (" << (total - remaining) << "/" << total << " entities)>";
} }
std::string repr_str = ss.str(); std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
@ -3060,26 +3062,30 @@ int UIEntityCollection::init(PyUIEntityCollectionObject* self, PyObject* args, P
PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self) PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self)
{ {
// Get the iterator type from the module to ensure we have the registered version // Cache the iterator type to avoid repeated dictionary lookups (#159)
PyTypeObject* iterType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UIEntityCollectionIter"); static PyTypeObject* cached_iter_type = nullptr;
if (!iterType) { if (!cached_iter_type) {
cached_iter_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UIEntityCollectionIter");
if (!cached_iter_type) {
PyErr_SetString(PyExc_RuntimeError, "Could not find UIEntityCollectionIter type in module"); PyErr_SetString(PyExc_RuntimeError, "Could not find UIEntityCollectionIter type in module");
return NULL; return NULL;
} }
// Keep a reference to prevent it from being garbage collected
Py_INCREF(cached_iter_type);
}
// Allocate new iterator instance // Allocate new iterator instance
PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)iterType->tp_alloc(iterType, 0); PyUIEntityCollectionIterObject* iterObj = (PyUIEntityCollectionIterObject*)cached_iter_type->tp_alloc(cached_iter_type, 0);
if (iterObj == NULL) { if (iterObj == NULL) {
Py_DECREF(iterType);
return NULL; // Failed to allocate memory for the iterator object return NULL; // Failed to allocate memory for the iterator object
} }
iterObj->data = self->data; iterObj->data = self->data;
iterObj->index = 0; iterObj->current = self->data->begin(); // Start at beginning
iterObj->end = self->data->end(); // Cache end iterator
iterObj->start_size = self->data->size(); iterObj->start_size = self->data->size();
Py_DECREF(iterType);
return (PyObject*)iterObj; return (PyObject*)iterObj;
} }

View file

@ -222,8 +222,9 @@ public:
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> data; std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> data;
int index; std::list<std::shared_ptr<UIEntity>>::iterator current; // Actual list iterator - O(1) increment
int start_size; std::list<std::shared_ptr<UIEntity>>::iterator end; // End iterator for bounds check
int start_size; // For detecting modification during iteration
} PyUIEntityCollectionIterObject; } PyUIEntityCollectionIterObject;
class UIEntityCollectionIter { class UIEntityCollectionIter {