Skip to content

Attribute cache silently disabled after 1000 class edit, leading to poor performance #156339

Open

Description

@perrinjerome

Bug report

Bug description:

After the changes from #114900 modifying a class more than 1000 times causes the attribute cache to be permanently disabled for that class. This was done to solve a crash when a class is modified more than 2**32 times ( #113462 )

It seems this number (1000) is too low for some real case applications.

I initially observed this as a ~50% performance regression in ERP5 performance tests when updating from python3.11 to python3.13. ERP5 is an application where classes are generated dynamically based on some configuration and some classes are modified more than 1000 times in the "normal" generation process.

This seems to also affect odoo, another ERP application, for this they resorted to using ctypes to reset the class version attribute ( odoo/odoo#275640 )

My suggestion would be to use a much higher number before disabling the cache, 1000 is low.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Activity

  1. added
    performancePerformance or resource usage
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    on Aug 25, 2026
  2. StanFromIreland commented on Aug 25, 2026

    @StanFromIreland
    Member
  3. markshannon commented on Aug 26, 2026

    @markshannon
    Member

    The limit (1000) was chosen quite arbitrarily.
    What limit would be sufficient for your use case?

  4. perrinjerome commented on Aug 26, 2026

    @perrinjerome
    ContributorAuthor

    Thank you. I tried running a patched interpreter with 10 000 instead of 1000 and it was enough for my case, but I suspect there can even be cases with more than 10 000 edits.

    If the original problem starts with 2**31, maybe the limit can be set to something much higher, would something like 2**30 be possible ?

  5. markshannon commented on Aug 26, 2026

    @markshannon
    Member

    maybe the limit can be set to something much higher, would something like 2**30

    The sum of all modifications for all classes should not reach 2**32.
    Allowing 2**30 for a single class could be broken by 4 classes.

    I understand why reloading a model would make many changes, but why the interleaved attribute loads?
    Typically, how many loads occur between each change to the class?

    I'm actually reluctant to increase the number much, for internal reasons to do with detecting the difference between cache misses due to polymorphism vs stale version numbers.

    What we could do, is add a counter that tracks the number of times we want to update the version number, and only update the version number when that reaches the number of versions used. Instead of N changes before the cache stopped working it would be N*(N-1)/2 changes.

    That should allow about 500k version changes, for N=1000, at the cost of slower look ups for the first several lookups after a change to the class.
    For a class that makes 1000 changes, it would take about 50 lookups before the cache kicks in after the 1000th change/load pair.

  6. perrinjerome commented on Aug 28, 2026

    @perrinjerome
    ContributorAuthor

    I understand why reloading a model would make many changes, but why the interleaved attribute loads?
    Typically, how many loads occur between each change to the class?

    The simplified pattern is something like this:

    for attribute_name, attribute in get_dynamic_attributes():
      if not hasattr(cls, attribute_name):
        setattr(cls, attribute_name, attribute)

    so each hasattr performs a load and we have a load directly after each class modification.

    I realize today I can probably just rewrite this pattern to:

    class_attributes = set(dir(cls))
    for method_name, method in get_dynamic_attributes():
      if attribute_name not in class_attributes:
        setattr(cls, attribute_name, attribute)

    with this approach, there is no load so the version is not increased. Building the class would also probably be slightly faster because it would not rebuild the cache in each loop, so I feel it's generally good if I can change my code like this. Thank you for helping me realize this, I will try this in the next days.

    Still, from this experience, I feel this change of disabling the cache after 1000 class modifications is not ideal, for users it manifests as a performance degradation and the root cause is hard to find. I still have the intuition that it could be increased a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions