Skip to content

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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
57 changes: 57 additions & 0 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,63 @@ 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:: next

.. function:: _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:: _jit.is_enabled()

Return ``True`` if JIT compilation is enabled for the current Python
process (implies :func:`sys._jit.is_available`), and ``False`` otherwise.
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:: _jit.is_active()

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

.. note::

This function is intended for testing and debugging the JIT itself.
Beyond using it to confirm that the JIT is actually working, most users
will never *need* to use it.

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

.. 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 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 +1284 to +1290
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.


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

Expand Down
4 changes: 4 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ struct _ts {
*/
PyObject *threading_local_sentinel;
_PyRemoteDebuggerSupport remote_debugger_support;

// Any frame between the topmost frame where the JIT is currently active,
// and the interpreter entry frame below it (used by sys._jit.is_active):
struct _PyInterpreterFrame *jit_entry;
};

/* other API */
Expand Down
40 changes: 4 additions & 36 deletions 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 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 Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def test_getitem_with_error(self):
CURRENT_THREAD_REGEX +
r' File .*, line 6 in <module>\n'
r'\n'
r'Extension modules: _testcapi, _testinternalcapi \(total: 2\)\n')
r'Extension modules: _testcapi \(total: 1\)\n')
else:
# Python built with NDEBUG macro defined:
# test _Py_CheckFunctionResult() instead.
Expand Down
2 changes: 1 addition & 1 deletion 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 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)
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 Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -999,12 +999,6 @@ get_co_localskinds(PyObject *self, PyObject *arg)
return kinds;
}

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

#ifdef _Py_TIER2

static PyObject *
Expand Down Expand Up @@ -2125,7 +2119,6 @@ static PyMethodDef module_functions[] = {
{"code_returns_only_none", code_returns_only_none, METH_O, NULL},
{"get_co_framesize", get_co_framesize, METH_O, NULL},
{"get_co_localskinds", get_co_localskinds, METH_O, 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
9 changes: 9 additions & 0 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,9 @@ dummy_func(
LOAD_IP(frame->return_offset);
res = temp;
LLTRACE_RESUME_FRAME();
#if TIER_TWO
tstate->jit_entry = frame;
#endif
}

tier1 op(_RETURN_VALUE_EVENT, (val -- val)) {
Expand Down Expand Up @@ -1377,6 +1380,9 @@ dummy_func(
LOAD_IP(1 + INLINE_CACHE_ENTRIES_SEND);
value = PyStackRef_MakeHeapSafe(temp);
LLTRACE_RESUME_FRAME();
#if TIER_TWO
tstate->jit_entry = frame;
#endif
}

tier1 op(_YIELD_VALUE_EVENT, (val -- val)) {
Expand Down Expand Up @@ -4962,6 +4968,9 @@ dummy_func(
RELOAD_STACK();
res = PyStackRef_FromPyObjectStealMortal((PyObject *)gen);
LLTRACE_RESUME_FRAME();
#if TIER_TWO
tstate->jit_entry = frame;
#endif
}

inst(BUILD_SLICE, (args[oparg] -- slice)) {
Expand Down
1 change: 1 addition & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
/* Tier 2 interpreter state */
_PyExecutorObject *current_executor = NULL;
const _PyUOpInstruction *next_uop = NULL;
_PyInterpreterFrame *jit_entry;
#endif
#if Py_TAIL_CALL_INTERP
# if Py_STATS
Expand Down
40 changes: 23 additions & 17 deletions Python/ceval_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,37 +355,43 @@ _PyFrame_SetStackPointer(frame, stack_pointer)
/* Tier-switching macros. */

#ifdef _Py_JIT
#define GOTO_TIER_TWO(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
_PyExecutorObject *_executor = (EXECUTOR); \
jit_func jitted = _executor->jit_code; \
/* Keep the shim frame alive via the executor: */ \
Py_INCREF(_executor); \
next_instr = jitted(frame, stack_pointer, tstate); \
Py_DECREF(_executor); \
Py_CLEAR(tstate->previous_executor); \
frame = tstate->current_frame; \
stack_pointer = _PyFrame_GetStackPointer(frame); \
if (next_instr == NULL) { \
next_instr = frame->instr_ptr; \
JUMP_TO_LABEL(error); \
} \
DISPATCH(); \
#define GOTO_TIER_TWO(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
_PyExecutorObject *_executor = (EXECUTOR); \
jit_func jitted = _executor->jit_code; \
/* Keep the shim frame alive via the executor: */ \
Py_INCREF(_executor); \
_PyInterpreterFrame *jit_entry = tstate->jit_entry; \
tstate->jit_entry = frame; \
next_instr = jitted(frame, stack_pointer, tstate); \
tstate->jit_entry = jit_entry; \
Py_DECREF(_executor); \
Py_CLEAR(tstate->previous_executor); \
frame = tstate->current_frame; \
stack_pointer = _PyFrame_GetStackPointer(frame); \
if (next_instr == NULL) { \
next_instr = frame->instr_ptr; \
JUMP_TO_LABEL(error); \
} \
DISPATCH(); \
} while (0)
#else
#define GOTO_TIER_TWO(EXECUTOR) \
do { \
OPT_STAT_INC(traces_executed); \
next_uop = (EXECUTOR)->trace; \
assert(next_uop->opcode == _START_EXECUTOR); \
jit_entry = tstate->jit_entry; \
tstate->jit_entry = frame; \
goto enter_tier_two; \
} while (0)
#endif

#define GOTO_TIER_ONE(TARGET) \
do \
{ \
tstate->jit_entry = jit_entry; \
next_instr = (TARGET); \
OPT_HIST(trace_uop_execution_counter, trace_run_length_hist); \
_PyFrame_SetStackPointer(frame, stack_pointer); \
Expand Down
Loading
Loading