-
-
Notifications
You must be signed in to change notification settings - Fork 30.4k
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
bpo-43503: Make limited API objects effectively immutable. #24828
bpo-43503: Make limited API objects effectively immutable. #24828
Conversation
[Removed my previous comment as I have misread the name of the macros] |
I was a bit confused as well. The PR doesn't work for the copy-on-write behavior until |
Correct. And if it is not defined then you get immortal objects with refcounts that change like normal, just at a very large value.
I'm unclear on what performance impact you mean. If
That should be fine. The initial refcount for immortal objects in that case should make it resilient to any races on incr/decr on the refcount. |
e80c8cb
to
5a00f82
Compare
Objects/object.c
Outdated
@@ -1817,7 +1835,9 @@ _Py_NewReference(PyObject *op) | |||
#ifdef Py_REF_DEBUG | |||
_Py_RefTotal++; | |||
#endif | |||
Py_SET_REFCNT(op, 1); | |||
/* Do not use Py_SET_REFCNT to skip the Immortal Instance check. This |
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 think this is dangerous. An immortal object may have any number of pending decrefs on it. Setting its refcnt to 1 would result in crashes.
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.
Yeah, I'd meant to address this. Thanks for reminding me.
This, by itself, won't allow sharing objects between parallel sub-interpreters. Also, I don't like the addition of compile-time flags, as they don't get tested, and will just start rotting. |
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 dislike the _Py_IMMORTAL_OBJECTS macro idea.
the API is opt-in for the public API (but never the limited API), if _Py_IMMORTAL_OBJECTS is defined
It sounds like something different than Python. It would mean that by default, Python would crash if an extension is built with _Py_IMMORTAL_OBJECTS and use immortal object. You should use a Python built with _Py_IMMORTAL_OBJECTS and rebuild all your extension with _Py_IMMORTAL_OBJECTS to fully support immortable objects.
CPU performance isn't impacted (no changes to _Py_INCREF() or Py_DECREF())
You PR modify _Py_INCREF() and _Py_DECREF() ... Please run pyperformance benchmark suite to measure the _Py_IMMORTAL_OBJECTS overhead. I expect something like 1.1x slower.
When you're done making the requested changes, leave the comment: |
I dislike the idea of hacking Py_INCREF/Py_DECREF for this specific use case. I would prefer to have shared data, and have one "proxy" object per interpreter. The proxy would protect against data race and has its own reference count. |
I'd like to take a step back. Let's forget (for now) about copy-on-write. Then we can leave INCREF/DECREF unchanged. IIUC, the goal is to be able to share objects like Since at the Python level such type objects are immutable, and assuming that C extensions won't break this, it would be sufficient to set the As long as the GIL is shared, the refcount of immortal objects should always be higher than the initial immortal refcount value. If the GIL is not shared, we could see drift in the refcount due to races, but the drift (esp. on a 64-bit machine) would never be so much that it would push the refcount all the way to zero or past the immortal bit. The only concern I have at this point is that class objects, including exceptions and builtin classes, maintain a list of weak references to all subclasses. This list is exposed via If we don't do something like this, how are we going to preserve backwards compatibility the existing C API where FWIW, I agree that for communication between interpreters we should use an entirely different mechanism. The current PR is only under consideration as a solution for the problem of how to share immutable objects between interpreters that are part of the public C API. Eric's solution is a good one IMO. |
Yeah, that's been a source of confusion. I was already thinking about pulling that part out to keep things more focused here.
All of that is correct.
Exactly! I'm proud of that idea. It's one of the few novel things I can claim in the PR. 🙂 We'll see if it holds up.
Yeah. We would have to make
Agreed.
Correct.
Right. AFAICS, the objects that are part of the public C-API are the biggest obstacle to eliminating all the unshareable C globals. That's why it's the thing I've been focused on lately. There are other approaches to tackle this, but none of them are as straightforward as the refcount solution.
Same here.
Correct.
Thanks! FWIW, I borrowed the main idea from a variety of old python-dev threads and past in-person discussions at sprints, as well as one or two details from Eddie's PR. |
091cca1
to
88dd6fa
Compare
For the sake of better clarity, I've dropped the "const refcount" parts (what Eddie wants), as well as the "opt-in API" part. I also plan on taking a look at making static types properly immutable in this PR. |
Good point. So even if immortality is part of the overall solution, a dedicated public API for immortal objects probably wouldn't be that helpful. A "public API for shareable" objects would be the right level. (The question of meeting Eddie's needs, relative to a public API, is a different matter.) Regardless, the problem is more specific than shareability. We will probably end up with a per-interpreter copy of every builtin type and singleton. The real problem at hand is how to deal with all the I expect that the answer will mostly be "stop creating static objects", and maybe provide some helpers to bridge the gap. However, that does not help with any PyObject already exposed by the public C-API. Thankfully that is a small set of cases (each of 5 singletons plus a portion of the static types), so we should be able to find a way to deal with it. That is what motivated this PR. Ideally we would stop exposing those objects in the public API. If we did so, there are things we can do to make that less painful for extension authors and maybe even transparent. With that in mind, here are some alternatives to having the actual objects in the public API (pretty much none require changes in extensions):
My preference would be the first one. However, the tricky part is how we deal with the limited API. As far as I understand it, it means the first and second options aren't viable. I suppose we could do the first one for the public API and one of the others for the limited API, but that seems like a headache. (Perhaps it's the cost of having a stable ABI?) So, this PR is in pursuit of that last option. I think we'll end up with at least the API from the first option, regardless of what we do, but that doesn't help fix the problem of the existing objects for us, relative to the limited API. Hence the introduction of immortal objects here. (Sorry, I should have been more clear about all this in the PR text above.) |
Include/object.h
Outdated
if (_py_is_immortal((PyObject *)ob)) { | ||
return; | ||
} |
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.
Would it make sense to emit a warning for this case?
@vstinner, do my (and Guido's) other comments address your concerns? |
The plan is to get rid of static types to replace them with heap types: see https://bugs.python.org/issue40077 The Exception class will become a heap type and so be mutable, as array.array for example:
At the C level, the str type is mutable (the cached hash is set when it's computed for the first time). It's hard to ensure that a Python cannot be modified by a subinterpreter :-/ |
This type member (subclasses) causes a very concrete issue which lead to crash in add_subclass() which is called by PyType_Ready(). I had the issue when I experimented running pip in parallel (with on GIL per interpreter): https://bugs.python.org/issue40512#msg383830 It's just yet another example why we should not share Python objects between interpreters. IMO we must never share any Python object between two interpreters, mutable or immutable. There are too many corner cases leading to inconsistent objects or crashes if a single object is shared. Sharing data is fine. Just put one Python proxy object on top of it per interpreter. |
There is a similar problem with static types which should become heap types but are currently directly exposed as static type in the public C API: https://bugs.python.org/issue40601 I propose to replace I can add a pattern to my https://github.com/pythoncapi/pythoncapi_compat project to automate this change without losing support for old Python versions. I did something similar for other changes. For example, I plan to propose a PEP to replace static types with heap types and the incompatible C API change. Yeah, it's painful. I prefer to "break some eggs" (C API compat) to be able to move the "good" design with best performances. |
Eric:
That change would break things, that's why a PEP is needed ;-) |
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.
An "immortal" object is one for which Py_DECREF() will never try to deallocate it.
This change goes against https://bugs.python.org/issue1635741 which tries to delete all Python objects at exit.
I understand that right now, some Python objects are defined statically and don't need to be released at exit. There are many changes being done to get rid of statically allocated objects (like static types) to use dynamically allocated objects.
Include/object.h
Outdated
static inline void _Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) { | ||
// We don't have clean access to pycore_object.h at this point. | ||
PyAPI_FUNC(int) _PyObject_IsImmortal(PyObject *); |
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's weird to define a function inside a static inline function. Why not defining it outside the function?
If you don't want to expose _PyObject_IsImmortal() in the public C API, a better option would be to convert Py_SET_REFCNT() static inline function to a regular function. So you can do whatever you want in the implementation. Calling Py_SET_REFCNT() is uncommon and so doesn't need to be optimized, no?
Hey, it's good to see that my backward incompatible C API change on Py_REFCNT() being useful :-) In Python 3.9, it was allowed to write Py_REFCNT(obj) = refcnt;
. I fixed many C extensions to use Py_REFCNT(): see bpo-39573 if you are curious.
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.
If making _Py_SET_REFCNT()
a regular function isn't a problem then I'll definitely take the cleaner approach.
Objects/typeobject.c
Outdated
@@ -146,7 +146,9 @@ _PyType_CheckConsistency(PyTypeObject *type) | |||
return 1; | |||
} | |||
|
|||
CHECK(Py_REFCNT(type) >= 1); | |||
if (!_PyObject_IsImmortal((PyObject *)type)) { | |||
CHECK(Py_REFCNT(type) >= 1); |
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 don't understand this change. Does it mean that refcount of immortal objects are negative and so Py_DECREF() on this type will call its deallocator?
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.
Hmm, I thought I had removed this. It is a holdover from when I used a negative refcount to indicate immortal and no longer applies. I'll fix it.
Doc/c-api/refcounting.rst
Outdated
.. versionadded:: 3.10 | ||
|
||
Also see :c:macro:`_PyObject_HEAD_IMMORTAL_INIT` and | ||
:c:macro:`_PyVarObject_HEAD_IMMORTAL_INIT`. |
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 don't think that we document API which are parts of the internal C API. If you want to document it, mention that it's only part of the internal C API.
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.
We do document some internal API, for the sake of core development. However, the docs I added don't add much value in that regard. I was thinking more of them as eventually public, which I pulled out of this PR, so I'll remove the docs.
* At the moment this API is strictly internal. However, if it proves | ||
* helpful for extension authors we may move it to the public API. */ | ||
|
||
#define _Py_IMMORTAL_OBJECTS 1 |
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 don't get the purpose of this macro since it's always defined.
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's there only as a marker relative to the public API. However, it is not necessary at this point and I'll drop it.
Doc/c-api/structures.rst
Outdated
_PyObject_IMMORTAL_INIT_REFCNT, type, | ||
|
||
For now you must opt in to use this by defining | ||
``_Py_IMMORTAL_OBJECTS``. |
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's always defined, no?
#define INIT_TYPE(TYPE, NAME) \ | ||
do { \ | ||
if (PyType_Ready(TYPE) < 0) { \ | ||
return _PyStatus_ERR("Can't initialize " NAME " type"); \ | ||
} \ | ||
_PyObject_SetImmortal((PyObject *)TYPE); \ |
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.
Don't do that: using static types from multiple interpreters in parallel is unsafe. See for example the PyTypeObject.tp_subclasses issue.
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.
Yuck, but yes. This detail seems to be a particular problem for the lofty idea of sub-interpreters one day eventually having their own GIL...
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.
Right. This is something that has to be addressed before this PR can proceed.
Guido:
When I tested to run pip in parallel, what I see is that calling PyType_Ready() in parallel from different subinterpreters (running in different threads) is causing crashes since it calls add_subclass() without any kind of locking, whereas PyTypeObject.tp_subclasses dict is not thread safe. Declaring builtin types such as object, int or str as immortal is likely to lead to a similar crash, so the overall approach sounds like a bad idea. The PR description explicitly lists static types:
Eric wrote:
But the PR still makes builtin types as immortals. subinterpreters issues are really complex and surprising. See for example this bug in the xml.parsers package caused by a change in the implementation of interned strings: https://bugs.python.org/issue40521#msg383829 The problem was that a C extension (pyexpat) wasn't ported to the new multiphase initialization API. Or said differently: a single shared Python object shared by multiple interpreters caused very surprising bad leading to a crash. I know that it's appealing to hack the refcount because it looks simple and safe, but be aware that sharing any Python object between interpreters cause many complex and surprising bugs. Also, I'm not comfortable with INCREF/REFCNT which modify the ref count in parallel without any synchronization. The PR makes the assumption that the _PyObject_IMMORTAL_BIT bit will not be cleared by DECREF. On 32-bit, _PyObject_IMMORTAL_BIT is 268_435_456. If you mark the None singleton as immortal, I'm not sure that the risk fo clearing the immortal bit is so unlikely. |
Victor, let's wait until Eric has submitted a new version of the PR. I agree that the issue with |
Doc/c-api/refcounting.rst
Outdated
Immortal Objects | ||
================ | ||
|
||
"Immortal" objects are those that are expected to never be deallocated |
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.
never be deallocated? even on main interpreter Py_Finalize()? It seems like we should kill the immortals at that point.
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.
As I noted to Victor, it shouldn't be a problem for us to address immortal objects directly in Py_Finalize()
. The question of dealing with static objects during runtime finalization is a separately question; one worth addressing.
#define INIT_TYPE(TYPE, NAME) \ | ||
do { \ | ||
if (PyType_Ready(TYPE) < 0) { \ | ||
return _PyStatus_ERR("Can't initialize " NAME " type"); \ | ||
} \ | ||
_PyObject_SetImmortal((PyObject *)TYPE); \ |
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.
Yuck, but yes. This detail seems to be a particular problem for the lofty idea of sub-interpreters one day eventually having their own GIL...
Cleaning up immortal objects at runtime finalization shouldn't be a problem. If it's a question of dealing with static objects then that is not a problem this PR introduces. 🙂 |
We need to be very careful about sharing objects between interpreters. In other words, there is no point in making objects immortal until we have a robust way to ensure that they are immutable. |
547240b
to
f103804
Compare
I have concerns similar to Mark's with this approach. I can't picture the end state and how it might work. It would help to have a sketch of what the final design is going to be. This PR is just doing the easy part, IMHO. One the key problems with removing the GIL (or implementing something like sub-interpreters) is that the reference counting it not thread safe. It (apparently) can only be made thread safe with large overhead. E.g. atomic operations for incref/decref or using something like buffered reference counting. The PR is an attempt to skirt that issue by disabling automatic memory management for the objects that are going to be shared between interpreters. However, as Mark points out, you have to do that to all the state that gets shared. So, not just the immediate objects that are shared but all the objects reachable from those objects. Also, we have automatic memory management for a reason and just turning it off creates new problems. Finalizing objects during shutdown is one of the problems but likely there will be others. IMHO, we need to devise a thread safe automatic memory management scheme that can be used for these shared objects and that can integrate with reference counting. We cannot remove reference counting because the C API depends on it. It seems to me that the key is to make the reference info something that is thread-local (e.g. ref counts or roots that a M&S collector follows) . Or, if not thread local, at least something you don't need locking to update. Could we use per-interpreter proxy objects that wrap the real objects? That way, updating it's reference count is thread safe since it's only changed by one thread. The wrapping would have to apply to objects reachable from the proxy. I remember something like this was done for security purposes years ago (bastion, rexec). It is hard to get right though. If used for security proposes, you get security holes if you don't properly wrap things. For multi-thread usage, you could get mysterious crashes or mis-behavior that would be almost impossible to debug since it could be a result of race conditions. |
For the moment let's set aside any objective of sharing objects between interpreters. It's not actually what I'm looking for right now. The more pressing issue for me is that we have a bunch of static This PR is one possible approach and arguably the simplest. (Plus, the fixed list of objects makes the solution easier to maintain.) See my explanation in #24828 (comment). (FWIW, I'd rather make each of those objects per-interpreter and expect we probably will regardless.) |
FYI, I've created https://bugs.python.org/issue43503 to address that underlying problem, of which this PR is only one solution. |
For singletons (None, True, False, ...), I proposed to make them per-interpreter, I have a draft PR: bpo-39511. For static types, I'm working on a PEP to remove them from the C API and to convert them to heap types. |
Hmm, what about saying that the limited API is only available for the main interpreter? Then pretty much all the problems go away. (This feels like something someone already suggested at some point and I forgot about it.) |
It has been proposed to mark extensions modules as being compatible with subinterpreters. For example, require to use the multiphase initialization API (PEP 489) and only define types as heap types. Trying to import an extension which is not marked this way would fail with an ImportError. The idea is to prevent random crashes if an extension access a Python object shared by multiple interpreters. The extension maintainer declares to be aware of subinterpreters issues and announces to not access shared Python objects. About the specific case of static types, I understand that you propose to continue using |
Exactly. And the multi-phase init API isn't part of the limited API, thus extensions using it wouldn't be able to use multi-phase init, so no subinterpreters.
It's worth trying it out since it reduces the impact of the change. Macros would help as a transition aid (though it might be tricky since all the Regardless, if we can take the limited-api-does-not-support-subinterpreters approach then a lot of options open up for us. I'm going to start a |
I'm dropping this in favor of avoiding the limited API. |
This is a slight alternative to @eduardo-elizondo's GH-19474, though our main motivations are different. Eddie is looking to optionally reduce the impact of refcounts on the copy-on-write semantics of forking.
My interest lies in supporting (immutable) objects that can be safely shared between interpreters without a shared GIL. In the short term this will help solve the problem of what to do about
PyObject
s that are part of the public C-API (and especially the limited API): the singletons and many static types (including the exception types).The problem of those public objects is something I was thinking through last December. This approach using refcounts is one of several I tried. A couple weeks later I found Eddie's PR and adopted a few of his changes (e.g. the high positive bit, whereas I was previously using a negative value).
Notable differences from GH-19474:
_Py_INCREF()
orPy_DECREF()
)_Py_IMMORTAL_OBJECTS
is definedAlso note that if
Py_IMMORTAL_CONST_REFCOUNTS
is defined then the refcount of immortal objects essentially becomes immutable, as in GH-19474. That should meet Eddie's needs without being invasive.UPDATE: I've dropped the "const refcounts" and "opt-in API" parts.
See #24828 (comment) for a more in-depth explanation of the motivation (and rationale) for this PR.
https://bugs.python.org/issue43503