gh-146031: Allow keeping specialization enabled when specifying eval … · python/cpython@c0af5c0 · GitHub
Skip to content

Commit c0af5c0

Browse files
authored
gh-146031: Allow keeping specialization enabled when specifying eval frame function (#146032)
Allow keeping specialization enabled when specifying eval frame function
1 parent cecf564 commit c0af5c0

10 files changed

Lines changed: 196 additions & 20 deletions

File tree

Doc/c-api/subinterpreters.rst

Lines changed: 21 additions & 0 deletions

Include/cpython/pystate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,8 @@ PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc(
319319
PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc(
320320
PyInterpreterState *interp,
321321
_PyFrameEvalFunction eval_frame);
322+
PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameAllowSpecialization(
323+
PyInterpreterState *interp,
324+
int allow_specialization);
325+
PyAPI_FUNC(int) _PyInterpreterState_IsSpecializationEnabled(
326+
PyInterpreterState *interp);

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,7 @@ struct _is {
927927
PyObject *builtins_copy;
928928
// Initialized to _PyEval_EvalFrameDefault().
929929
_PyFrameEvalFunction eval_frame;
930+
int eval_frame_allow_specialization;
930931

931932
PyFunction_WatchCallback func_watchers[FUNC_MAX_WATCHERS];
932933
// One bit is set for each non-NULL entry in func_watchers

Lib/test/test_capi/test_misc.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,88 @@ def func():
28702870
self.do_test(func, names)
28712871

28722872

2873+
class Test_Pep523AllowSpecialization(unittest.TestCase):
2874+
"""Tests for _PyInterpreterState_SetEvalFrameFunc with
2875+
allow_specialization=1."""
2876+
2877+
def test_is_specialization_enabled_default(self):
2878+
# With no custom eval frame, specialization should be enabled
2879+
self.assertTrue(_testinternalcapi.is_specialization_enabled())
2880+
2881+
def test_is_specialization_enabled_with_eval_frame(self):
2882+
# Setting eval frame with allow_specialization=0 disables specialization
2883+
try:
2884+
_testinternalcapi.set_eval_frame_record([])
2885+
self.assertFalse(_testinternalcapi.is_specialization_enabled())
2886+
finally:
2887+
_testinternalcapi.set_eval_frame_default()
2888+
2889+
def test_is_specialization_enabled_after_restore(self):
2890+
# Restoring the default eval frame re-enables specialization
2891+
try:
2892+
_testinternalcapi.set_eval_frame_record([])
2893+
self.assertFalse(_testinternalcapi.is_specialization_enabled())
2894+
finally:
2895+
_testinternalcapi.set_eval_frame_default()
2896+
self.assertTrue(_testinternalcapi.is_specialization_enabled())
2897+
2898+
def test_is_specialization_enabled_with_allow(self):
2899+
# Setting eval frame with allow_specialization=1 keeps it enabled
2900+
try:
2901+
_testinternalcapi.set_eval_frame_interp([])
2902+
self.assertTrue(_testinternalcapi.is_specialization_enabled())
2903+
finally:
2904+
_testinternalcapi.set_eval_frame_default()
2905+
2906+
def test_allow_specialization_call(self):
2907+
def func():
2908+
pass
2909+
2910+
def func_outer():
2911+
func()
2912+
2913+
actual_calls = []
2914+
try:
2915+
_testinternalcapi.set_eval_frame_interp(
2916+
actual_calls)
2917+
for i in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE * 2):
2918+
func_outer()
2919+
finally:
2920+
_testinternalcapi.set_eval_frame_default()
2921+
2922+
# With specialization enabled, calls to inner() will dispatch
2923+
# through the installed frame evaluator
2924+
self.assertEqual(actual_calls.count("func"), 0)
2925+
2926+
# But the normal interpreter loop still shouldn't be inlining things
2927+
self.assertNotEqual(actual_calls.count("func_outer"), 0)
2928+
2929+
def test_no_specialization_call(self):
2930+
# Without allow_specialization, ALL calls go through the eval frame.
2931+
# This is the existing PEP 523 behavior.
2932+
def inner(x=42):
2933+
pass
2934+
def func():
2935+
inner()
2936+
2937+
# Pre-specialize
2938+
for _ in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE):
2939+
func()
2940+
2941+
actual_calls = []
2942+
try:
2943+
_testinternalcapi.set_eval_frame_record(actual_calls)
2944+
for _ in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE):
2945+
func()
2946+
finally:
2947+
_testinternalcapi.set_eval_frame_default()
2948+
2949+
# Without allow_specialization, every call including inner() goes
2950+
# through the eval frame
2951+
expected = ["func", "inner"] * SUFFICIENT_TO_DEOPT_AND_SPECIALIZE
2952+
self.assertEqual(actual_calls, expected)
2953+
2954+
28732955
@unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED')
28742956
class TestPyThreadId(unittest.TestCase):
28752957
def test_py_thread_id(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The unstable API _PyInterpreterState_SetEvalFrameFunc has a companion function _PyInterpreterState_SetEvalFrameAllowSpecialization to specify if specialization should be allowed. When this option is set to 1 the specializer will turn Python -> Python calls into specialized opcodes which the replacement interpreter loop can choose to respect and perform inlined dispatch.

Modules/_testinternalcapi.c

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -996,12 +996,51 @@ get_eval_frame_stats(PyObject *self, PyObject *Py_UNUSED(args))
996996
}
997997

998998
static PyObject *
999-
set_eval_frame_interp(PyObject *self, PyObject *Py_UNUSED(args))
999+
record_eval_interp(PyThreadState *tstate, struct _PyInterpreterFrame *f, int exc)
10001000
{
1001-
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), Test_EvalFrame);
1001+
if (PyStackRef_FunctionCheck(f->f_funcobj)) {
1002+
PyFunctionObject *func = _PyFrame_GetFunction(f);
1003+
PyObject *module = _get_current_module();
1004+
assert(module != NULL);
1005+
module_state *state = get_module_state(module);
1006+
Py_DECREF(module);
1007+
int res = PyList_Append(state->record_list, func->func_name);
1008+
if (res < 0) {
1009+
return NULL;
1010+
}
1011+
}
1012+
1013+
return Test_EvalFrame(tstate, f, exc);
1014+
}
1015+
1016+
static PyObject *
1017+
set_eval_frame_interp(PyObject *self, PyObject *args)
1018+
{
1019+
if (PyTuple_GET_SIZE(args) == 1) {
1020+
module_state *state = get_module_state(self);
1021+
PyObject *list = PyTuple_GET_ITEM(args, 0);
1022+
if (!PyList_Check(list)) {
1023+
PyErr_SetString(PyExc_TypeError, "argument must be a list");
1024+
return NULL;
1025+
}
1026+
Py_XSETREF(state->record_list, Py_NewRef(list));
1027+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), record_eval_interp);
1028+
_PyInterpreterState_SetEvalFrameAllowSpecialization(_PyInterpreterState_GET(), 1);
1029+
} else {
1030+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), Test_EvalFrame);
1031+
_PyInterpreterState_SetEvalFrameAllowSpecialization(_PyInterpreterState_GET(), 1);
1032+
}
1033+
10021034
Py_RETURN_NONE;
10031035
}
10041036

1037+
static PyObject *
1038+
is_specialization_enabled(PyObject *self, PyObject *Py_UNUSED(args))
1039+
{
1040+
return PyBool_FromLong(
1041+
_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET()));
1042+
}
1043+
10051044
/*[clinic input]
10061045
10071046
_testinternalcapi.compiler_cleandoc -> object
@@ -2875,8 +2914,9 @@ static PyMethodDef module_functions[] = {
28752914
{"EncodeLocaleEx", encode_locale_ex, METH_VARARGS},
28762915
{"DecodeLocaleEx", decode_locale_ex, METH_VARARGS},
28772916
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
2878-
{"set_eval_frame_interp", set_eval_frame_interp, METH_NOARGS, NULL},
2917+
{"set_eval_frame_interp", set_eval_frame_interp, METH_VARARGS, NULL},
28792918
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
2919+
{"is_specialization_enabled", is_specialization_enabled, METH_NOARGS, NULL},
28802920
_TESTINTERNALCAPI_COMPILER_CLEANDOC_METHODDEF
28812921
_TESTINTERNALCAPI_NEW_INSTRUCTION_SEQUENCE_METHODDEF
28822922
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF

Modules/_testinternalcapi/interpreter.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
#include "../../Python/ceval_macros.h"
1111

12+
#undef IS_PEP523_HOOKED
13+
#define IS_PEP523_HOOKED(tstate) (tstate->interp->eval_frame != NULL && !tstate->interp->eval_frame_allow_specialization)
14+
1215
int Test_EvalFrame_Resumes, Test_EvalFrame_Loads;
1316

1417
#ifdef _Py_TIER2

Python/ceval_macros.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,14 @@ do { \
220220
DISPATCH_GOTO_NON_TRACING(); \
221221
}
222222

223-
#define DISPATCH_INLINED(NEW_FRAME) \
224-
do { \
225-
assert(tstate->interp->eval_frame == NULL); \
226-
_PyFrame_SetStackPointer(frame, stack_pointer); \
227-
assert((NEW_FRAME)->previous == frame); \
228-
frame = tstate->current_frame = (NEW_FRAME); \
229-
CALL_STAT_INC(inlined_py_calls); \
230-
JUMP_TO_LABEL(start_frame); \
223+
#define DISPATCH_INLINED(NEW_FRAME) \
224+
do { \
225+
assert(!IS_PEP523_HOOKED(tstate)); \
226+
_PyFrame_SetStackPointer(frame, stack_pointer); \
227+
assert((NEW_FRAME)->previous == frame); \
228+
frame = tstate->current_frame = (NEW_FRAME); \
229+
CALL_STAT_INC(inlined_py_calls); \
230+
JUMP_TO_LABEL(start_frame); \
231231
} while (0)
232232

233233
/* Tuple access macros */

Python/pystate.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3026,9 +3026,32 @@ _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp,
30263026
RARE_EVENT_INC(set_eval_frame_func);
30273027
_PyEval_StopTheWorld(interp);
30283028
interp->eval_frame = eval_frame;
3029+
// reset when evaluator is reset
3030+
interp->eval_frame_allow_specialization = 0;
30293031
_PyEval_StartTheWorld(interp);
30303032
}
30313033

3034+
void
3035+
_PyInterpreterState_SetEvalFrameAllowSpecialization(PyInterpreterState *interp,
3036+
int allow_specialization)
3037+
{
3038+
if (allow_specialization == interp->eval_frame_allow_specialization) {
3039+
return;
3040+
}
3041+
_Py_Executors_InvalidateAll(interp, 1);
3042+
RARE_EVENT_INC(set_eval_frame_func);
3043+
_PyEval_StopTheWorld(interp);
3044+
interp->eval_frame_allow_specialization = allow_specialization;
3045+
_PyEval_StartTheWorld(interp);
3046+
}
3047+
3048+
int
3049+
_PyInterpreterState_IsSpecializationEnabled(PyInterpreterState *interp)
3050+
{
3051+
return interp->eval_frame == NULL
3052+
|| interp->eval_frame_allow_specialization;
3053+
}
3054+
30323055

30333056
const PyConfig*
30343057
_PyInterpreterState_GetConfig(PyInterpreterState *interp)

Python/specialize.c

Lines changed: 9 additions & 9 deletions

0 commit comments

Comments
 (0)