Skip to content

pathfinding for __dict__ in Cell#3113

Closed
quaquel wants to merge 26 commits intomesa:mainfrom
quaquel:perf_cell
Closed

pathfinding for __dict__ in Cell#3113
quaquel wants to merge 26 commits intomesa:mainfrom
quaquel:perf_cell

Conversation

@quaquel
Copy link
Copy Markdown
Member

@quaquel quaquel commented Jan 11, 2026

This PR removes __dict__ and @cached_property from Cell, This is in line with #3108 and reduces the memory footprint of a grid. This works because only @cached_property requires Cell.__dict__. Properties, descriptors, but also @cache don't need Cell.__dict__, because they rely on storage at the class-level, not the instance-level.

Motivation for removing __dict__
As indicated by @falloficarus22 in #3108,

Mesa environments often consist of millions of cells. Previously, the Cell class included __dict__ in its__slots__, which resulted in an overhead of approximately 300 bytes per instance (due to the presence of an empty dictionary). For high-resolution grids (e.g., 1000x1000), this contributed ~300MB of unnecessary RAM usage. This bottleneck limited the scalability of complex spatial models.

It also replaces the PropertyDescriptor with a property factory pattern, as suggested by @codebreaker32 in #3080. But, I still need to include the cell.empty read-only content from #3080.

  1. What are the performance implications of this?

So far, it appears that there is a slight performance loss from removing @cached_property. This is largely due to the extra method call from the property to get_neighborhood, which is still cached. Shifting from self.cell.neighborhood to self.cell.get_neighborhood() avoids the indirection, which has a small performance benefit. For Mesa 4.0, we may consider removing cell.neighborhood, as we currently have two different ways of obtaining a neighborhood with a radius of 1 and the center excluded.

Shifting from a descriptor to a property halves the time to access it. There might be room for further improvement here.

  1. cell.empty in main works because of Grid adding a property layer. However, as per Enforce read-only safety for 'empty' layer #3080, empty is a bit of a weird case. Cell.empty is special. The cell itself should be able to write to it, but the user should not be able to. One option is to rename it to _empty to clearly signal that it is used internally and should be left alone by the user. In which case, we can still use property layers. Another option is to avoid using self.add_property_layer and instead work around it for empty.

Regardless, a benefit of shifting from a descriptor to a property factory is that write-only access becomes trivial. The property() function takes as keyword arguments a getter, a setter, and a deleter. By omitting the setter, you have automatically created read-only access.

@quaquel quaquel added 0 - Triage trigger-benchmarks Special label that triggers the benchmarking CI labels Jan 11, 2026
@github-actions

This comment was marked as outdated.

@quaquel quaquel added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Jan 11, 2026
@github-actions

This comment was marked as outdated.

@quaquel quaquel removed the trigger-benchmarks Special label that triggers the benchmarking CI label Jan 11, 2026
@quaquel quaquel added the trigger-benchmarks Special label that triggers the benchmarking CI label Jan 11, 2026
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔴 +3.5% [+3.2%, +3.9%] 🔴 +4.2% [+4.1%, +4.4%]
BoltzmannWealth large 🔴 +4.2% [+3.7%, +4.8%] 🔵 +0.5% [+0.2%, +0.8%]
Schelling small 🔵 +0.6% [-0.0%, +1.0%] 🔵 -2.0% [-2.3%, -1.8%]
Schelling large 🔵 -1.6% [-2.1%, -1.2%] 🔵 -2.4% [-2.8%, -2.0%]
WolfSheep small 🔵 +1.9% [+1.7%, +2.2%] 🔵 +1.6% [+1.4%, +1.8%]
WolfSheep large 🔵 +1.4% [-2.1%, +3.3%] 🔵 -0.2% [-0.9%, +0.3%]
BoidFlockers small 🔵 +1.3% [+1.0%, +1.6%] 🔵 +0.9% [+0.7%, +1.1%]
BoidFlockers large 🔵 +2.5% [+2.0%, +3.0%] 🔵 +0.6% [+0.3%, +0.9%]

EwoutH and others added 2 commits January 11, 2026 13:05
Switch the Ruff pre-commit hook from the deprecated `ruff` legacy alias to the canonical `ruff-check` hook ID, aligning the configuration with upstream changes in `ruff-pre-commit` and eliminating the “legacy alias” warning while preserving existing linting and auto-fix behavior.

See astral-sh/ruff-pre-commit#124
Copy link
Copy Markdown
Collaborator

@codebreaker32 codebreaker32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will be faster

@falloficarus22
Copy link
Copy Markdown
Contributor

falloficarus22 commented Jan 11, 2026

In grid.py we have

self.cell_klass = type(
            "GridCell",
            (self.cell_klass,),
            {"_mesa_properties": set()},
        )

Since this GridCell subclass doesn't define slots, Python will automatically give it a dict, defeating the purpose.

@quaquel
Copy link
Copy Markdown
Member Author

quaquel commented Jan 11, 2026

Since this GridCell subclass doesn't define slots, Python will automatically give it a dict, defeating the purpose.

I have updated the PR to include this change, as well as added a test for it (and fixed it in a few other places not covered in #3108).

@quaquel quaquel added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Jan 11, 2026
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.9% [-0.3%, +2.0%] 🔵 +2.9% [+2.8%, +3.1%]
BoltzmannWealth large 🔵 +0.5% [-2.2%, +2.3%] 🔵 +0.6% [-1.1%, +2.6%]
Schelling small 🔵 -2.2% [-3.0%, -1.6%] 🔵 -2.7% [-3.2%, -2.4%]
Schelling large 🔵 -2.3% [-2.7%, -1.9%] 🔵 -3.6% [-5.1%, -1.6%]
WolfSheep small 🔵 -0.0% [-0.3%, +0.3%] 🔵 -0.5% [-0.7%, -0.3%]
WolfSheep large 🔵 +3.6% [+2.8%, +4.4%] 🔵 +1.7% [+0.3%, +3.7%]
BoidFlockers small 🔵 -0.4% [-0.8%, -0.1%] 🔵 +0.7% [+0.5%, +0.9%]
BoidFlockers large 🔵 +0.4% [-0.1%, +0.8%] 🔵 +0.5% [+0.1%, +0.9%]

@quaquel
Copy link
Copy Markdown
Member Author

quaquel commented Jan 12, 2026

I split of the dict/slots stuff into its own stand alone PR. I now understand how it works and its a nice stand alone PR.

I also split of the PropertyLayer.from_data bugfix and the property factory pattern into stand alone PR's. Once those are all merged, I'll return here and see what's left. the main things seems to be #3080/#3072.

@quaquel quaquel closed this Jan 15, 2026
@quaquel quaquel deleted the perf_cell branch February 4, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

0 - Triage trigger-benchmarks Special label that triggers the benchmarking CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants