-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
gh-101859: Add caching of types.GenericAlias
objects
#103541
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
base: main
Are you sure you want to change the base?
Conversation
Yes, as you can see memory leak tests are failing:
|
I think it’s due to how we detect ref leak in out tests. I propose that a new API can be introduced to clear this GenericAlias cache. We only guarantee it’s the same object between 2 consecutive API calls. Normally we expect users will never call this API, and it’s exposed mainly for testing. |
Since no existing codes utilize this cache method, it wont break things. |
Sorry, I don't understand this idea.
Yes, undocumented protected methods are a good indication of this :) |
means a = list[int]
clear_cache()
b = list[int]
a is b # False (no more guarantee) |
Yes, agreed! 👍 I am willing to go further and never give this guarantee at all. |
Yeah we can expose a private function in |
Ok, global cached is now cleaned, but there's still a leak somewhere. Without cache cleaned:
With cache cleaned:
Now I have no idea what is going on; I think that I need advice here @Fidget-Spinner |
Sorry, I won't be able to take a close look at this until my exams are over. Could you ping me in exactly 2 weeks please? |
if (PyErr_Occurred()) { | ||
goto error; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
args
are not released in these paths. L1022, L1025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome catch! I've missed this completely. Manual memory management is driving me crazy :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote an extension to track down all ref count changes. Then I got here.
I am considering opening an issue to add this util into the main.
If anyone is interested, my debugger demo (for x64 Linux) is available at https://github.com/sunmy2019/cpython/tree/memory-tracking-demo Try compiling the Prints something like
|
I haven't looked closely into this, but Several tests with it failed. |
Objects/genericaliasobject.c
Outdated
_PyGenericAlias_Fini(PyInterpreterState *interp) { | ||
if (interp->genericalias_cache != NULL) { | ||
PyObject *cache = interp->genericalias_cache; | ||
PyObject_GC_Del(cache); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My debugger tells me that PyObject_GC_Del
will leave sys.gettotalrefcount()
unchanged. It is responsible for some leaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can clear the dict
in the early stages of fini?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused. Shouldn't the dict's refcount be 1 at this point. So a possible way is to call Py_DECREF
on the dict and let it handle its own deallocation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused. Shouldn't the dict's refcount be 1 at this point. So a possible way is to call
Py_DECREF
on the dict and let it handle its own deallocation?
It needs a fully functional interpreter state to do GC. But we are shutting the interpreter down at this moment, the interpreter state may be broken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs a fully functional interpreter state to do GC. But we are shutting the interpreter down at this moment, the interpreter's state may be broken.
That is my guess of what @sobolevn was thinking. Seems ok to use Py_DECREF(cache)
here since it is called before _PyDict_Fini
.
When running in sub-intepreter, in call to
Lines 2154 to 2168 in 6be7aee
The ref count is leaked here! |
The main thing we want to verify:
We already know it should save memory (if you want to test how much it saves, that's also welcome!) For a workload, you can run the |
I am already working on perf stats 😉 |
@samuelcolvin raised concerns about how this would interact with pydantic today at the typing summit, so marking as "do not merge" until the pydantic folks have had a chance to give their thoughts on the issue thread :) |
Do they have a timeline? |
As explained at the typing summit yesterday - it's really important to us that order of Unions is maintained. While in the static typing context, union order does not matter, in other contexts it really does:
In both contexts I think we absolutely fine with caching, but it would be extremely problematic if the current broken behaviour of Namely: from typing import List, Union
print(list[Union[int, float]])
#> list[typing.Union[int, float]]
print(list[Union[float, int]])
#> list[typing.Union[float, int]] correct!
print(List[Union[int, float]])
#> typing.List[typing.Union[int, float]]
print(List[Union[float, int]])
#> typing.List[typing.Union[int, float]] broken! If you need further explanation of why union order is important in the runtime use of type hints in particular - I can give it, but I hope this explanation is sufficient. I'm very happy with caching provided it doesn't mistakenly assuming |
@samuelcolvin since I am a grateful user of Here's the minimal demo: >>> import pydantic
>>> class My(pydantic.BaseModel):
... x: int | str
... y: str | int
...
>>> My(x='1', y='1')
My(x=1, y='1') (notice that So others can see why is that important to runtime type-checkers. >>> type(int | str).__mro__
(<class 'types.UnionType'>, <class 'object'>) C-implementation of I've added several test cases to be sure that |
Isn't it still possible we could end up with broken behavior? Note that For example, on Python 3.11 >>> d = {}
>>> d[list[int | str]] = 1
>>> d
{list[int | str]: 1}
>>> d[list[str | int]] = 2
>>> d
{list[int | str]: 2} Does the cache cause the same problems? I'm not sure but I'd imagine it would. |
The main problem is that unions with different order do not hash to different things. This is however in-line with the spec, as unions are supposed to be unordered, much like |
Yup - that's the kernel of this problem, and something I think we need to get changed. Given the comments of the SC yesterday about legitimate uses of type hints, I think they would be amenable to a change. Question is, what is required to get this changed? Does it need a PEP? |
My view as a (someone outdated) maintainer of typing is that that would need a PEP. The main reason is that it is modifying something that PEP 604 spells out very clearly. Note that PEP 604 says
And it gives some examples. Note that
https://docs.python.org/3/reference/datamodel.html#object.__hash__ Which means PEP 604 through specifying equality, is also indirectly requiring that union order must not affect its hash. Modifying portions of an accepted PEP usually require another PEP. Along with a deprecation period following Python's deprecation policy (currently minimum 2 cycles). I don't think this PR is the right place to discuss this. Could you open a thread on Discourse please? Thanks. |
I agree with @Fidget-Spinner that it probably requires a PEP to reverse behaviour clearly articulated in an accepted PEP :/ A https://discuss.python.org/c/ideas/6 thread or an issue at https://github.com/python/typing would probably be the best next step, to float the idea and try to get consensus. You'll probably reach more of a general audience with the former venue, but more folks from the typing community with the latter venue (I found it worked pretty well for getting to a consensus in python/typing#1363). |
I do wonder how much that was target at runtime behavior vs. what static type checkers should do. |
Ah yes I thought so too, but the example code from the PEP makes it extremely clear it is also targetting runtime. (lifted from PEP 604)
|
I did see the example code, but I read that as more like pseudocode or "communicating complex ideas via a simple code example" more so than "this should literally execute and give this result" |
Whether or not this partially reverses a PEP, I still think it would be best to open a thread at discuss.python.org or an issue at python/typing, so that this potential change to the behavior at runtime can be discussed in a more public forum |
Hmm, I see your point @adriangb. I'll go ahead and open the Discourse post so we can continue discussions there. |
Yes, indeed >>> list[int | str] is list[str | int]
True
>>> list[str | int].__args__[0].__args__
(<class 'int'>, <class 'str'>) |
I have a different Union runtime use case where currently equality is True but I would like to be able to distinguish the two and not have them cached the same. >>> from typing import Union
>>> A = Union[int, list[int]]
>>> Union[A, str] == Union[int, list[int], str] # True I do have dictionaries that map from a type to some serializer/deserializer. For certain types I write parsers for specific Union and want to be able to recognize that Union when it's present in another Union. I'd guess documentation use case may also keep track of union alias and be able to output back A (especially if it's lengthy union), but if it gets collapsed automatically would lose that. Would this type caching affect that? edit: I just noticed that |
@sobolevn sorry if this is in the PR, I can’t find it. Are there tests for interaction between the cache and subclasses of GenericAlias? |
Goals of this PR:
Py_GenericAlias
C-function cacheable. This is similar to how we usetyping.List[int] is typing.List[int]
*tuple[int] is *tuple[int]
, but clearlyis not tuple[int]
types.GenericAlias
creation. For example, right nowtyping._GenericAlias
has this behavior:typing._GenericAlias(typing.List, (int,)) is not typing._GenericAlias(typing.List, (int,))
. So, I kept the same fortypes.GenericAlias
:types.GenericAlias(list, (int,)) is not types.GenericAlias(list, (int,))
. Note: this basically means that all classes using__class_getitem__ = classmethod(GenericAlias)
are not cached3.
Open problems
References are leaked 🙂
For example, run:
./python.exe -m test -R 3:3 -v test_genericalias -m test_no_chaining
It will detect a reference leak. As far as I understand the only thing we leak is
genericalias_cache
state object. It is only cleared after the interpreter is shut down.I see several options here:
types.GenericAlias
to clear the cache and use it in global test teardown with other cleanup_types
modulePerformance and memory testing
I haven't done any yet. But, I think I slowed things down pretty badly :(
But, as far as I understand this is a price we want to pay for less memory usage.
Final thoughts
I am bit rusty (🦀, pun intended) with C code, so any feedback is welcome.