Abstract
I’d like to propose that we have a one time break in the stable ABI,
such that extensions can be compiled into one of two builds:
- Support versions up to 3.15
- Support version 3.12 (3.14 for 32 bit builds) and later.
With this break, it will not be possible to compile a single binary that
supports both 3.11 and 3.16, or both 3.13 and 3.16 for 32 bit builds.
Having this break will allow significant improvements in maintainability
and performance of CPython.
Motivation
Maintainability
Having the freedom to adjust the PyObject header means that
we can homogenize the code across 64 bit (little and big endian)
and 32 bit platforms, reduce the differences between the default
and free-threading structs, and simplify GC and allocator code by
storing necessary information in the object header.
Specifically this gets us:
- Reduced code size
- Better testing
- More flexibility in the implementation
- Reduced diff between default and free-threading implementations
Performance
Simple refactoring should produce a 1-2% speedup, by speeding up checks for GC attributes, object layout, etc.
On top of that, the main performance benefit will be in allowing better data structures and algorithms for allocation, reference counting and garbage collection.
I don’t know much this will gain us, but there is lots of room for improvement in the those areas of the CPython code base. My guess would be 5-10%.
Some of these improvements can be done without the ABI breakage, but not as well or as cleanly.
Why is a break necessary? Why does the stable ABI need to change?
Traditionally, Py_TYPE(), Py_DECREF() and PY_INCREF() were implemented as macros, meaning that the ABI depended on the layout of PyObject, which could never be changed.
Since 3.12, Py_DECREF and PY_INCREF have been implemented as functions in the stable ABI. With builds of C extensions that support 3.12 and later only, the implementation can change the layout of the PyObject header.
Requiring that we support 3.12 does limit the changes that can be made, specifically we cannot change the size of the object header.
That is fine for 64 bit builds as there is spare space in the object header, but 32 bit builds may need the object header to grow.
Consequently the break has to be at 3.14 for 32 bit builds.
Proof of concept
As a test of the utility of this proposal, I refactored just the GC code to use bits in the ob_flags field of the header, instead of tagging pointers.
The resulting code is easier to understand and less error prone. Quantitatively, it reduces the code size by 100 lines and produces a consistent 1% speedup across the 5 platforms (all 64 bit) tested on.