Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

GH-133231: Add JIT utilities in sys._jit #133233

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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
53 changes: 53 additions & 0 deletions 53 Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,59 @@ always available. Unless explicitly noted otherwise, all variables are read-only

.. versionadded:: 3.5

.. data:: _jit

Utilities for observing just-in-time compilation.

.. impl-detail::

JIT compilation is an *experimental implementation detail* of CPython.
``sys._jit`` is not guaranteed to exist or behave the same way in all
Python implementations, versions, or build configurations.

.. versionadded:: 3.14
brandtbucher marked this conversation as resolved.
Show resolved Hide resolved

.. function:: sys._jit.is_available()

Return ``True`` if the current Python executable supports JIT compilation,
and ``False`` otherwise. This can be controlled by building CPython with
the ``--experimental-jit`` option on Windows, and the
:option:`--enable-experimental-jit` option on all other platforms.

.. function:: sys._jit.is_enabled()

Return ``True`` if JIT compilation is enabled for the current Python
process (implies ``sys._jit.is_available()``), and ``False`` otherwise.
brandtbucher marked this conversation as resolved.
Show resolved Hide resolved
If JIT compilation is available, this can be controlled by setting the
:envvar:`PYTHON_JIT` environment variable to ``0`` (disabled) or ``1``
(enabled) at interpreter startup.

.. function:: sys._jit.is_active()

Return ``True`` if the topmost Python frame is currently executing JIT
code, and ``False`` otherwise.

.. note::

Due to the nature of tracing JIT compilers, repeated calls to this
function may give surprising results. For example, branching on its
return value will likely lead to unexpected behavior (if doing so
causes JIT code to be entered or exited):
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: pycon

>>> for warmup in range(BIG_NUMBER):
... # This line is "hot", and is eventually JIT-compiled:
... if sys._jit.is_active():
... # This line is "cold", and is run in the interpreter:
... assert sys._jit.is_active()
...
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
assert sys._jit.is_active()
~~~~~~~~~~~~~~~~~~^^
AssertionError

.. data:: last_exc

This variable is not always defined; it is set to the exception instance
Expand Down
8 changes: 8 additions & 0 deletions 8 Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,14 @@ conflict.

.. versionadded:: 3.14

.. envvar:: PYTHON_JIT

On builds where experimental just-in-time compilation is available, this
variable can force the JIT to be disabled (``0``) or enabled (``1``) at
interpreter startup.

.. versionadded:: 3.13
Comment on lines +1282 to +1288
Copy link
Member

Choose a reason for hiding this comment

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

We should backport this, should it be split out into a different PR for ease?

Copy link
Member Author

Choose a reason for hiding this comment

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

We can just backport it separately, since I need the ref for this PR's changes.

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 using miss-islington for the backport here?

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 just do it manually in a bit.


Debug-mode variables
~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions 2 Include/internal/pycore_interpframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ _PyFrame_Initialize(
frame->return_offset = 0;
frame->owner = FRAME_OWNED_BY_THREAD;
frame->visited = 0;
frame->jit_active = false;
#ifdef Py_DEBUG
frame->lltrace = 0;
#endif
Expand Down Expand Up @@ -327,6 +328,7 @@ _PyFrame_PushTrampolineUnchecked(PyThreadState *tstate, PyCodeObject *code, int
#endif
frame->owner = FRAME_OWNED_BY_THREAD;
frame->visited = 0;
frame->jit_active = false;
#ifdef Py_DEBUG
frame->lltrace = 0;
#endif
Expand Down
8 changes: 4 additions & 4 deletions 8 Include/internal/pycore_interpframe_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ struct _PyInterpreterFrame {
#endif
uint16_t return_offset; /* Only relevant during a function call */
char owner;
#ifdef Py_DEBUG
uint8_t visited:1;
uint8_t lltrace:7;
#else
uint8_t visited;
// Set on interpreter entry frames when the JIT is active:
uint8_t jit_active:1;
#ifdef Py_DEBUG
uint8_t lltrace:6;
#endif
/* Locals and stack */
_PyStackRef localsplus[1];
Expand Down
40 changes: 4 additions & 36 deletions 40 Lib/test/libregrtest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,43 +335,11 @@ def get_build_info():
build.append('with_assert')

# --enable-experimental-jit
tier2 = re.search('-D_Py_TIER2=([0-9]+)', cflags)
if tier2:
tier2 = int(tier2.group(1))

if not sys.flags.ignore_environment:
PYTHON_JIT = os.environ.get('PYTHON_JIT', None)
if PYTHON_JIT:
PYTHON_JIT = (PYTHON_JIT != '0')
else:
PYTHON_JIT = None

if tier2 == 1: # =yes
if PYTHON_JIT == False:
jit = 'JIT=off'
else:
jit = 'JIT'
elif tier2 == 3: # =yes-off
if PYTHON_JIT:
jit = 'JIT'
if sys._jit.is_available():
if sys._jit.is_enabled():
build.append("JIT")
else:
jit = 'JIT=off'
elif tier2 == 4: # =interpreter
if PYTHON_JIT == False:
jit = 'JIT-interpreter=off'
else:
jit = 'JIT-interpreter'
elif tier2 == 6: # =interpreter-off (Secret option!)
if PYTHON_JIT:
jit = 'JIT-interpreter'
else:
jit = 'JIT-interpreter=off'
elif '-D_Py_JIT' in cflags:
jit = 'JIT'
else:
jit = None
if jit:
build.append(jit)
build.append("JIT (disabled)")

# --enable-framework=name
framework = sysconfig.get_config_var('PYTHONFRAMEWORK')
Expand Down
10 changes: 3 additions & 7 deletions 10 Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2648,13 +2648,9 @@ def exceeds_recursion_limit():

Py_TRACE_REFS = hasattr(sys, 'getobjects')

try:
from _testinternalcapi import jit_enabled
except ImportError:
requires_jit_enabled = requires_jit_disabled = unittest.skip("requires _testinternalcapi")
else:
requires_jit_enabled = unittest.skipUnless(jit_enabled(), "requires JIT enabled")
requires_jit_disabled = unittest.skipIf(jit_enabled(), "requires JIT disabled")
_JIT_ENABLED = sys._jit.is_enabled()
requires_jit_enabled = unittest.skipUnless(_JIT_ENABLED, "requires JIT enabled")
requires_jit_disabled = unittest.skipIf(_JIT_ENABLED, "requires JIT disabled")


_BASE_COPY_SRC_DIR_IGNORED_NAMES = frozenset({
Expand Down
2 changes: 1 addition & 1 deletion 2 Lib/test/test_dis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,7 @@ def test_loop_quicken(self):
# Loop can trigger a quicken where the loop is located
self.code_quicken(loop_test)
got = self.get_disassembly(loop_test, adaptive=True)
jit = import_helper.import_module("_testinternalcapi").jit_enabled()
jit = sys._jit.is_enabled()
expected = dis_loop_test_quickened_code.format("JIT" if jit else "NO_JIT")
self.do_disassembly_compare(got, expected)

Expand Down
58 changes: 58 additions & 0 deletions 58 Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,64 @@ def test_remote_exec_in_process_without_debug_fails_xoption(self):
self.assertIn(b"Remote debugging is not enabled", err)
self.assertEqual(out, b"")

class TestSysJIT(unittest.TestCase):

def test_jit_is_available(self):
available = sys._jit.is_available()
script = f"import sys; assert sys._jit.is_available() is {available}"
assert_python_ok("-c", script, PYTHON_JIT="0")
assert_python_ok("-c", script, PYTHON_JIT="1")

def test_jit_is_enabled(self):
available = sys._jit.is_available()
script = "import sys; assert sys._jit.is_enabled() is {enabled}"
assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0")
assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1")

def test_jit_is_active(self):
available = sys._jit.is_available()
script = textwrap.dedent(
"""
import _testinternalcapi
import operator
import sys

def frame_0_interpreter() -> None:
assert sys._jit.is_active() is False

def frame_1_interpreter() -> None:
assert sys._jit.is_active() is False
frame_0_interpreter()
assert sys._jit.is_active() is False

def frame_2_jit(expected: bool) -> None:
# Inlined into the last loop of frame_3_jit:
assert sys._jit.is_active() is expected
# Insert C frame:
operator.call(frame_1_interpreter)
brandtbucher marked this conversation as resolved.
Show resolved Hide resolved
assert sys._jit.is_active() is expected

def frame_3_jit() -> None:
# JITs just before the last loop:
for i in range(_testinternalcapi.TIER2_THRESHOLD + 1):
# Careful, doing this in the reverse order breaks tracing:
expected = {enabled} and i == _testinternalcapi.TIER2_THRESHOLD
assert sys._jit.is_active() is expected
frame_2_jit(expected)
assert sys._jit.is_active() is expected

def frame_4_interpreter() -> None:
assert sys._jit.is_active() is False
frame_3_jit()
assert sys._jit.is_active() is False

assert sys._jit.is_active() is False
frame_4_interpreter()
assert sys._jit.is_active() is False
"""
)
assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0")
assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1")


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add new utilities of observing JIT compilation:
:func:`sys._jit.is_available`, :func:`sys._jit.is_enabled`, and
:func:`sys._jit.is_active`.
7 changes: 0 additions & 7 deletions 7 Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1165,12 +1165,6 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
return NULL;
}

static PyObject *
jit_enabled(PyObject *self, PyObject *arg)
{
return PyBool_FromLong(_PyInterpreterState_GET()->jit);
}

#ifdef _Py_TIER2

static PyObject *
Expand Down Expand Up @@ -2293,7 +2287,6 @@ static PyMethodDef module_functions[] = {
{"get_co_localskinds", get_co_localskinds, METH_O, NULL},
{"get_code_var_counts", _PyCFunction_CAST(get_code_var_counts),
METH_VARARGS | METH_KEYWORDS, NULL},
{"jit_enabled", jit_enabled, METH_NOARGS, NULL},
#ifdef _Py_TIER2
{"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL},
{"invalidate_executors", invalidate_executors, METH_O, NULL},
Expand Down
3 changes: 2 additions & 1 deletion 3 Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1036,9 +1036,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
entry_frame.f_executable = PyStackRef_None;
entry_frame.instr_ptr = (_Py_CODEUNIT *)_Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS + 1;
entry_frame.stackpointer = entry_frame.localsplus;
entry_frame.return_offset = 0;
entry_frame.owner = FRAME_OWNED_BY_INTERPRETER;
entry_frame.visited = 0;
entry_frame.return_offset = 0;
entry_frame.jit_active = false;
#ifdef Py_DEBUG
entry_frame.lltrace = 0;
#endif
Expand Down
8 changes: 8 additions & 0 deletions 8 Python/ceval_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,11 @@ do { \
jit_func jitted = _executor->jit_code; \
/* Keep the shim frame alive via the executor: */ \
Py_INCREF(_executor); \
assert(!entry_frame.jit_active); \
entry_frame.jit_active = true; \
next_instr = jitted(frame, stack_pointer, tstate); \
assert(entry_frame.jit_active); \
entry_frame.jit_active = false; \
Py_DECREF(_executor); \
Py_CLEAR(tstate->previous_executor); \
frame = tstate->current_frame; \
Expand All @@ -379,13 +383,17 @@ do { \
OPT_STAT_INC(traces_executed); \
next_uop = (EXECUTOR)->trace; \
assert(next_uop->opcode == _START_EXECUTOR); \
assert(!entry_frame.jit_active); \
entry_frame.jit_active = true; \
goto enter_tier_two; \
} while (0)
#endif

#define GOTO_TIER_ONE(TARGET) \
do \
{ \
assert(entry_frame.jit_active); \
entry_frame.jit_active = false; \
next_instr = (TARGET); \
OPT_HIST(trace_uop_execution_counter, trace_run_length_hist); \
_PyFrame_SetStackPointer(frame, stack_pointer); \
Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.