Immortalize/defer reference count all functions in `__builtins__`

I propose that we immortalize/defer reference count all the initial functions in the builtins namespace on the default build. This benefits the free-threading build and JIT builds. New functions added to it via __builtins__['name'] will not be immortalized.

Benefit to Free-Threading

By immortalizing/deferring objects in the builtins namespace, we move closer to the free-threading build. IIRC, functions in the free-threading build are already deferred. So this would move us closer to what FT already does.

Benefits to the JIT

The other benefit is that we can specialize for the reference type in the JIT. See Eliminate redundant refcounting in the JIT · Issue #134584 · python/cpython · GitHub . This means 1. refcount eliimination the JIT 2. more effective register allocation/tos caching, as less spilling is required around all builtin calls.

Impact

I don’t think there should be any issues. The builtins namespace and its functions don’t consume that much memory. We also don’t immortalize new things added to it, so there will be no unbounded growth of memory.

CC @markshannon @eric.snow

5 Likes

Even open?

Sorry I don’t have enough context here: what’s special about open ?

It is an alias of io.open.

Well, most of the initial builtins content is already practically immortal. The copy of the original dict of builtins is made at the beginning, so the references to its classes and functions (which are all immutable) remain until the very late stage of the shutdown, after all modules are cleaned up. The only exceptions are open and _. So from user’s point of view, making them immortal should not have any impact.

1 Like

Can we also statically define the objects? The main point of immortalization was to be able to remove dynamic allocations entirely, and let the PyObject be fully defined at compile-time and stored in read-only pages. The reference count should be the only mutable field on these - hence immortalization to stop mutating it (and if it’s not, moving the mutable field to a table in the interpreter state that can be looked up when needed was the idea - e.g. for weakrefs and subclasses).

If we can get the entire module to be statically defined, with only its __dict__ being dynamically allocated, that’s good for a lot of cases (e.g. no need to recreate the module for each new subinterpreter).

I think it’s possible. However, Eric Snow is probably the authority on that, not me. I don’t know enough about the freezing stage to give a full answer. I suppose that might speed up startup though, because allocation is quite expensive from a runtime perspective.

To start small, I’ll probably just call immortalize everything in that module first after it’s initialized. If after one release it doesn’t break any code, we can move to statically defining them. This next step will probably be more disruptive and might break some strange monkey-patching code out there.

Even just immortalizing things will allow for a lot of reference count removal in both the default and FT build and possibly a lot of contention removal in the FT build.

Yeah, I’m proposing a bigger and more invasive change, but when Eric and I were working on immortalization it was definitely the intent! Simply disabling refcounting on individual objects was never good enough, but probably is the best we can do until people stop using ABI3 (as in, we need to keep letting refcounts be mutable for a while, in case someone is using an extension module that ignores immortalization).

How does this affect use cases where you want to remove builtins from the __builtins__ dict to e.g. disallow certain operations ?

I assume this would still be possible, since you’re just referring to the objects themselves, not their reference in the (main interpreter’s) __builtins__ dict.

Yup my assumption is that usually the monkey patching is doing __builtins__['len'] = my_len or something similar. So that won’t break.

1 Like

I missed that my proposed approach leaks memory on finalization. So looks like we have to go with your approach of making all the function objects statically allocated.

Declaring them immortal should mean that you can deallocate them at finalisation, since the promise is that they’ll outlive the runtime (not necessarily by long, just that the runtime doesn’t have to figure out when to deallocate). You just have to make sure that no Python code is running and they should be good to go.

Of course, static allocation is simpler in that respect, so don’t let me talk you out of it!

1 Like