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

File tree

10 files changed

+196
-20
lines changed

10 files changed

+196
-20
lines changed

Doc/c-api/subinterpreters.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,27 @@ High-level APIs
399399
400400
.. versionadded:: 3.9
401401
402+
.. c:function:: void _PyInterpreterState_SetEvalFrameAllowSpecialization(PyInterpreterState *interp, int allow_specialization)
403+
404+
Enables or disables specialization why a custom frame evaluator is in place.
405+
406+
If *allow_specialization* is non-zero, the adaptive specializer will
407+
continue to specialize bytecodes even though a custom eval frame function
408+
is set. When *allow_specialization* is zero, setting a custom eval frame
409+
disables specialization. The standard interpreter loop will continue to deopt
410+
while a frame evaluation API is in place - the frame evaluation function needs
411+
to handle the specialized opcodes to take advantage of this.
412+
413+
.. versionadded:: 3.15
414+
415+
.. c:function:: int _PyInterpreterState_IsSpecializationEnabled(PyInterpreterState *interp)
416+
417+
Return non-zero if adaptive specialization is enabled for the interpreter.
418+
Specialization is enabled when no custom eval frame function is set, or
419+
when one is set with *allow_specialization* enabled.
420+
421+
.. versionadded:: 3.15
422+
402423
403424
Low-level APIs
404425
--------------

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
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ do_specialize_instance_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject*
838838
return -1;
839839
}
840840
/* Don't specialize if PEP 523 is active */
841-
if (_PyInterpreterState_GET()->eval_frame) {
841+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
842842
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OTHER);
843843
return -1;
844844
}
@@ -922,7 +922,7 @@ do_specialize_instance_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject*
922922
return -1;
923923
}
924924
/* Don't specialize if PEP 523 is active */
925-
if (_PyInterpreterState_GET()->eval_frame) {
925+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
926926
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OTHER);
927927
return -1;
928928
}
@@ -1740,7 +1740,7 @@ specialize_py_call(PyFunctionObject *func, _Py_CODEUNIT *instr, int nargs,
17401740
PyCodeObject *code = (PyCodeObject *)func->func_code;
17411741
int kind = function_kind(code);
17421742
/* Don't specialize if PEP 523 is active */
1743-
if (_PyInterpreterState_GET()->eval_frame) {
1743+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
17441744
SPECIALIZATION_FAIL(CALL, SPEC_FAIL_CALL_PEP_523);
17451745
return -1;
17461746
}
@@ -1783,7 +1783,7 @@ specialize_py_call_kw(PyFunctionObject *func, _Py_CODEUNIT *instr, int nargs,
17831783
PyCodeObject *code = (PyCodeObject *)func->func_code;
17841784
int kind = function_kind(code);
17851785
/* Don't specialize if PEP 523 is active */
1786-
if (_PyInterpreterState_GET()->eval_frame) {
1786+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
17871787
SPECIALIZATION_FAIL(CALL, SPEC_FAIL_CALL_PEP_523);
17881788
return -1;
17891789
}
@@ -2046,7 +2046,7 @@ binary_op_fail_kind(int oparg, PyObject *lhs, PyObject *rhs)
20462046
return SPEC_FAIL_WRONG_NUMBER_ARGUMENTS;
20472047
}
20482048

2049-
if (_PyInterpreterState_GET()->eval_frame) {
2049+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
20502050
/* Don't specialize if PEP 523 is active */
20512051
Py_DECREF(descriptor);
20522052
return SPEC_FAIL_OTHER;
@@ -2449,7 +2449,7 @@ _Py_Specialize_BinaryOp(_PyStackRef lhs_st, _PyStackRef rhs_st, _Py_CODEUNIT *in
24492449
PyHeapTypeObject *ht = (PyHeapTypeObject *)container_type;
24502450
if (kind == SIMPLE_FUNCTION &&
24512451
fcode->co_argcount == 2 &&
2452-
!_PyInterpreterState_GET()->eval_frame && /* Don't specialize if PEP 523 is active */
2452+
_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET()) && /* Don't specialize if PEP 523 is active */
24532453
_PyType_CacheGetItemForSpecialization(ht, descriptor, (uint32_t)tp_version))
24542454
{
24552455
specialize(instr, BINARY_OP_SUBSCR_GETITEM);
@@ -2707,7 +2707,7 @@ _Py_Specialize_ForIter(_PyStackRef iter, _PyStackRef null_or_index, _Py_CODEUNIT
27072707
instr[oparg + INLINE_CACHE_ENTRIES_FOR_ITER + 1].op.code == INSTRUMENTED_END_FOR
27082708
);
27092709
/* Don't specialize if PEP 523 is active */
2710-
if (_PyInterpreterState_GET()->eval_frame) {
2710+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
27112711
goto failure;
27122712
}
27132713
specialize(instr, FOR_ITER_GEN);
@@ -2750,7 +2750,7 @@ _Py_Specialize_Send(_PyStackRef receiver_st, _Py_CODEUNIT *instr)
27502750
PyTypeObject *tp = Py_TYPE(receiver);
27512751
if (tp == &PyGen_Type || tp == &PyCoro_Type) {
27522752
/* Don't specialize if PEP 523 is active */
2753-
if (_PyInterpreterState_GET()->eval_frame) {
2753+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
27542754
SPECIALIZATION_FAIL(SEND, SPEC_FAIL_OTHER);
27552755
goto failure;
27562756
}
@@ -2773,7 +2773,7 @@ _Py_Specialize_CallFunctionEx(_PyStackRef func_st, _Py_CODEUNIT *instr)
27732773

27742774
if (Py_TYPE(func) == &PyFunction_Type &&
27752775
((PyFunctionObject *)func)->vectorcall == _PyFunction_Vectorcall) {
2776-
if (_PyInterpreterState_GET()->eval_frame) {
2776+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
27772777
goto failure;
27782778
}
27792779
specialize(instr, CALL_EX_PY);

0 commit comments

Comments
 (0)