Skip to content

Commit

Permalink
pythongh-118335: Configure Tier 2 interpreter at build time (python#1…
Browse files Browse the repository at this point in the history
…18339)

The code for Tier 2 is now only compiled when configured
with `--enable-experimental-jit[=yes|interpreter]`.

We drop support for `PYTHON_UOPS` and -`Xuops`,
but you can disable the interpreter or JIT
at runtime by setting `PYTHON_JIT=0`.
You can also build it without enabling it by default
using `--enable-experimental-jit=yes-off`;
enable with `PYTHON_JIT=1`.

On Windows, the `build.bat` script supports
`--experimental-jit`, `--experimental-jit-off`,
`--experimental-interpreter`.

In the C code, `_Py_JIT` is defined as before
when the JIT is enabled; the new variable
`_Py_TIER2` is defined when the JIT *or* the
interpreter is enabled. It is actually a bitmask:
1: JIT; 2: default-off; 4: interpreter.
  • Loading branch information
gvanrossum authored and SonicField committed May 8, 2024
1 parent 0cb0d07 commit 3345321
Show file tree
Hide file tree
Showing 32 changed files with 181 additions and 42 deletions.
30 changes: 23 additions & 7 deletions Doc/whatsnew/3.13.rst
Expand Up @@ -888,7 +888,7 @@ Experimental JIT Compiler
=========================

When CPython is configured using the ``--enable-experimental-jit`` option,
a just-in-time compiler is added which can speed up some Python programs.
a just-in-time compiler is added which may speed up some Python programs.

The internal architecture is roughly as follows.

Expand All @@ -905,19 +905,35 @@ The internal architecture is roughly as follows.
before it is interpreted or translated to machine code.

* There is a Tier 2 interpreter, but it is mostly intended for debugging
the earlier stages of the optimization pipeline. If the JIT is not
enabled, the Tier 2 interpreter can be invoked by passing Python the
``-X uops`` option or by setting the ``PYTHON_UOPS`` environment
variable to ``1``.
the earlier stages of the optimization pipeline.
The Tier 2 interpreter can be enabled by configuring Python
with ``--enable-experimental-jit=interpreter``.

* When the ``--enable-experimental-jit`` option is used, the optimized
* When the JIT is enabled, the optimized
Tier 2 IR is translated to machine code, which is then executed.
This does not require additional runtime options.

* The machine code translation process uses an architecture called
*copy-and-patch*. It has no runtime dependencies, but there is a new
build-time dependency on LLVM.

The ``--enable-experimental-jit`` flag has the following optional values:

* ``no`` (default) -- Disable the entire Tier 2 and JIT pipeline.

* ``yes`` (default if the flag is present without optional value)
-- Enable the JIT. To disable the JIT at runtime,
pass the environment variable ``PYTHON_JIT=0``.

* ``yes-off`` -- Build the JIT but disable it by default.
To enable the JIT at runtime, pass the environment variable
``PYTHON_JIT=1``.

* ``interpreter`` -- Enable the Tier 2 interpreter but disable the JIT.
The interpreter can be disabled by running with
``PYTHON_JIT=0``.

(On Windows, use ``PCbuild/build.bat --enable-jit`` to enable the JIT.)

See :pep:`744` for more details.

(JIT by Brandt Bucher, inspired by a paper by Haoran Xu and Fredrik Kjolstad.
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_opcode_metadata.h

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

2 changes: 1 addition & 1 deletion Include/internal/pycore_uop_metadata.h

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

2 changes: 1 addition & 1 deletion Lib/dis.py
Expand Up @@ -216,7 +216,7 @@ def _get_code_array(co, adaptive):
if op == ENTER_EXECUTOR:
try:
ex = get_executor(co, i)
except ValueError:
except (ValueError, RuntimeError):
ex = None

if ex:
Expand Down
8 changes: 4 additions & 4 deletions Lib/test/support/__init__.py
Expand Up @@ -2539,17 +2539,17 @@ def exceeds_recursion_limit():
# Decorator to disable optimizer while a function run
def without_optimizer(func):
try:
import _testinternalcapi
from _testinternalcapi import get_optimizer, set_optimizer
except ImportError:
return func
@functools.wraps(func)
def wrapper(*args, **kwargs):
save_opt = _testinternalcapi.get_optimizer()
save_opt = get_optimizer()
try:
_testinternalcapi.set_optimizer(None)
set_optimizer(None)
return func(*args, **kwargs)
finally:
_testinternalcapi.set_optimizer(save_opt)
set_optimizer(save_opt)
return wrapper


Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_capi/test_opt.py
Expand Up @@ -34,6 +34,8 @@ def clear_executors(func):


@requires_specialization
@unittest.skipUnless(hasattr(_testinternalcapi, "get_optimizer"),
"Requires optimizer infrastructure")
class TestOptimizerAPI(unittest.TestCase):

def test_new_counter_optimizer_dealloc(self):
Expand Down Expand Up @@ -136,6 +138,8 @@ def get_opnames(ex):


@requires_specialization
@unittest.skipUnless(hasattr(_testinternalcapi, "get_optimizer"),
"Requires optimizer infrastructure")
class TestExecutorInvalidation(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -215,6 +219,8 @@ def f():


@requires_specialization
@unittest.skipUnless(hasattr(_testinternalcapi, "get_optimizer"),
"Requires optimizer infrastructure")
@unittest.skipIf(os.getenv("PYTHON_UOPS_OPTIMIZE") == "0", "Needs uop optimizer to run.")
class TestUops(unittest.TestCase):

Expand Down Expand Up @@ -579,6 +585,8 @@ def testfunc(n):


@requires_specialization
@unittest.skipUnless(hasattr(_testinternalcapi, "get_optimizer"),
"Requires optimizer infrastructure")
@unittest.skipIf(os.getenv("PYTHON_UOPS_OPTIMIZE") == "0", "Needs uop optimizer to run.")
class TestUopsOptimization(unittest.TestCase):

Expand Down
10 changes: 6 additions & 4 deletions Lib/test/test_monitoring.py
Expand Up @@ -1831,15 +1831,17 @@ class TestOptimizer(MonitoringTestBase, unittest.TestCase):

def setUp(self):
_testinternalcapi = import_module("_testinternalcapi")
self.old_opt = _testinternalcapi.get_optimizer()
opt = _testinternalcapi.new_counter_optimizer()
_testinternalcapi.set_optimizer(opt)
if hasattr(_testinternalcapi, "get_optimizer"):
self.old_opt = _testinternalcapi.get_optimizer()
opt = _testinternalcapi.new_counter_optimizer()
_testinternalcapi.set_optimizer(opt)
super(TestOptimizer, self).setUp()

def tearDown(self):
super(TestOptimizer, self).tearDown()
import _testinternalcapi
_testinternalcapi.set_optimizer(self.old_opt)
if hasattr(_testinternalcapi, "get_optimizer"):
_testinternalcapi.set_optimizer(self.old_opt)

def test_for_loop(self):
def test_func(x):
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_opcache.py
Expand Up @@ -16,6 +16,8 @@

def disabling_optimizer(func):
def wrapper(*args, **kwargs):
if not hasattr(_testinternalcapi, "get_optimizer"):
return func(*args, **kwargs)
old_opt = _testinternalcapi.get_optimizer()
_testinternalcapi.set_optimizer(None)
try:
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_optimizer.py
Expand Up @@ -80,6 +80,8 @@ def func(x=0):

class TestOptimizerSymbols(unittest.TestCase):

@unittest.skipUnless(hasattr(_testinternalcapi, "uop_symbols_test"),
"requires _testinternalcapi.uop_symbols_test")
def test_optimizer_symbols(self):
_testinternalcapi.uop_symbols_test()

Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_weakref.py
Expand Up @@ -17,6 +17,7 @@
from test.support import gc_collect
from test.support import import_helper
from test.support import threading_helper
from test.support import is_wasi, Py_DEBUG

# Used in ReferencesTestCase.test_ref_created_during_del() .
ref_from_del = None
Expand Down Expand Up @@ -960,6 +961,7 @@ def test_hashing(self):
self.assertEqual(hash(a), hash(42))
self.assertRaises(TypeError, hash, b)

@unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack")
def test_trashcan_16602(self):
# Issue #16602: when a weakref's target was part of a long
# deallocation chain, the trashcan mechanism could delay clearing
Expand Down
@@ -0,0 +1,4 @@
Change how to use the tier 2 interpreter. Instead of running Python with
``-X uops`` or setting the environment variable ``PYTHON_UOPS=1``, this
choice is now made at build time by configuring with
``--enable-experimental-jit=interpreter``.
6 changes: 6 additions & 0 deletions Modules/_opcode.c
Expand Up @@ -367,7 +367,13 @@ _opcode_get_executor_impl(PyObject *module, PyObject *code, int offset)
Py_TYPE(code)->tp_name);
return NULL;
}
#ifdef _Py_TIER2
return (PyObject *)PyUnstable_GetExecutor((PyCodeObject *)code, offset);
#else
PyErr_Format(PyExc_RuntimeError,
"Executors are not available in this build");
return NULL;
#endif
}

static PyMethodDef
Expand Down
13 changes: 12 additions & 1 deletion Modules/_testinternalcapi.c
Expand Up @@ -985,6 +985,8 @@ get_co_framesize(PyObject *self, PyObject *arg)
return PyLong_FromLong(code->co_framesize);
}

#ifdef _Py_TIER2

static PyObject *
new_counter_optimizer(PyObject *self, PyObject *arg)
{
Expand Down Expand Up @@ -1012,7 +1014,10 @@ set_optimizer(PyObject *self, PyObject *opt)
static PyObject *
get_optimizer(PyObject *self, PyObject *Py_UNUSED(ignored))
{
PyObject *opt = (PyObject *)PyUnstable_GetOptimizer();
PyObject *opt = NULL;
#ifdef _Py_TIER2
opt = (PyObject *)PyUnstable_GetOptimizer();
#endif
if (opt == NULL) {
Py_RETURN_NONE;
}
Expand Down Expand Up @@ -1045,6 +1050,8 @@ invalidate_executors(PyObject *self, PyObject *obj)
Py_RETURN_NONE;
}

#endif

static int _pending_callback(void *arg)
{
/* we assume the argument is callable object to which we own a reference */
Expand Down Expand Up @@ -2020,12 +2027,14 @@ static PyMethodDef module_functions[] = {
{"iframe_getline", iframe_getline, METH_O, NULL},
{"iframe_getlasti", iframe_getlasti, METH_O, NULL},
{"get_co_framesize", get_co_framesize, METH_O, NULL},
#ifdef _Py_TIER2
{"get_optimizer", get_optimizer, METH_NOARGS, NULL},
{"set_optimizer", set_optimizer, METH_O, NULL},
{"new_counter_optimizer", new_counter_optimizer, METH_NOARGS, NULL},
{"new_uop_optimizer", new_uop_optimizer, METH_NOARGS, NULL},
{"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL},
{"invalidate_executors", invalidate_executors, METH_O, NULL},
#endif
{"pending_threadfunc", _PyCFunction_CAST(pending_threadfunc),
METH_VARARGS | METH_KEYWORDS},
{"pending_identify", pending_identify, METH_VARARGS, NULL},
Expand Down Expand Up @@ -2072,7 +2081,9 @@ static PyMethodDef module_functions[] = {
{"py_thread_id", get_py_thread_id, METH_NOARGS},
#endif
{"set_immortalize_deferred", set_immortalize_deferred, METH_VARARGS},
#ifdef _Py_TIER2
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
#endif
{NULL, NULL} /* sentinel */
};

Expand Down
6 changes: 6 additions & 0 deletions Objects/codeobject.c
Expand Up @@ -1496,6 +1496,8 @@ PyCode_GetFreevars(PyCodeObject *code)
return _PyCode_GetFreevars(code);
}

#ifdef _Py_TIER2

static void
clear_executors(PyCodeObject *co)
{
Expand All @@ -1515,6 +1517,8 @@ _PyCode_Clear_Executors(PyCodeObject *code)
clear_executors(code);
}

#endif

static void
deopt_code(PyCodeObject *code, _Py_CODEUNIT *instructions)
{
Expand Down Expand Up @@ -1739,9 +1743,11 @@ code_dealloc(PyCodeObject *co)

PyMem_Free(co_extra);
}
#ifdef _Py_TIER2
if (co->co_executors != NULL) {
clear_executors(co);
}
#endif

Py_XDECREF(co->co_consts);
Py_XDECREF(co->co_names);
Expand Down
4 changes: 4 additions & 0 deletions Objects/object.c
Expand Up @@ -2281,9 +2281,11 @@ static PyTypeObject* static_types[] = {
&_PyBufferWrapper_Type,
&_PyContextTokenMissing_Type,
&_PyCoroWrapper_Type,
#ifdef _Py_TIER2
&_PyCounterExecutor_Type,
&_PyCounterOptimizer_Type,
&_PyDefaultOptimizer_Type,
#endif
&_Py_GenericAliasIterType,
&_PyHamtItems_Type,
&_PyHamtKeys_Type,
Expand All @@ -2304,8 +2306,10 @@ static PyTypeObject* static_types[] = {
&_PyPositionsIterator,
&_PyUnicodeASCIIIter_Type,
&_PyUnion_Type,
#ifdef _Py_TIER2
&_PyUOpExecutor_Type,
&_PyUOpOptimizer_Type,
#endif
&_PyWeakref_CallableProxyType,
&_PyWeakref_ProxyType,
&_PyWeakref_RefType,
Expand Down
6 changes: 6 additions & 0 deletions PCbuild/_testinternalcapi.vcxproj
Expand Up @@ -108,6 +108,12 @@
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions Condition="'$(UseJIT)' == 'true'">_Py_JIT;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(UseTIER2)' != '0'">_Py_TIER2=$(UseTIER2);%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
Expand Down
11 changes: 9 additions & 2 deletions PCbuild/build.bat
Expand Up @@ -36,7 +36,9 @@ echo. overrides -c and -d
echo. --disable-gil Enable experimental support for running without the GIL.
echo. --test-marker Enable the test marker within the build.
echo. --regen Regenerate all opcodes, grammar and tokens.
echo. --experimental-jit Enable the experimental just-in-time compiler.
echo. --experimental-jit Enable the experimental just-in-time compiler.
echo. --experimental-jit-off Ditto but off by default (PYTHON_JIT=1 enables).
echo. --experimental-interpreter Enable the experimental Tier 2 interpreter.
echo.
echo.Available flags to avoid building certain modules.
echo.These flags have no effect if '-e' is not given:
Expand Down Expand Up @@ -66,6 +68,7 @@ set verbose=/nologo /v:m /clp:summary
set kill=
set do_pgo=
set pgo_job=-m test --pgo
set UseTIER2=0

:CheckOpts
if "%~1"=="-h" goto Usage
Expand All @@ -86,7 +89,10 @@ if "%~1"=="--disable-gil" (set UseDisableGil=true) & shift & goto CheckOpts
if "%~1"=="--test-marker" (set UseTestMarker=true) & shift & goto CheckOpts
if "%~1"=="-V" shift & goto Version
if "%~1"=="--regen" (set Regen=true) & shift & goto CheckOpts
if "%~1"=="--experimental-jit" (set UseJIT=true) & shift & goto CheckOpts
if "%~1"=="--experimental-jit" (set UseJIT=true) & (set UseTIER2=1) & shift & goto CheckOpts
if "%~1"=="--experimental-jit-off" (set UseJIT=true) & (set UseTIER2=3) & shift & goto CheckOpts
if "%~1"=="--experimental-interpreter" (set UseTIER2=4) & shift & goto CheckOpts
if "%~1"=="--experimental-interpreter-off" (set UseTIER2=6) & shift & goto CheckOpts
rem These use the actual property names used by MSBuild. We could just let
rem them in through the environment, but we specify them on the command line
rem anyway for visibility so set defaults after this
Expand Down Expand Up @@ -179,6 +185,7 @@ echo on
/p:DisableGil=%UseDisableGil%^
/p:UseTestMarker=%UseTestMarker% %GITProperty%^
/p:UseJIT=%UseJIT%^
/p:UseTIER2=%UseTIER2%^
%1 %2 %3 %4 %5 %6 %7 %8 %9

@echo off
Expand Down
1 change: 1 addition & 0 deletions PCbuild/pythoncore.vcxproj
Expand Up @@ -105,6 +105,7 @@
<PreprocessorDefinitions>_USRDLL;Py_BUILD_CORE;Py_BUILD_CORE_BUILTIN;Py_ENABLE_SHARED;MS_DLL_ID="$(SysWinVer)";%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(IncludeExternals)">_Py_HAVE_ZLIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(UseJIT)' == 'true'">_Py_JIT;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(UseTIER2)' != '0'">_Py_TIER2=$(UseTIER2);%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalDependencies>version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies)</AdditionalDependencies>
Expand Down

0 comments on commit 3345321

Please sign in to comment.