New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type cache is not correctly invalidated on a class defining mro() #73052
Comments
I ran into a strange bug while experimenting with metaclasses that implement a custom mro() method. It only seems to occur in interactive mode, either returning completely unrelated values or causing a segfault. Python 2 appears unaffected. I have been able to reproduce this with the following code: # $ python -i weird.py
# >>> proxy.x
# 52011448
# >>> proxy.x
# 6160
# ... class Foo:
pass
class Meta(type):
def mro(cls):
return (cls, Foo, object)
def __setattr__(cls, name, value):
setattr(Foo, name, value)
proxy = Meta('FooProxy', (), {})
proxy.x = 300
proxy.x # don't omit
proxy.x = 0 |
Python 2 is affected if make Foo a new style class. |
Additionally, trying to access an attribute before assigning it will result in an AttributeError on all subsequent accesses (even if set). I didn't manage to get a segfault, however. |
FWIW, in _PyType_Lookup I see the "300" PyLong being cached, and later, just before the segfault, I see its address getting out of the cache (a cache hit) but it's no longer a PyLong, it's scrambled, so we're getting a real pointer with scrambled values on the line: attribute = _PyType_Lookup(type, name); segfaulting two lines later in:
When I write scrambled value I mean:
To debug interactive session in GDB I used:
with "proxy.x" in the stdin file. |
I'm able to reproduce the segmentation fault outside the interactive mode, see attached file. |
Problem looks mainly due to the __setattr__ done on a different type than the getattr, and the cache of PyType_Lookup:
The timeline (with my weird_without_interactive.py) is:
|
If you call sys._clear_type_cache() after the setattr(), weird_without_interactive.py doesn't crash anymore. |
I found a way to fix it, but as I'm just discovering cpython internals, I'm not sure it's the right way, review are very welcome. I fixed it this way because: The bytecode generated for "proxy.x = 0" (a STORE_ATTR) will call a The actual attribute setting is done from the PyObject_SetAttr with Foo (calling in turn type_setattro, and so on), but it's already too late to invalidate the FooProxy type: we no longer have a reference on it and can't guess that FooProxy delegated __setattr__ to Foo. So the only place when we can invalidate correctly is in the first call of PyObject_SetAttr when we know on which type the attribute will be set, and it make sense: It does not matter what a __setattr__ does and how it does it: invalidation should happen for this attribute on the current type, so invalidating here seems logic. I did not yet took the time to measure performance loss induced with this patch. With the patch: ./python -i weird.py
>>> proxy.x
0
>>> proxy.x
0 |
The issue is that the Meta class has a reference to the class Foo in its mro() method, but Foo is not aware of Meta. So when Foo is modified, the Foo cache is invalidated, but not Meta cache. bpo-28866.diff always invalidates the cache, so it works. But it is suboptimal, IMO it defeats the whole purpose of a cache. I never defined a mro() method. I'm not sure that it's possible to have a type cache and a mro() method? Options:
|
This was implemented when the type cache was implemented in Python 2.6, but only for explicit subclasses. PyType_Modified() iterates on tp_subclasses. PyType_Ready() updates tp_subclasses: it stores a weak reference to sub classes in each base class. I understand that, if we want to implement this feature, type_mro_modified() should be modified to add a backward reference in each base class of the MRO. type_mro_modified() is called when a type is defined, but also when type.__bases__ is explicitly modified. It would require to add a new slot to types, and so increase a little bit the memory usage, and slow down the creation of a type, and type.__bases__ (slow down: probably negligible, O(1) since the existing tp_subclasses uses a dict). |
Not sure about defeating the purpose of the cache as I only invalidate in setattr, getattr are still cache hitting. I tried: $ python3 -m performance run --python=./python --rigorous -b all -o master.json
$ git checkout issue28866; make -j 8
$ python3 -m performance run --python=./python --rigorous -b all -o issue28866.json
$ python3 -m performance compare master.json issue28866.json And I don't see much differences, probably only noise as I get some faster tests: ### call_method_unknown ### ### pybench.IfThenElse ### and some slower: ### unpack_sequence ### I'm not yet accustomed to this perf suite, so I may miss something obvious. I'll ran it again on master to measure the noise and should probably fine-tune my system for stability. I'll also try a benchmark without the cache for comparison. |
Hi, Tried again, this time getting some stats with MCACHE_STATS 1, to check if my patch is defeating the cache: Without my patch: $ time ./python performance/benchmarks/bm_chaos.py --worker -l1 -w0 -n1 --filename chaos.ppm --width=512 --height=512 --iterations 50000
chaos: Median: 2.51 sec
-- Method cache hits = 16581735 (99%)
-- Method cache true misses = 4092 (0%)
-- Method cache collisions = 28542 (0%)
-- Method cache size = 96 KB With my patch: $ time ./python performance/benchmarks/bm_chaos.py --worker -l1 -w0 -n1 --filename chaos.ppm --width=512 --height=512 --iterations 50000
chaos: Median: 2.53 sec
-- Method cache hits = 16582260 (99%)
-- Method cache true misses = 4096 (0%)
-- Method cache collisions = 28012 (0%)
-- Method cache size = 96 KB |
This failure appears to be a symptom of recursively traversing __bases__ rather scanning __mro__ in the implementation of type.__subclasses__ python -i weird.py
>>> C = Meta("C", (), {})
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.Foo'>, <class 'object'>)
>>> Foo.__subclasses__()
[]
>>> C.__bases__
(<class 'object'>,) Fixing this may need a change in the API for type.__subclasses__() to return all subclasses, as defined by __mro__, not just the bases. A simpler, temporary fix might be to set Py_TPFLAGS_HAVE_VERSION_TAG to 0 for any class that has a custom mro() |
Well done Julien |
Python 2.7 and 3 7 should be fixed as well, no? |
I think I can backport it to 3.7, and I let you choose if 2.7 need this. |
Python 2.7 is also affected according to Serhiy. Does someone want to write a backport to 2.7? |
A refcount problem possibly caused by the fix: https://bugs.python.org/issue39016 |
Steve, I would like to add your reproducer (weird.py) to CPython's test suite, to make sure this doesn't happen again. Would you be willing to sign the contributor agreement, so we can use your code in Python? The instructions are here: https://www.python.org/psf/contrib/contrib-form/ |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: