Skip to content
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

Merged
merged 34 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b396af6
Add a PEP for immortal objects.
ericsnowcurrently Feb 11, 2022
19991dc
Move the mention of the python-dev thread.
ericsnowcurrently Feb 11, 2022
49a78f0
Add some open questions.
ericsnowcurrently Feb 11, 2022
4241d1b
Py_SETREF() -> Py_SET_REFCNT().
ericsnowcurrently Feb 11, 2022
56e7174
Update the documentation section.
ericsnowcurrently Feb 11, 2022
9589df7
Update the PEP header.
ericsnowcurrently Feb 11, 2022
0cbd50b
Fold "Proposal" into "Specification".
ericsnowcurrently Feb 11, 2022
413f465
Update the "Performance" section.
ericsnowcurrently Feb 11, 2022
4099282
Drop unused sections.
ericsnowcurrently Feb 11, 2022
02260bf
Fix typos.
ericsnowcurrently Feb 11, 2022
9126cc6
Add an open issue.
ericsnowcurrently Feb 11, 2022
850bd8d
Enumerate objects that will be immortalized.
ericsnowcurrently Feb 11, 2022
57b536c
Update the "Motivation" section.
ericsnowcurrently Feb 11, 2022
7437848
Clarify how we will probably identify immortal objects.
ericsnowcurrently Feb 11, 2022
016f895
Give examples of what are global objects.
ericsnowcurrently Feb 11, 2022
904868d
Assign a PEP number.
ericsnowcurrently Feb 11, 2022
a048b57
Update the PEP header.
ericsnowcurrently Feb 11, 2022
7d5401c
Drop the "Concerns" section.
ericsnowcurrently Feb 11, 2022
5340a17
Drop the "How to Teach This" section.
ericsnowcurrently Feb 11, 2022
63caa44
Drop an unnecessary aside.
ericsnowcurrently Feb 11, 2022
ab6943e
Add a rejected idea.
ericsnowcurrently Feb 11, 2022
743c753
Update CODEOWNERS.
ericsnowcurrently Feb 11, 2022
92fc5d4
Do not use an inline link for the implementation PR.
ericsnowcurrently Feb 11, 2022
4e1f5fc
Add a link to the docs explaining refcounts.
ericsnowcurrently Feb 11, 2022
c94ef99
Drop the PEP delegate.
ericsnowcurrently Feb 11, 2022
4667e82
Don't use footnotes for plain links.
ericsnowcurrently Feb 11, 2022
d6a88f3
Update Eddie's email address.
ericsnowcurrently Feb 14, 2022
6816e8f
Change the type.
ericsnowcurrently Feb 14, 2022
db4a703
Fix the References section.
ericsnowcurrently Feb 15, 2022
77bd7ee
Mention how Py_SET_REFCNT() won' change the refcount.
ericsnowcurrently Feb 15, 2022
f8cf975
Mention that GC state also impacts true immutability for containers.
ericsnowcurrently Feb 15, 2022
bb17abb
Emphasize the impact on app scalability.
ericsnowcurrently Feb 15, 2022
6eb4b36
Explain cleanup in the Specification section.
ericsnowcurrently Feb 15, 2022
aa5f14e
sys.gettotalrefcount() is not affected.
ericsnowcurrently Feb 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ pep-0679.rst @pablogsal
pep-0680.rst @encukou
pep-0681.rst @jellezijlstra
pep-0682.rst @mdickinson
pep-0683.rst @ericsnowcurrently
# ...
# pep-0754.txt
# ...
Expand Down
335 changes: 335 additions & 0 deletions pep-0683.rst
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
Copy link
Member

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).

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful have a citation/link for this.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Choose a reason for hiding this comment

The 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
Copy link

@eduardo-elizondo eduardo-elizondo Feb 15, 2022

Choose a reason for hiding this comment

The 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:

  • Immortalized interned strings (including Py_IDENTIFIERs)
  • Immortalizing heap after startup
  • Immortalizing module-level globals at import time

Also the following but it probably won't impact current benchmarks:

  • GC improvements for large applications
    Mental note to expand the benchmark suite to include something that overflows the last GC generation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Immortalized interned strings (including Py_IDENTIFIERs)

Note that I've replaced all core uses of _Py_IDENTIFIER() with direct access of statically allocated/initialized objects under _PyRuntimeState.global_objects.


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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link

@eduardo-elizondo eduardo-elizondo Feb 14, 2022

Choose a reason for hiding this comment

The 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:

  1. Runtime globals: Py_None, Py_True, Py_False, Small Ints, and types defined by: PyTypeObject Foo =. None of these instances are cleaned up today, thus, the behavior will remain the same even after they are made immortal.

  2. Immortalizing non-containers during runtime: Examples of these include interned strings. All of these objects have to be found during runtime shutdown, so they must be tracked somewhere. Fortunately, the biggest use case of this would be interned strings which are already tracked in a python dictionary. We should just refer to those at runtime shutdown to properly clean them up. But yes, the generic strategy is to track and deallocate at shutdown.

  3. Immortalizing containers during runtime: Example of these include immortalizing the heap after initialization. To correctly track these instances, we'll leverage the GC's permanent generation by pushing all immortalized containers there. During runtime shutdown, the strategy will be to first let the runtime try to do its best effort of deallocating these instances normally. Most of the module deallocation will now be handled by pylifecycle.c:finalize_modules which cleans up the remaining modules as best as we can. It will change which modules are available during __del__ but that's already defined as undefined behavior by the docs. Optionally, we could do some topological disorder to guarantee that user modules will be deallocated first before the stdlib modules. Finally, anything leftover (if any) can be found through the permanent generation gc list which we can clear after finalize_modules.

Copy link
Member

@brettcannon brettcannon Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. None of these instances are cleaned up today, thus, the behavior will remain the same even after they are made immortal.

Does it make sense to continue to do that, or should we change this?

Copy link
Member Author

Choose a reason for hiding this comment

The 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()``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth tweaking this to always return sys.maxsize() for immortal objects so people can test if an object is immortal?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, the value of sys.maxsize is slightly different from the immortal value but I think this refers to a general way to testing if an instance is immortal. We could in theory expose an API for this specifically (which should be different from maxsize). But for now I agree with Eric, let's keep this it as an implementation detail.

* (public) ``sys.gettotalrefcount()``
Copy link

@eduardo-elizondo eduardo-elizondo Feb 15, 2022

Choose a reason for hiding this comment

The 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. gettotalrefcount relies on _Py_RefTotal which is updated independently from the object's refcount field. Thus, an object's immortality will not make the value of gettotalrefcount be something really large.

Currently, I've placed the _Py_RefTotal update after the immortalization check. Thus, this value now reflects all the increfs/decrefs that did take place. If we move the check to before the immortalization check, it will now reflect all the attempts to incref and decrefs that took place (but they could have been a no-op). I chose the former since it's a bit unintuitive to have a returned value that's larger than the actual effective operations. This is enough to make the refcount leak tooling to work as is. However, if we choose the latter, it will work even in cases where we can upgrade a once mortal object to be immortal (which is beyond of the scope of any of the current changes in the PR). In this case, _Py_RefTotal will always be updated regardless of the the immortalization status of the instance, but the returned value of gettotalrefcount might make less sense as a whole.

The final consideration is that we need to make sure that _PySet_Dummy is not set to be immortal, otherwise, it will affect the implementation of _Py_GetRefTotal. Perhaps we should just remove the use of _PySet_Dummy and fix the gdb plugin in another way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've mentioned that sys.gettotalrefcount() is unaffected but didn't include any of your explanation. If you think any of it would be helpful to PEP readers then we can add it in a follow-up.


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: