From d67c2a7cee04988b15b239b64777fa83e0f1d440 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 30 Apr 2025 19:05:10 -0700 Subject: [PATCH 01/10] Add sys._jit --- Doc/library/sys.rst | 53 ++++++++++++ Doc/using/cmdline.rst | 8 +- Include/internal/pycore_interpframe.h | 2 + Include/internal/pycore_interpframe_structs.h | 8 +- Lib/test/libregrtest/utils.py | 40 +-------- Lib/test/support/__init__.py | 10 +-- Lib/test/test_dis.py | 2 +- Lib/test/test_sys.py | 58 +++++++++++++ Modules/_testinternalcapi.c | 10 --- Python/ceval.c | 3 +- Python/ceval_macros.h | 8 ++ Python/clinic/sysmodule.c.h | 86 ++++++++++++++++++- Python/sysmodule.c | 81 +++++++++++++++++ 13 files changed, 308 insertions(+), 61 deletions(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index fbfd5e1e75b766..222b3a8e8f9991 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -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 + + .. 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. + 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): + + .. 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 "", line 5, in + assert sys._jit.is_active() + ~~~~~~~~~~~~~~~~~~^^ + AssertionError + .. data:: last_exc This variable is not always defined; it is set to the exception instance diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index fa7c9cddf9c6d6..044ff2f1c2e2ee 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -1279,7 +1279,13 @@ conflict. free-threaded builds and to ``0`` otherwise. See :option:`-X context_aware_warnings<-X>`. - .. 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 Debug-mode variables ~~~~~~~~~~~~~~~~~~~~ diff --git a/Include/internal/pycore_interpframe.h b/Include/internal/pycore_interpframe.h index d3fd218b27eed7..72c361710960b5 100644 --- a/Include/internal/pycore_interpframe.h +++ b/Include/internal/pycore_interpframe.h @@ -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 @@ -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 diff --git a/Include/internal/pycore_interpframe_structs.h b/Include/internal/pycore_interpframe_structs.h index 835b8e58194863..cd7ebc137ab594 100644 --- a/Include/internal/pycore_interpframe_structs.h +++ b/Include/internal/pycore_interpframe_structs.h @@ -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]; diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index c4a1506c9a7d60..63a2e427d185f1 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -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') diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 24984ad81fff99..ad0dbab273e7b7 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -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({ diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index f2586fcee57d87..ae68c1dd75c641 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -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) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 56413d00823f4a..1bfe23fd3512f4 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -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__": diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 812737e294fcb7..d8662ef15c809c 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -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 * @@ -2093,13 +2087,10 @@ reset_rare_event_counters(PyObject *self, PyObject *Py_UNUSED(type)) interp->rare_events.set_class = 0; interp->rare_events.set_bases = 0; - interp->rare_events.set_eval_frame_func = 0; interp->rare_events.builtin_dict = 0; interp->rare_events.func_modification = 0; return Py_None; -} - #ifdef Py_GIL_DISABLED static PyObject * @@ -2293,7 +2284,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}, diff --git a/Python/ceval.c b/Python/ceval.c index 4a75b60c9f0063..05f3ecf0d59ec1 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -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 diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h index e1d2673848cc69..8a014ac1d2fee0 100644 --- a/Python/ceval_macros.h +++ b/Python/ceval_macros.h @@ -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; \ @@ -379,6 +383,8 @@ 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 @@ -386,6 +392,8 @@ do { \ #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); \ diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index 8b73ccefc30ee5..77df5e39bbef05 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -1821,6 +1821,90 @@ sys__is_gil_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(_jit_is_available__doc__, +"is_available($module, /)\n" +"--\n" +"\n" +"Return True if the current Python executable supports JIT compilation, and False otherwise."); + +#define _JIT_IS_AVAILABLE_METHODDEF \ + {"is_available", (PyCFunction)_jit_is_available, METH_NOARGS, _jit_is_available__doc__}, + +static int +_jit_is_available_impl(PyObject *module); + +static PyObject * +_jit_is_available(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = _jit_is_available_impl(module); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + +PyDoc_STRVAR(_jit_is_enabled__doc__, +"is_enabled($module, /)\n" +"--\n" +"\n" +"Return True if JIT compilation is enabled for the current Python process (implies sys._jit.is_available()), and False otherwise."); + +#define _JIT_IS_ENABLED_METHODDEF \ + {"is_enabled", (PyCFunction)_jit_is_enabled, METH_NOARGS, _jit_is_enabled__doc__}, + +static int +_jit_is_enabled_impl(PyObject *module); + +static PyObject * +_jit_is_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = _jit_is_enabled_impl(module); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + +PyDoc_STRVAR(_jit_is_active__doc__, +"is_active($module, /)\n" +"--\n" +"\n" +"Return True if the topmost Python frame is currently executing JIT code, and False otherwise."); + +#define _JIT_IS_ACTIVE_METHODDEF \ + {"is_active", (PyCFunction)_jit_is_active, METH_NOARGS, _jit_is_active__doc__}, + +static int +_jit_is_active_impl(PyObject *module); + +static PyObject * +_jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = _jit_is_active_impl(module); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + #ifndef SYS_GETWINDOWSVERSION_METHODDEF #define SYS_GETWINDOWSVERSION_METHODDEF #endif /* !defined(SYS_GETWINDOWSVERSION_METHODDEF) */ @@ -1864,4 +1948,4 @@ sys__is_gil_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=1aca52cefbeb800f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b8cd807acf1686d5 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index e650444108e8f7..4288a20f300830 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3986,6 +3986,77 @@ _PySys_SetPreliminaryStderr(PyObject *sysdict) PyObject *_Py_CreateMonitoringObject(void); +/*[clinic input] +module _jit +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=10952f74d7bbd972]*/ + +PyDoc_STRVAR(_jit_doc, "Utilities for observing just-in-time compilation."); + +/*[clinic input] +_jit.is_available -> bool +Return True if the current Python executable supports JIT compilation, and False otherwise. +[clinic start generated code]*/ + +static int +_jit_is_available_impl(PyObject *module) +/*[clinic end generated code: output=6849a9cd2ff4aac9 input=03add84aa8347cf1]*/ +{ + (void)module; +#ifdef _Py_TIER2 + return true; +#else + return false; +#endif +} + +/*[clinic input] +_jit.is_enabled -> bool +Return True if JIT compilation is enabled for the current Python process (implies sys._jit.is_available()), and False otherwise. +[clinic start generated code]*/ + +static int +_jit_is_enabled_impl(PyObject *module) +/*[clinic end generated code: output=55865f8de993fe42 input=02439394da8e873f]*/ +{ + (void)module; + return _PyInterpreterState_GET()->jit; +} + +/*[clinic input] +_jit.is_active -> bool +Return True if the topmost Python frame is currently executing JIT code, and False otherwise. +[clinic start generated code]*/ + +static int +_jit_is_active_impl(PyObject *module) +/*[clinic end generated code: output=7facca06b10064d4 input=a7e31db659d40a0b]*/ +{ + (void)module; + _PyInterpreterFrame *frame = _PyThreadState_GET()->current_frame; + while (true) { + frame = frame->previous; + if (frame->owner == FRAME_OWNED_BY_INTERPRETER) { + return frame->jit_active; + } + } +} + +static PyMethodDef _jit_methods[] = { + _JIT_IS_AVAILABLE_METHODDEF + _JIT_IS_ENABLED_METHODDEF + _JIT_IS_ACTIVE_METHODDEF + {NULL} +}; + +static struct PyModuleDef _jit_module = { + PyModuleDef_HEAD_INIT, + .m_name = "sys._jit", + .m_doc = _jit_doc, + .m_size = -1, + .m_methods = _jit_methods, +}; + /* Create sys module without all attributes. _PySys_UpdateConfig() should be called later to add remaining attributes. */ PyStatus @@ -4047,6 +4118,16 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) goto error; } + PyObject *jit = _PyModule_CreateInitialized(&_jit_module, PYTHON_API_VERSION); + if (jit == NULL) { + goto error; + } + err = PyDict_SetItemString(sysdict, "_jit", jit); + Py_DECREF(jit); + if (err < 0) { + goto error; + } + assert(!_PyErr_Occurred(tstate)); *sysmod_p = sysmod; From ba5620223300e2ee87101911b015d7531a5927a9 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 30 Apr 2025 19:09:09 -0700 Subject: [PATCH 02/10] blurb add --- .../2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst new file mode 100644 index 00000000000000..7892ff25f2e363 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst @@ -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`. From 5a6f11f307810395208b3da888d3fc040bb518f7 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 30 Apr 2025 19:12:55 -0700 Subject: [PATCH 03/10] fixup --- Doc/using/cmdline.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 044ff2f1c2e2ee..92c93ceb925733 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -1279,6 +1279,8 @@ conflict. free-threaded builds and to ``0`` otherwise. See :option:`-X context_aware_warnings<-X>`. + .. versionadded:: 3.14 + .. envvar:: PYTHON_JIT On builds where experimental just-in-time compilation is available, this From 5b27d20b054deb546e6b5d6704ec94ca573681dc Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 30 Apr 2025 19:19:33 -0700 Subject: [PATCH 04/10] fixup --- Modules/_testinternalcapi.c | 3 +++ Python/sysmodule.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index d8662ef15c809c..397ce31ff28aa8 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2087,10 +2087,13 @@ reset_rare_event_counters(PyObject *self, PyObject *Py_UNUSED(type)) interp->rare_events.set_class = 0; interp->rare_events.set_bases = 0; + interp->rare_events.set_eval_frame_func = 0; interp->rare_events.builtin_dict = 0; interp->rare_events.func_modification = 0; return Py_None; +} + #ifdef Py_GIL_DISABLED static PyObject * diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 4288a20f300830..e79fb5b244895d 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -4124,7 +4124,7 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) } err = PyDict_SetItemString(sysdict, "_jit", jit); Py_DECREF(jit); - if (err < 0) { + if (err) { goto error; } From 080fd51eaa9dd3634bf87facdcaf8619a08200d2 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 30 Apr 2025 19:24:14 -0700 Subject: [PATCH 05/10] fixup --- Python/sysmodule.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index e79fb5b244895d..5fc1ddfc07858f 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -4118,12 +4118,12 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) goto error; } - PyObject *jit = _PyModule_CreateInitialized(&_jit_module, PYTHON_API_VERSION); - if (jit == NULL) { + PyObject *_jit = _PyModule_CreateInitialized(&_jit_module, PYTHON_API_VERSION); + if (_jit == NULL) { goto error; } - err = PyDict_SetItemString(sysdict, "_jit", jit); - Py_DECREF(jit); + err = PyDict_SetItemString(sysdict, "_jit", _jit); + Py_DECREF(_jit); if (err) { goto error; } From 1b2e5119f1a901c69c49c6f99797024cc609dadf Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 1 May 2025 08:40:50 -0700 Subject: [PATCH 06/10] Fix failing test --- Lib/test/test_capi/test_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 98dc3b42ef0bec..a597f23a992e7b 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -306,7 +306,7 @@ def test_getitem_with_error(self): CURRENT_THREAD_REGEX + r' File .*, line 6 in \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. From 775eb32ed26ce66f4c8e6b84cf71e173430264fb Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 1 May 2025 10:24:40 -0700 Subject: [PATCH 07/10] Feedback from code review --- Doc/library/sys.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 222b3a8e8f9991..f2ac8163661148 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1292,7 +1292,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only ``sys._jit`` is not guaranteed to exist or behave the same way in all Python implementations, versions, or build configurations. - .. versionadded:: 3.14 + .. versionadded:: next .. function:: sys._jit.is_available() @@ -1304,7 +1304,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. 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. + 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. @@ -1316,6 +1316,10 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. 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 From db7609fa924db4c30e18a1a64a604859b62b6d7a Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 1 May 2025 11:40:45 -0700 Subject: [PATCH 08/10] Fix references --- Doc/library/sys.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index f2ac8163661148..0e265ab6542d9e 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1294,14 +1294,14 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. versionadded:: next - .. function:: sys._jit.is_available() + .. 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:: sys._jit.is_enabled() + .. 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. @@ -1309,7 +1309,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only :envvar:`PYTHON_JIT` environment variable to ``0`` (disabled) or ``1`` (enabled) at interpreter startup. - .. function:: sys._jit.is_active() + .. function:: _jit.is_active() Return ``True`` if the topmost Python frame is currently executing JIT code, and ``False`` otherwise. From fc4824b3868614742fea7d351ed22132afedeca0 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 1 May 2025 13:27:31 -0700 Subject: [PATCH 09/10] Rework sys._jit.is_active to support the tail-calling interpreter --- Include/cpython/pystate.h | 4 ++ Include/internal/pycore_interpframe.h | 2 - Include/internal/pycore_interpframe_structs.h | 8 ++-- Python/bytecodes.c | 9 ++++ Python/ceval.c | 3 +- Python/ceval_macros.h | 48 +++++++++---------- Python/executor_cases.c.h | 9 ++++ Python/generated_cases.c.h | 15 ++++++ Python/pystate.c | 2 + Python/sysmodule.c | 14 ++++-- 10 files changed, 78 insertions(+), 36 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 97c097aa01c508..0d3e212f5945eb 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -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 */ diff --git a/Include/internal/pycore_interpframe.h b/Include/internal/pycore_interpframe.h index 72c361710960b5..d3fd218b27eed7 100644 --- a/Include/internal/pycore_interpframe.h +++ b/Include/internal/pycore_interpframe.h @@ -146,7 +146,6 @@ _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 @@ -328,7 +327,6 @@ _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 diff --git a/Include/internal/pycore_interpframe_structs.h b/Include/internal/pycore_interpframe_structs.h index cd7ebc137ab594..835b8e58194863 100644 --- a/Include/internal/pycore_interpframe_structs.h +++ b/Include/internal/pycore_interpframe_structs.h @@ -43,11 +43,11 @@ struct _PyInterpreterFrame { #endif uint16_t return_offset; /* Only relevant during a function call */ char owner; - uint8_t visited:1; - // Set on interpreter entry frames when the JIT is active: - uint8_t jit_active:1; #ifdef Py_DEBUG - uint8_t lltrace:6; + uint8_t visited:1; + uint8_t lltrace:7; +#else + uint8_t visited; #endif /* Locals and stack */ _PyStackRef localsplus[1]; diff --git a/Python/bytecodes.c b/Python/bytecodes.c index b6fde3f1029421..820edb39dba9ee 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -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)) { @@ -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)) { @@ -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)) { diff --git a/Python/ceval.c b/Python/ceval.c index 3b0bc1bfb65b1c..c777e7944f6c4c 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1035,10 +1035,9 @@ _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.jit_active = false; + entry_frame.return_offset = 0; #ifdef Py_DEBUG entry_frame.lltrace = 0; #endif diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h index 8a014ac1d2fee0..6480d958123e59 100644 --- a/Python/ceval_macros.h +++ b/Python/ceval_macros.h @@ -355,27 +355,26 @@ _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); \ - 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; \ - 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) \ @@ -383,8 +382,8 @@ 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; \ + jit_entry = tstate->jit_entry; \ + tstate->jit_entry = frame; \ goto enter_tier_two; \ } while (0) #endif @@ -392,8 +391,7 @@ do { \ #define GOTO_TIER_ONE(TARGET) \ do \ { \ - assert(entry_frame.jit_active); \ - entry_frame.jit_active = false; \ + tstate->jit_entry = jit_entry; \ next_instr = (TARGET); \ OPT_HIST(trace_uop_execution_counter, trace_run_length_hist); \ _PyFrame_SetStackPointer(frame, stack_pointer); \ diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 040be54f9b87ff..8ce7745d90e95e 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -1820,6 +1820,9 @@ LOAD_IP(frame->return_offset); res = temp; LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif stack_pointer[0] = res; stack_pointer += 1; assert(WITHIN_STACK_BOUNDS()); @@ -1985,6 +1988,9 @@ LOAD_IP(1 + INLINE_CACHE_ENTRIES_SEND); value = PyStackRef_MakeHeapSafe(temp); LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif stack_pointer[0] = value; stack_pointer += 1; assert(WITHIN_STACK_BOUNDS()); @@ -6646,6 +6652,9 @@ stack_pointer = _PyFrame_GetStackPointer(frame); res = PyStackRef_FromPyObjectStealMortal((PyObject *)gen); LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif stack_pointer[0] = res; stack_pointer += 1; assert(WITHIN_STACK_BOUNDS()); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 004f4db593dc10..f76fe58a9b4c67 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7540,6 +7540,9 @@ LOAD_IP(frame->return_offset); res = temp; LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif } stack_pointer[0] = res; stack_pointer += 1; @@ -7608,6 +7611,9 @@ LOAD_IP(1 + INLINE_CACHE_ENTRIES_SEND); value = PyStackRef_MakeHeapSafe(temp); LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif } stack_pointer[0] = value; stack_pointer += 1; @@ -10525,6 +10531,9 @@ stack_pointer = _PyFrame_GetStackPointer(frame); res = PyStackRef_FromPyObjectStealMortal((PyObject *)gen); LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif stack_pointer[0] = res; stack_pointer += 1; assert(WITHIN_STACK_BOUNDS()); @@ -10556,6 +10565,9 @@ LOAD_IP(frame->return_offset); res = temp; LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif stack_pointer[0] = res; stack_pointer += 1; assert(WITHIN_STACK_BOUNDS()); @@ -12270,6 +12282,9 @@ LOAD_IP(1 + INLINE_CACHE_ENTRIES_SEND); value = PyStackRef_MakeHeapSafe(temp); LLTRACE_RESUME_FRAME(); + #if TIER_TWO + tstate->jit_entry = frame; + #endif stack_pointer[0] = value; stack_pointer += 1; assert(WITHIN_STACK_BOUNDS()); diff --git a/Python/pystate.c b/Python/pystate.c index 5685957b160dba..b1b1b38cbb1120 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1582,6 +1582,8 @@ init_threadstate(_PyThreadStateImpl *_tstate, tstate->delete_later = NULL; + tstate->jit_entry = NULL; + llist_init(&_tstate->mem_free_queue); llist_init(&_tstate->asyncio_tasks_head); if (interp->stoptheworld.requested || _PyRuntime.stoptheworld.requested) { diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 5fc1ddfc07858f..b7b1eb4715e54b 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -4033,13 +4033,21 @@ _jit_is_active_impl(PyObject *module) /*[clinic end generated code: output=7facca06b10064d4 input=a7e31db659d40a0b]*/ { (void)module; - _PyInterpreterFrame *frame = _PyThreadState_GET()->current_frame; + PyThreadState *tstate = _PyThreadState_GET(); + _PyInterpreterFrame *frame = tstate->current_frame; + _PyInterpreterFrame *jit_entry_frame = tstate->jit_entry; while (true) { - frame = frame->previous; + // If we hit the JIT's "entry" frame first, we're in JIT code: + if (frame == jit_entry_frame) { + return true; + } + // If we hit the interpreter's "entry" frame first, we're not: if (frame->owner == FRAME_OWNED_BY_INTERPRETER) { - return frame->jit_active; + return false; } + frame = frame->previous; } + Py_UNREACHABLE(); } static PyMethodDef _jit_methods[] = { From b80bdf8e445e3f45af87d324f7c08c0e6b126c2c Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 1 May 2025 13:32:52 -0700 Subject: [PATCH 10/10] Fix JIT interpreter --- Python/ceval.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/ceval.c b/Python/ceval.c index c777e7944f6c4c..a7209b98f1c6a9 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -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