Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 Include/internal/pycore_optimizer_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ typedef union {

typedef struct _Py_UOpsAbstractFrame {
bool globals_watched;
bool builtins_checked;
// The version number of the globals dicts, once checked. 0 if unchecked.
uint32_t globals_checked_version;
// Max stacklen
Expand Down
9 changes: 7 additions & 2 deletions Include/internal/pycore_uop_ids.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions Include/internal/pycore_uop_metadata.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import builtins
import contextlib
import dis
import itertools
Expand Down Expand Up @@ -5054,6 +5055,86 @@ def jitted(funcs):
with self.assertRaises(NameError):
jitted([f, f_with_bad_globals])

def test_jitted_code_sees_changed_copied_builtins(self):
# Trace-time check. The traced function's builtins is a copy of the
# canonical dict with the same keys version, so a version check
# cannot tell them apart. The optimizer must see that func_builtins
# is not interp->builtins and keep _LOAD_GLOBAL_BUILTINS, which reads
# the frame's own dict, rather than fold a constant from the
# canonical one. No runtime guard is involved.

def f(n):
return [len("hello") for _ in range(n)]

copied_builtins = vars(builtins).copy()
f = types.FunctionType(f.__code__, {"__builtins__": copied_builtins})

f(TIER2_THRESHOLD)
ex = get_first_executor(f)
self.assertIsNotNone(ex)
# Not folded: the load must still consult the frame's builtins.
self.assertIn("_LOAD_GLOBAL_BUILTINS", get_opnames(ex))

# Replacing an existing value does not change the keys version.
copied_builtins["len"] = lambda s: 42
self.assertEqual(f(8), [42] * 8)

def test_jitted_code_sees_different_builtins(self):
# Runtime check. The traced function's builtins IS the canonical
# dict, so folding len to a constant is correct at trace time.
# A second function sharing the code object then enters the same
# executor with other builtins, so only the runtime guard on the
# executing frame's builtins can catch it.
def f(n):
return [len("hello") for _ in range(n)]

namespace = {"__builtins__": builtins}
f_canonical = types.FunctionType(f.__code__, namespace)
copied_builtins = vars(builtins).copy()
namespace["__builtins__"] = copied_builtins
f_copied = types.FunctionType(f.__code__, namespace)


f_canonical(TIER2_THRESHOLD)
ex = get_first_executor(f_canonical)
self.assertIsNotNone(ex)
self.assertIn("_GUARD_BUILTINS_IS_CANONICAL", get_opnames(ex))

copied_builtins["len"] = lambda s: 42
# The executor's owner still sees the canonical len.
self.assertEqual(f_canonical(8), [5] * 8)
# A different function enters the same executor with other builtins.
self.assertEqual(f_copied(8), [42] * 8)

def test_builtins_guard_emitted_once_per_frame(self):
# A frame's builtins cannot change once the frame is pushed, so
# repeated builtin loads in one frame share a single guard, just as
# they already share a single _GUARD_GLOBALS_VERSION.

def warmup(n):
x = 0
for _ in range(n):
x += len("ab")
return x

def one_frame(n):
x = 0
for _ in range(n):
x += len("ab") + abs(-1) + ord("c")
return x

# The optimizer context is reused for every compilation, so compile an
# unrelated trace first: state that is not reset per frame leaks here.
warmup(TIER2_THRESHOLD)
self.assertIsNotNone(get_first_executor(warmup))

_, ex = self._run_with_optimizer(one_frame, TIER2_THRESHOLD)
self.assertIsNotNone(ex)
uop_names = get_opnames(ex)
self.assertNotIn("_LOAD_GLOBAL_BUILTINS", uop_names) # all folded
self.assertEqual(uop_names.count("_GUARD_BUILTINS_IS_CANONICAL"), 1)
self.assertEqual(uop_names.count("_GUARD_GLOBALS_VERSION"), 1)

def test_reference_tracking_across_call_doesnt_crash(self):

def f1():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fix the JIT optimizer folding a builtin name to a constant taken from the
interpreter's builtins dictionary even when the running function uses a
different ``__builtins__`` mapping, such as one created with
``vars(builtins).copy()``. The optimizer now only folds when the function's
builtins is the interpreter's, and the folded constant is guarded at runtime so
that another function sharing the same code object but a different builtins
mapping does not use it.
4 changes: 4 additions & 0 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,10 @@ dummy_func(
STAT_INC(LOAD_GLOBAL, hit);
}

tier2 op(_GUARD_BUILTINS_IS_CANONICAL, (--)) {
DEOPT_IF(BUILTINS() != tstate->interp->builtins);
}

macro(LOAD_GLOBAL_MODULE) =
unused/1 + // Skip over the counter
NOP + // For guard insertion in the JIT optimizer
Expand Down
70 changes: 70 additions & 0 deletions Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion Python/optimizer_bytecodes.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <stdint.h>

#include "Python.h"
#include "pycore_long.h"
#include "pycore_opcode_utils.h"
Expand Down Expand Up @@ -2519,13 +2521,24 @@ dummy_func(void) {
else if (interp->rare_events.builtin_dict >= _Py_MAX_ALLOWED_BUILTINS_MODIFICATIONS) {
/* Do nothing */
}
else if (ctx->frame->func == NULL ||
ctx->frame->func->func_builtins != builtins) {
}
else {
if (!ctx->builtins_watched) {
PyDict_Watch(BUILTINS_WATCHER_ID, builtins);
ctx->builtins_watched = true;
}
if (ctx->frame->globals_checked_version != 0 && ctx->frame->globals_watched) {
if (ctx->frame->globals_checked_version != 0 &&
ctx->frame->globals_watched &&
uop_buffer_remaining_space(&ctx->out_buffer) >= 2)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We always allow enough headroom for small changes like this. No need to check here.

{
cnst = convert_global_to_const(this_instr, builtins);
if (cnst != NULL && !ctx->frame->builtins_checked) {
ctx->frame->builtins_checked = true;
ADD_OP(_GUARD_BUILTINS_IS_CANONICAL, 0, 0);
ADD_OP(this_instr->opcode, 0, (uintptr_t)cnst);

@markshannon markshannon Sep 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are you adding the same instruction twice?
My mistake. convert_global_to_const converts the input inplace and relies on its being copied. We should probably update its interface, but that's for a different PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The two ADD_OP calls emit different instructions:

// Rewrite this_instr as a constant load; nothing is emitted yet.
cnst = convert_global_to_const(this_instr, builtins);

if (cnst != NULL) {
    // Emit the builtins identity guard.
    ADD_OP(_GUARD_BUILTINS_IS_CANONICAL, 0, 0);

    // Emit the constant load prepared above.
    ADD_OP(this_instr->opcode, 0, this_instr->operand0);
}

}
}
}
if (cnst == NULL) {
Expand Down
17 changes: 16 additions & 1 deletion Python/optimizer_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Python/optimizer_symbols.c
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,7 @@ _Py_uop_frame_new(
frame->stack_pointer = frame->stack;
frame->globals_checked_version = 0;
frame->globals_watched = false;
frame->builtins_checked = false;
frame->func = NULL;
frame->caller = false;
frame->is_c_recursion_checked = false;
Expand Down
Loading