-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
PEP 683: Add a PEP for immortal objects #2320
Changes from 28 commits
b396af6
19991dc
49a78f0
4241d1b
56e7174
9589df7
0cbd50b
413f465
4099282
02260bf
9126cc6
850bd8d
57b536c
7437848
016f895
904868d
a048b57
7d5401c
5340a17
63caa44
ab6943e
743c753
92fc5d4
4e1f5fc
c94ef99
4667e82
d6a88f3
6816e8f
db4a703
77bd7ee
f8cf975
bb17abb
6eb4b36
aa5f14e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
PEP: 683 | ||
Title: Immortal Objects, Using a Fixed Refcount | ||
Author: Eric Snow <ericsnowcurrently@gmail.com>, Eddie Elizondo <eduardo.elizondorueda@gmail.com> | ||
Discussions-To: python-dev@python.org | ||
Status: Draft | ||
Type: Standards Track | ||
Content-Type: text/x-rst | ||
Created: 10-Feb-2022 | ||
Python-Version: 3.11 | ||
Post-History: | ||
Resolution: | ||
|
||
|
||
Abstract | ||
======== | ||
|
||
Under this proposal, any object may be marked as immortal. | ||
"Immortal" means the object will never be cleaned up (at least until | ||
runtime finalization). Specifically, the `refcount`_ for an immortal | ||
object is set to a sentinel value, and that refcount is never | ||
changed by ``Py_INCREF()`` or ``Py_DECREF()``. | ||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Avoiding changes to the refcount is an essential part of this | ||
proposal. For what we call "immutable" objects, it makes them | ||
truly immutable. As described further below, this allows us | ||
to avoid performance penalties in scenarios that | ||
would otherwise be prohibitive. | ||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
This proposal is CPython-specific and, effectively, describes | ||
internal implementation details. | ||
|
||
.. _refcount: https://docs.python.org/3.11/c-api/intro.html#reference-counts | ||
|
||
|
||
Motivation | ||
========== | ||
|
||
Without immortal objects, all objects are effectively mutable. That | ||
includes "immutable" objects like ``None`` and ``str`` instances. | ||
This is because every object's refcount is frequently modified | ||
as it is used during execution. | ||
|
||
This has a concrete impact on active projects in the Python community. | ||
Below we describe several ways in which refcount modification has | ||
a real negative effect on those projects. None of that would | ||
happen for objects that are truly immutable. | ||
|
||
Reducing Cache Invalidation | ||
--------------------------- | ||
|
||
Every modification of a refcount causes the corresponding cache | ||
line to be invalidated. This has a number of effects. | ||
|
||
For one, the write must be propagated to other cache levels | ||
and to main memory. This has small effect on all Python programs. | ||
Immortal objects would provide a slight relief in that regard. | ||
|
||
On top of that, multi-core applications pay a price. If two threads | ||
are interacting with the same object (e.g. ``None``) then they will | ||
end up invalidating each other's caches with each incref and decref. | ||
This is true even for otherwise immutable objects like ``True``, | ||
``0``, and ``str`` instances. This is also true even with | ||
the GIL, though the impact is smaller. | ||
|
||
Avoiding Data Races | ||
------------------- | ||
|
||
Speaking of multi-core, we are considering making the GIL | ||
a per-interpreter lock, which would enable true multi-core parallelism. | ||
Among other things, the GIL currently protects against races between | ||
multiple threads that concurrently incref or decref. Without a shared | ||
GIL, two running interpreters could not safely share any objects, | ||
even otherwise immutable ones like ``None``. | ||
|
||
This means that, to have a per-interpreter GIL, each interpreter must | ||
have its own copy of *every* object, including the singletons and | ||
static types. We have a viable strategy for that but it will | ||
require a meaningful amount of extra effort and extra | ||
complexity. | ||
|
||
The alternative is to ensure that all shared objects are truly immutable. | ||
There would be no races because there would be no modification. This | ||
is something that the immortality proposed here would enable for | ||
otherwise immutable objects. With immortal objects, | ||
support for a per-interpreter GIL | ||
becomes much simpler. | ||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Avoiding Copy-on-Write | ||
---------------------- | ||
|
||
For some applications it makes sense to get the application into | ||
a desired initial state and then fork the process for each worker. | ||
This can result in a large performance improvement, especially | ||
memory usage. Several enterprise Python users (e.g. Instagram, | ||
YouTube) have taken advantage of this. However, the above | ||
Comment on lines
+99
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be useful have a citation/link for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really have any. However, it's fairly common knowledge on the core team. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't think we've made a public facing post about this, but I added this to Instagram and can confirm that we are still using this 🙂 |
||
refcount semantics drastically reduce the benefits and | ||
has led to some sub-optimal workarounds. | ||
|
||
Also note that "fork" isn't the only operating system mechanism | ||
that uses copy-on-write semantics. | ||
|
||
|
||
Rationale | ||
========= | ||
|
||
The proposed solution is obvious enough that two people came to the | ||
same conclusion (and implementation, more or less) independently. | ||
Other designs were also considered. Several possibilities | ||
have also been discussed on python-dev in past years. | ||
|
||
Alternatives include: | ||
|
||
* use a high bit to mark "immortal" but do not change ``Py_INCREF()`` | ||
* add an explicit flag to objects | ||
* implement via the type (``tp_dealloc()`` is a no-op) | ||
* track via the object's type object | ||
* track with a separate table | ||
|
||
Each of the above makes objects immortal, but none of them address | ||
the performance penalties from refcount modification described above. | ||
|
||
In the case of per-interpreter GIL, the only realistic alternative | ||
is to move all global objects into ``PyInterpreterState`` and add | ||
one or more lookup functions to access them. Then we'd have to | ||
add some hacks to the C-API to preserve compatibility for the | ||
may objects exposed there. The story is much, much simpler | ||
with immortal objects | ||
|
||
|
||
Impact | ||
====== | ||
|
||
Benefits | ||
-------- | ||
|
||
Most notably, the cases described in the two examples above stand | ||
to benefit greatly from immortal objects. Projects using pre-fork | ||
can drop their workarounds. For the per-interpreter GIL project, | ||
immortal objects greatly simplifies the solution for existing static | ||
types, as well as objects exposed by the public C-API. | ||
|
||
Performance | ||
----------- | ||
|
||
A naive implementation shows `a 4% slowdown`_. | ||
Several promising mitigation strategies will be pursued in the effort | ||
to bring it closer to performance-neutral. | ||
Comment on lines
+157
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not needed to include here but these are four different strategies that I'll be pursuing:
Also the following but it probably won't impact current benchmarks:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note that I've replaced all core uses of |
||
|
||
On the positive side, immortal objects save a significant amount of | ||
memory when used with a pre-fork model. Also, immortal objects provide | ||
opportunities for specialization in the eval loop that would improve | ||
performance. | ||
|
||
.. _a 4% slowdown: https://github.com/python/cpython/pull/19474#issuecomment-1032944709 | ||
|
||
Backward Compatibility | ||
----------------------- | ||
|
||
This proposal is completely compatible. It is internal-only so no API | ||
is changing. | ||
|
||
The approach is also compatible with extensions compiled to the stable | ||
ABI. Unfortunately, they will modify the refcount and invalidate all | ||
the performance benefits of immortal objects. However, the high bit | ||
of the refcount will still match ``_Py_IMMORTAL_REFCNT`` so we can | ||
still identify such objects as immortal. | ||
|
||
No user-facing behavior changes, with the following exceptions: | ||
|
||
* code that inspects the refcount (e.g. ``sys.getrefcount()`` | ||
or directly via ``ob_refcnt``) will see a really, really large | ||
value | ||
* ``Py_SET_REFCNT()`` will be a no-op for immortal objects | ||
|
||
Neither should cause a problem. | ||
|
||
Alternate Python Implementations | ||
-------------------------------- | ||
|
||
This proposal is CPython-specific. | ||
|
||
Security Implications | ||
--------------------- | ||
|
||
This feature has no known impact on security. | ||
|
||
Maintainability | ||
--------------- | ||
|
||
This is not a complex feature so it should not cause much mental | ||
overhead for maintainers. The basic implementation doesn't touch | ||
much code so it should have much impact on maintainability. There | ||
may be some extra complexity due to performance penalty mitigation. | ||
However, that should be limited to where we immortalize all | ||
objects post-init and that code will be in one place. | ||
|
||
Non-Obvious Consequences | ||
------------------------ | ||
|
||
* immortal containers effectively immortalize each contained item | ||
* the same is true for objects held internally by other objects | ||
(e.g. ``PyTypeObject.tp_subclasses``) | ||
* an immortal object's type is effectively immortal | ||
* though extremely unlikely (and technically hard), any object could | ||
be incref'ed enough to reach ``_Py_IMMORTAL_REFCNT`` and then | ||
be treated as immortal | ||
|
||
|
||
Specification | ||
============= | ||
|
||
The approach involves these fundamental changes: | ||
|
||
* add ``_Py_IMMORTAL_REFCNT`` (the magic value) to the internal C-API | ||
* update ``Py_INCREF()`` and ``Py_DECREF()`` to no-op for objects with | ||
the magic refcount (or its most significant bit) | ||
* do the same for any other API that modifies the refcount | ||
* ensure that all immortal objects are cleaned up during | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would this work? Some list of pointers that you can iterate through to free at runtime finalization? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll have to check with Eddie, but I expect the plan was to walk through the GC objects. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @brettcannon at a high level, there are three different kinds of cases that we need to care about when immortalizing objects to make sure that we correctly clean them up:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Does it make sense to continue to do that, or should we change this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Static objects should not be freed. |
||
runtime finalization | ||
|
||
Then setting any object's refcount to ``_Py_IMMORTAL_REFCNT`` | ||
makes it immortal. | ||
|
||
To be clear, we will likely use the most-significant bit of | ||
``_Py_IMMORTAL_REFCNT`` to tell if an object is immortal, rather | ||
than comparing with ``_Py_IMMORTAL_REFCNT`` directly. | ||
|
||
(There are other minor, internal changes which are not described here.) | ||
|
||
This is not meant to be a public feature but rather an internal one. | ||
So the proposal does *not* including adding any new public C-API, | ||
nor any Python API. However, this does not prevent us from | ||
adding (publicly accessible) private API to do things | ||
like immortalize an object or tell if one | ||
is immortal. | ||
|
||
Affected API | ||
------------ | ||
|
||
API that will now ignore immortal objects: | ||
|
||
* (public) ``Py_INCREF()`` | ||
* (public) ``Py_DECREF()`` | ||
* (public) ``Py_SET_REFCNT()`` | ||
* (private) ``_Py_NewReference()`` | ||
|
||
API that exposes refcounts (unchanged but may now return large values): | ||
|
||
* (public) ``Py_REFCNT()`` | ||
* (public) ``sys.getrefcount()`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth tweaking this to always return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now I'd rather folks avoid even thinking about immortal objects and keep this as internal as possible. We can deal with it later if it looks like it might actually be useful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case, the value of |
||
* (public) ``sys.gettotalrefcount()`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't change! Or rather, it will change but not in the expected way. This is something that I really looked into to make sure that the refleak tooling wouldn't break from this change. Currently, I've placed the The final consideration is that we need to make sure that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've mentioned that |
||
|
||
Immortal Global Objects | ||
----------------------- | ||
|
||
The following objects will be made immortal: | ||
|
||
* singletons (``None``, ``True``, ``False``, ``Ellipsis``, ``NotImplemented``) | ||
* all static types (e.g. ``PyLong_Type``, ``PyExc_Exception``) | ||
* all static objects in ``_PyRuntimeState.global_objects`` (e.g. identifiers, | ||
small ints) | ||
|
||
There will likely be others we have not enumerated here. | ||
|
||
Documentation | ||
------------- | ||
|
||
The feature itself is internal and will not be added to the documentation. | ||
|
||
We *may* add a note about immortal objects to the following, | ||
to help reduce any surprise users may have with the change: | ||
|
||
* ``Py_SET_REFCNT()`` (a no-op for immortal objects) | ||
* ``Py_REFCNT()`` (value may be surprisingly large) | ||
* ``sys.getrefcount()`` (value may be surprisingly large) | ||
|
||
Other API that might benefit from such notes are currently undocumented. | ||
|
||
We wouldn't add a note anywhere else (including for ``Py_INCREF()`` and | ||
``Py_DECREF()``) since the feature is otherwise transparent to users. | ||
|
||
|
||
Rejected Ideas | ||
============== | ||
|
||
Equate Immortal with Immutable | ||
------------------------------ | ||
|
||
Making a mutable object immortal isn't particularly helpful. | ||
The exception is if you can ensure the object isn't actually | ||
modified again. Since we aren't enforcing any immutability | ||
for immortal objects it didn't make sense to emphasis | ||
that relationship. | ||
|
||
|
||
Reference Implementation | ||
======================== | ||
|
||
The implementation is proposed on GitHub: | ||
|
||
https://github.com/python/cpython/pull/19474 | ||
|
||
|
||
Open Issues | ||
=========== | ||
|
||
* how do we ensure all immortal objects get cleaned up during runtime finalization? | ||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* how do we adjust ``sys.gettotalrefcount()`` to reflect things properly (for the sake of buildbots)? | ||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* is there any other impact on GC? | ||
|
||
|
||
References | ||
========== | ||
|
||
.. [python-dev] This was discussed in December 2021 on python-dev: | ||
https://mail.python.org/archives/list/python-dev@python.org/thread/7O3FUA52QGTVDC6MDAV5WXKNFEDRK5D6/#TBTHSOI2XRWRO6WQOLUW3X7S5DUXFAOV | ||
https://mail.python.org/archives/list/python-dev@python.org/thread/PNLBJBNIQDMG2YYGPBCTGOKOAVXRBJWY | ||
|
||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Copyright | ||
========= | ||
|
||
This document is placed in the public domain or under the | ||
CC0-1.0-Universal license, whichever is more permissive. | ||
|
||
|
||
|
||
.. | ||
Local Variables: | ||
mode: indented-text | ||
indent-tabs-mode: nil | ||
sentence-end-double-space: t | ||
fill-column: 70 | ||
coding: utf-8 | ||
End: |
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.
Please update the PEP with a link to the actual thread once you create it, so it is easy for others to find without a lot of guessing and Googling.
Also, there are a smattering of relatively non-critical editing issues (aside from the title containing a comma, which is a bit irregular), but as this was merged just as I got to it, I'm a bit late to the party now so if needed, I can make a separate PR fixing those (though they aren't terribly critical).