gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__ (alternative)#141171
gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__ (alternative)#141171dolfinus wants to merge 8 commits into
Conversation
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
1 similar comment
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
b3bbd15 to
a7ef5b0
Compare
a7ef5b0 to
6869658
Compare
|
pyperformance results doesn't show a much difference: sudo ./venv/bin/python -m pyperf system tune
./venv/bin/python -m pyperformance run --rigorous --affinity 0,1,2,3,4,5main_cd4d0ae75c_rigorous_affinity.json DetailsBenchmarks with tag 'apps':
Benchmark hidden because not significant (4): 2to3, docutils, html5lib, tornado_http Benchmarks with tag 'asyncio':
Benchmark hidden because not significant (11): async_tree_none_tg, async_tree_eager, async_tree_memoization, async_tree_eager_memoization_tg, asyncio_websockets, async_tree_io_tg, asyncio_tcp_ssl, async_tree_memoization_tg, async_tree_io, async_tree_none, async_tree_cpu_io_mixed_tg Benchmarks with tag 'math':
Benchmark hidden because not significant (1): float Benchmarks with tag 'regex':
Benchmark hidden because not significant (1): regex_compile Benchmarks with tag 'serialize':
Benchmark hidden because not significant (4): pickle_pure_python, pickle_dict, pickle, unpickle Benchmarks with tag 'startup':
Benchmarks with tag 'template':
All benchmarks:
Benchmark hidden because not significant (36): bench_mp_pool, html5lib, meteor_contest, scimark_sor, docutils, pprint_pformat, regex_compile, pickle_pure_python, async_tree_none_tg, pickle_dict, async_tree_eager, tornado_http, dask, pyflate, go, scimark_sparse_mat_mult, async_tree_memoization, pickle, async_tree_eager_memoization_tg, asyncio_websockets, many_optionals, sqlglot_v2_normalize, pprint_safe_repr, 2to3, float, unpickle, async_tree_io_tg, asyncio_tcp_ssl, async_tree_memoization_tg, async_tree_io, sqlite_synth, sqlalchemy_imperative, sqlglot_v2_optimize, sqlglot_v2_transpile, async_tree_none, async_tree_cpu_io_mixed_tg |
|
If class Number(ABC): ...
class Real(Number): ...
Real.register(int)
Real.register(float)
assert issubclass(int, Number)
assert issubclass(float, Number)then it is not required anymore. In this case I haven't found any uses of the such overrides with ABC/ABCMeta, other than implementing custom non-ABC metaclasses or using this as typeshed annotations in UPD: implementation from #150540 drops recursive subclass check completely, and does not mess around with class attributes. |
|
This PR is stale because it has been open for 30 days with no activity. |
Documentation build overview
|
|
Rebased to main branch and rerun benchmarks. Added test case with unrelated class tree. |
_abc._abc_subclasscheckhas very poor performance and (I think) a memory leak #92810Alternative implementation of #131914 which doesn't modify class attributes on every
__subclasscheck__call. Instead it checks in__new__does current class havedef __subclasses__(cls)overriden, and if so, enables casefor scls in cls.__subclasses__(), which is disabled by default.To handle cases like:
which previously were implemented via
__subclasses__, methodcls.register(subclass)is callingsuper(cls).register(subclass)recursively ("bubble-up" registration to all the parents).For benchmark from #131914:
isinstance(child, Parent)4MiB...15MiB
4MiB...15MiB
11MiB...24MiB
11MiB...24MiB
issubclass(Child, Parent)0MiB...1MiB
0MiB...1MiB
6MiB...8MiB
6MiB...8MiB
isinstance(child, Grandparent)0MiB...2MiB
0MiB...2MiB
4MiB...7MiB
4MiB...7MiB
issubclass(Child, Grandparent)0MiB
0MiB
0MiB...1MiB
0MiB...1MiB
not isinstance(child, Sibling)4MiB...14MiB
4MiB...15MiB
13MiB...23MiB
13MiB...23MiB
not issubclass(Child, Sibling)1MiB
1MiB
8MiB...10MiB
9MiB...11MiB
not isinstance(child, Cousin)1MiB...2MiB
1MiB...2MiB
7MiB...9MiB
7MiB...9MiB
not issubclass(Child, Cousin)0MiB
0MiB
4MiB
4MiB...5MiB
not isinstance(child, Uncle)6174MiB...6333MiB
0MiB...1MiB
4382MiB...4422MiB
6MiB
not issubclass(Child, Uncle)6171MiB
0MiB
4380MiB
4MiB
Memory increment is measured during
isinstance()/issubclass()calls, not during preparation, like class creation or registration where actual registry allocation is performed. So memory usage in tables below is almost always 0.Timing drop in _py_abc implementation for 2 first rows is due to
if subclass in cls._abc_registry:check added to match _abc.c implementation.isinstance(child, Parent.register)0MiB
0MiB
0MiB
0MiB
issubclass(Child, Parent.register)0MiB
0MiB
0MiB
0MiB
isinstance(child, Grandparent.register)0MiB
0MiB
0MiB
0MiB
issubclass(Child, Grandparent.register)0MiB
0MiB
0MiB
0MiB
not isinstance(child, Sibling.register)0MiB
1MiB
0MiB
2MiB
not issubclass(Child, Sibling.register)0MiB
1MiB
0MiB
2MiB
not isinstance(child, Cousin.register)0MiB
2MiB
0MiB
3MiB
not issubclass(Child, Cousin.register)0MiB
2MiB
0MiB
3MiB
not isinstance(child, Uncle.register)0MiB
2MiB
0MiB
4MiB
not issubclass(Child, Uncle.register)0MiB
2MiB
0MiB
4MiB
isinstance(child, Parent.__subclasses__)0MiB
0MiB
0MiB
0MiB
issubclass(Child, Parent.__subclasses__)0MiB
0MiB
0MiB
0MiB
isinstance(child, Grandparent.__subclasses__)0MiB
0MiB
0MiB
0MiB
issubclass(Child, Grandparent.__subclasses__)0MiB
0MiB
0MiB
0MiB
not isinstance(child, Sibling.__subclasses__)0MiB
0MiB
0MiB
1MiB
not issubclass(Child, Sibling.__subclasses__)0MiB
0MiB
0MiB
1MiB
not isinstance(child, Cousin.__subclasses__)0MiB
0MiB
0MiB
1MiB
not issubclass(Child, Cousin.__subclasses__)0MiB
0MiB
0MiB
1MiB
not isinstance(child, Uncle.__subclasses__)0MiB
1MiB
0MiB
2MiB
not issubclass(Child, Uncle.__subclasses__)0MiB
1MiB
0MiB
2MiB
Just to check that nothing is broken:
not isinstance(child, Unrelated)0MiB
0MiB
0MiB
0MiB
not issubclass(Child, Unrelated)0MiB
0MiB
0MiB
0MiB
not isinstance(child, UnrelatedABC)0MiB
0MiB
0MiB
0MiB
not issubclass(Child, UnrelatedABC)0MiB
0MiB
0MiB
0MiB
Flamegraphs for
_py_abcimpl and testissubclass_uncle(the most time and memory consuming case onmain):main_vs_pr141171.tar.gz
Alternative: #150540 drops recursive subclass check completely, and does not mess around with class attributes.