Types with `mro()` unconditionally disables type attribute cache, causing performance regression for compatible MRO implementations, such as Zope
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Ambito
- backend, performance
Direzione di ricerca
Inizia in Objects/typeobject.c, all'interno di type_mro_modified(), e leggi la logica circostante di invalidazione della cache. Confronta quindi il comportamento dell'MRO personalizzato collegato con il commit di bozza proposto. Il lavoro è completo quando viene preservata l'invalidazione della cache per le voci MRO inserite al di fuori di tp_bases, mantenendo al contempo la cache per gli MRO personalizzati compatibili, con copertura di regressione per entrambi i casi.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Bug report
Bug description:
Since https://github.com/python/cpython/issues/127773 / https://github.com/python/cpython/pull/127924, any type whose metaclass defines a custom mro() method has its type attribute cache permanently disabled. This was made initially to fix a problem with metaclasses defining mro() to inject new bases that are not in the class bases.
This affects Zope's ExtensionClass, used as base class for most classes the Zope ecosystem. This package has 5000 daily downloads on pypi ( https://pypistats.org/packages/extensionclass ). ExtensionClass defines a custom mro(), but only to reorganizes bases that are already bases of the class, which is different from the scenario from the issue and it looks like a case where the cache could still be used.
Compared to python3.11, this seems to causes a ~3x performance regression for attribute lookups on all zope classes:
$ uv run --python=3.11 --with Zope python -m timeit -s 'import OFS.Folder; f = OFS.Folder.Folder("f")' 'f.getId'
2000000 loops, best of 5: 116 nsec per loop
$ uv run --python=3.14 --with Zope python -m timeit -s 'import OFS.Folder; f = OFS.Folder.Folder("f")' 'f.getId'
500000 loops, best of 5: 399 nsec per loop
it's hard to tell how much this affect applications, this is just a micro benchmark, but we are also observing that running ERP5 test suites (a complex application based on zope) is slower on python3.13 than it was on python3.11.
Analysis
In type_mro_modified(), the check has_custom_mro(type) unconditionally jumps to clear:
which sets tp_versions_used = _Py_ATTR_CACHE_UNUSED, whenever the metaclass overrides mro():
This was introduced to fix https://github.com/python/cpython/issues/127773, where a custom mro() that injects a class not in tp_bases (e.g. Base) causes stale cache entries because PyType_Modified() only propagates through tp_subclasses, not through MRO-only relationships.
Although these are not much relevant here, ExtensionClass's mro is implemented in C as _ExtensionClass.c:569-626 and also has an equivalent pure-python implementation in ExtensionClass.__init__.py:181-200.
Suggested fix
Check whether the custom MRO actually contains non-base entries before disabling the cache. This requires an is_superclass() helper that walks tp_bases (not tp_mro) to determine reachability:
// Return true if `super` is reachable from `sub` through `tp_bases`,
// as opposed to a type merely injected into the MRO by a custom `mro()`
// implementation. Only entries reachable through `tp_bases` are covered
// by the version-tag invalidation that _PyType_Modified_Unlocked()
// propagates through `tp_subclasses`.
static int
is_superclass(PyTypeObject *super, PyTypeObject *sub)
{
PyObject *bases = lookup_tp_bases(sub);
if (bases == NULL) {
return 0;
}
Py_ssize_t n = PyTuple_GET_SIZE(bases);
for (Py_ssize_t i = 0; i < n; i++) {
PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, i));
if (base == super || is_superclass(super, base)) {
return 1;
}
}
return 0;
}
Then in type_mro_modified, replace the unconditional goto clear for custom MROs:
if (!Py_IS_TYPE(type, &PyType_Type) && has_custom_mro(type)) {
// Only disable cache if the custom mro() injected an entry that
// is not reachable through tp_bases;
PyObject *mro = lookup_tp_mro(type);
if (mro == NULL) {
goto clear;
}
Py_ssize_t mro_size = PyTuple_GET_SIZE(mro);
for (Py_ssize_t m = 0; m < mro_size; m++) {
PyTypeObject *entry = _PyType_CAST(PyTuple_GET_ITEM(mro, m));
if (entry != type && !is_superclass(entry, type)) {
goto clear;
}
}
}
I'm not at all familiar with this, but it seems to me that this would preserve the correctness of https://github.com/python/cpython/pull/127924 for types that do inject non-base entries into their MRO (because cache would still be disabled for those), while restoring cache performance for compatible custom MRO implementations like Zope's ExtensionClass.
If this approach makes sense, I already have a draft commit implementing this: https://github.com/perrinjerome/cpython/commit/89af3385eb7e930466dcaef97b2d7b9286eb755e and I would be happy to clean it up and make a pull request.
CPython versions tested on:
3.13
Operating systems tested on:
Linux
- Lingua principale
- Python
- Stelle
- 77.2k
- Fork
- 36k
- Merge medio
- 1g 9h
- PR unite (30g)
- 558
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di python/cpython
-
docs pending
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
stdlib type-feature
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
stdlib type-feature
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
build type-bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
-
stdlib topic-email type-feature
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
Tutte le issue di python/cpython
Issue simili
-
link-check link-check:sphinx-theme
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
qgis/QGIS-Documentation#11275 ·
-
bug priority:normal ready-for-dev
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
OpenHands/extensions#626 · 1 commento ·
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
CSCfi/sd-search-api#39 ·
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100