-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
Consider integration of PyObject_GC_UnTrack() with the trashcan C API #89044
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
Comments
The fix for bpo-33930 includes a somewhat mysterious comment:
I wonder if we can just integrate the untrack into the body of the trashcan code. Then, the explicit call to untrack in the dealloc function body can be removed. That removes the risk of incorrectly using the macro version. I suspect the reason this was not done originally is because the original trashcan mechanism did not use the GC header pointers to store objects. Now, any object that uses the trashcan *must* be a GC object. |
We probably will need a new set of macros because these macros are public IIRC correctly. Although as PyObject_GC_UnTrack has a check, we probably can just absorb it and let backwards compat work by being idempotent if called twice |
Extensions that call PyObject_GC_UnTrack before calling Py_TRASHCAN_BEGIN will still work, they will just take a very minor performance hit. I don't think it is worth the trouble to introduce new macros for that reason. Extensions that really care about performance can wrap the call in a Python version ifdef. There is an issue if someone writes and tests their extension with the new API, i.e. without having the explicit PyObject_GC_UnTrack() call in their dealloc method. If they compile with an older Python, they likely get a crash. If they compile with asserts enable, they would get an assert fail in _PyTrash_thread_deposit_object, i.e.:
I guess that's an argument for new macros. |
For examples of bugs caused by forgetting the untrack calls, see bpo-31095. |
Since C99 is now allowed, perhaps we should suggest that declarations go after Py_TRASHCAN_BEGIN, e.g. mytype_dealloc(mytype *p)
{
Py_TRASHCAN_BEGIN(p, mytype_dealloc)
... declarations go here ...
... The body of the deallocator goes here, including all calls ...
... to Py_DECREF on contained objects. ...
Py_TRASHCAN_END // there should be no code after this
} The only dealloc method that doesn't fit this pattern is subtype_dealloc() and that's because the object might not be a GC object. Given the pattern, it occurs to me that that _Py_Dealloc() could do the trashcan begin/end work. However, the authors of the dealloc methods would have to realize the special rules of the trashcan (e.g. no returns from the dealloc method body). Also, there would be some overhead added to _Py_Dealloc() to check if the trashcan is supported. E.g. checking a type flag. Another idea would be to add a new slot for the method, e.g. tp_dealloc_trash. Then, _Py_Dealloc() could check if it exists and if so do the trashcan begin/end dance around it. That would still add some overhead to _Py_Dealloc() so I think the current macros are the best approach. |
Please don't add new macros. The TRASHCAN C API is already complex enough. |
Moreover, before making any change, I would suggest by starting with documenting the existing trashcan C API. Right now, there is no documentation in Doc/c-api/. Documentation can be copied from Include/cpython/object.h. Last year, I modified the trashcan API to hide implementation details. There were also issues with the stable ABI... thing which is unclear to me, since the trashcan C API is excluded from the limited C API. Well, I excluded it. Previously, it was part of it, but it did not work with the limited C API. commit 0fa4f43
commit 38965ec
commit db532a0
|
It would be nice to add an assertion in _PyTrash_begin(). |
I was thinking about this more today and I think the better fix is to actually build the trashcan mechanism into _Py_Dealloc(). Requiring that types opt-in to the trashcan mechanism by using the trashcan macros is not ideal. First, the trashcan macros are a bit tricky to use correctly. Second, every "container" type is potentially a part of a long ref chain and could blow up the stack on deallocation (i.e. triggered from DECREF). So, for correctness/robustness, every type that supports cyclic GC should get trashcan-style deallocation. We would have to find a way to do this without incurring a (significant) performance hit. Also, it would have to be done without breaking C extensions. Ideally they would get trashcan-style deallocation without any source code changes. I'm not sure if that can be done but I'm hopeful it's possible. |
The problem of PyObject_GC_UnTrack() is just the most visible effect of the trashcan mecanism: tp_dealloc can be called twice, and this is *not* expected by the tp_dealloc API. Putting trashcan mecanism outside tp_dealloc can allow to make sure that tp_dealloc is called exactly once. _Py_Dealloc() sounds like a good candidate, but I didn't check if it's the only way to call tp_dealloc. Are there other places where tp_dealloc is called *directly*? Using military grade regex, I found the following functions calling tp_dealloc: grep 'dealloc[) ]([a-zA-Z]\+)' */.c
So if we move the trashcan usage inside functions, 3 functions must be modified:
|
The fact that Py_TRASHCAN_BEGIN and Py_TRASHCAN_END can cause tp_dealloc to be The point of this PR is to make the macros harder to use incorrectly. If there tp_dealloc can get called more than once for another reason. If the object is
tp_dealloc is called from a few more places, as you found. As for providing I suppose it would be possible to blow up the C stack via a call chain like:
I think you would have to make a long chain of subclasses. Seems unlikely in
Take a look at bpo-44897. It implements the _Py_Dealloc approach. You can see I think the big change is calling tp_dealloc methods with the object already mytype_dealloc(PyObject *self)
{
...
assert(PyObject_GC_IsTracked(self));
...
} That would break after PR 27738, at least as it's currently written. The other issue I'm not quite sure about is that |
Thanks Neil for the thorough explanation! I think in any case we should benchmark this because this will affect *all* GC types if I understand correctly and the TS mechanism had shown slowdowns before |
We definitely need to benchmark. I would guess that adding trashcan protection The performance hit will likely come as a result of adding a branch that Because of PEP-442, _PyType_IS_GC() checks not only a type flag but might also Small correction for my explaination above: if tp_dealloc gets called mutiple |
Another small correction, _PyType_IS_GC() checks only the type flag (Py_TPFLAGS_HAVE_GC). _PyObject_IS_GC() checks both the type flag and calls tp_is_gc(), if it exists. tp_is_gc() is wart, IMHO, and it would be nice if we can kill it off so only the type flag is needed. That's a topic for a different issue though. |
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: