Skip to content

Commit e11e149

Browse files
committed
Allow keeping specialization enabled when specifying eval frame function
1 parent 6450b1d commit e11e149

File tree

10 files changed

+185
-28
lines changed

10 files changed

+185
-28
lines changed

Doc/c-api/subinterpreters.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,14 +391,31 @@ High-level APIs
391391
.. versionadded:: 3.9
392392
393393
394-
.. c:function:: void _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, _PyFrameEvalFunction eval_frame)
394+
.. c:function:: void _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp, _PyFrameEvalFunction eval_frame, int allow_specialization)
395395
396396
Set the frame evaluation function.
397397
398+
If *allow_specialization* is non-zero, the adaptive specializer will
399+
continue to specialize bytecodes even though a custom eval frame function
400+
is set. When *allow_specialization* is zero, setting a custom eval frame
401+
disables specialization.
402+
398403
See the :pep:`523` "Adding a frame evaluation API to CPython".
399404
400405
.. versionadded:: 3.9
401406
407+
.. versionchanged:: 3.15
408+
Added the *allow_specialization* parameter.
409+
410+
411+
.. c:function:: int _PyInterpreterState_IsSpecializationEnabled(PyInterpreterState *interp)
412+
413+
Return non-zero if adaptive specialization is enabled for the interpreter.
414+
Specialization is enabled when no custom eval frame function is set, or
415+
when one is set with *allow_specialization* enabled.
416+
417+
.. versionadded:: 3.15
418+
402419
403420
Low-level APIs
404421
--------------

Include/cpython/pystate.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,7 @@ PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc(
318318
PyInterpreterState *interp);
319319
PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc(
320320
PyInterpreterState *interp,
321-
_PyFrameEvalFunction eval_frame);
321+
_PyFrameEvalFunction eval_frame,
322+
int allow_specialization);
323+
PyAPI_FUNC(int) _PyInterpreterState_IsSpecializationEnabled(
324+
PyInterpreterState *interp);

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ struct _is {
899899
PyObject *builtins_copy;
900900
// Initialized to _PyEval_EvalFrameDefault().
901901
_PyFrameEvalFunction eval_frame;
902+
int eval_frame_allow_specialization;
902903

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

Lib/test/test_capi/test_misc.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,90 @@ 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+
func_calls = [c for c in actual_calls if c == "func"]
2925+
self.assertEqual(len(func_calls), 0)
2926+
2927+
# But the normal interpreter loop still shouldn't be inlining things
2928+
func_outer_calls = [c for c in actual_calls if c == "func_outer"]
2929+
self.assertNotEqual(len(func_outer_calls), 0)
2930+
2931+
def test_no_specialization_call(self):
2932+
# Without allow_specialization, ALL calls go through the eval frame.
2933+
# This is the existing PEP 523 behavior.
2934+
def inner(x=42):
2935+
pass
2936+
def func():
2937+
inner()
2938+
2939+
# Pre-specialize
2940+
for _ in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE):
2941+
func()
2942+
2943+
actual_calls = []
2944+
try:
2945+
_testinternalcapi.set_eval_frame_record(actual_calls)
2946+
for _ in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE):
2947+
func()
2948+
finally:
2949+
_testinternalcapi.set_eval_frame_default()
2950+
2951+
# Without allow_specialization, every call including inner() goes
2952+
# through the eval frame
2953+
expected = ["func", "inner"] * SUFFICIENT_TO_DEOPT_AND_SPECIALIZE
2954+
self.assertEqual(actual_calls, expected)
2955+
2956+
28732957
@unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED')
28742958
class TestPyThreadId(unittest.TestCase):
28752959
def test_py_thread_id(self):

Modules/_testinternalcapi.c

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,7 @@ static PyObject *
929929
set_eval_frame_default(PyObject *self, PyObject *Py_UNUSED(args))
930930
{
931931
module_state *state = get_module_state(self);
932-
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), _PyEval_EvalFrameDefault);
932+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), _PyEval_EvalFrameDefault, 0);
933933
Py_CLEAR(state->record_list);
934934
Py_RETURN_NONE;
935935
}
@@ -961,7 +961,7 @@ set_eval_frame_record(PyObject *self, PyObject *list)
961961
return NULL;
962962
}
963963
Py_XSETREF(state->record_list, Py_NewRef(list));
964-
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), record_eval);
964+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), record_eval, 0);
965965
Py_RETURN_NONE;
966966
}
967967

@@ -996,12 +996,49 @@ 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, 1);
1028+
} else {
1029+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), Test_EvalFrame, 1);
1030+
}
1031+
10021032
Py_RETURN_NONE;
10031033
}
10041034

1035+
static PyObject *
1036+
is_specialization_enabled(PyObject *self, PyObject *Py_UNUSED(args))
1037+
{
1038+
return PyBool_FromLong(
1039+
_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET()));
1040+
}
1041+
10051042
/*[clinic input]
10061043
10071044
_testinternalcapi.compiler_cleandoc -> object
@@ -2861,8 +2898,9 @@ static PyMethodDef module_functions[] = {
28612898
{"EncodeLocaleEx", encode_locale_ex, METH_VARARGS},
28622899
{"DecodeLocaleEx", decode_locale_ex, METH_VARARGS},
28632900
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
2864-
{"set_eval_frame_interp", set_eval_frame_interp, METH_NOARGS, NULL},
2901+
{"set_eval_frame_interp", set_eval_frame_interp, METH_VARARGS, NULL},
28652902
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
2903+
{"is_specialization_enabled", is_specialization_enabled, METH_NOARGS, NULL},
28662904
_TESTINTERNALCAPI_COMPILER_CLEANDOC_METHODDEF
28672905
_TESTINTERNALCAPI_NEW_INSTRUCTION_SEQUENCE_METHODDEF
28682906
_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: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,15 @@ 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(tstate->interp->eval_frame == NULL || \
226+
tstate->interp->eval_frame_allow_specialization); \
227+
_PyFrame_SetStackPointer(frame, stack_pointer); \
228+
assert((NEW_FRAME)->previous == frame); \
229+
frame = tstate->current_frame = (NEW_FRAME); \
230+
CALL_STAT_INC(inlined_py_calls); \
231+
JUMP_TO_LABEL(start_frame); \
231232
} while (0)
232233

233234
/* Tuple access macros */

Python/perf_trampoline.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,12 +530,12 @@ _PyPerfTrampoline_Init(int activate)
530530
code_watcher_id = -1;
531531
}
532532
if (!activate) {
533-
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame);
533+
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame, 0);
534534
perf_status = PERF_STATUS_NO_INIT;
535535
}
536536
else if (tstate->interp->eval_frame != py_trampoline_evaluator) {
537537
prev_eval_frame = _PyInterpreterState_GetEvalFrameFunc(tstate->interp);
538-
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator);
538+
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator, 0);
539539
extra_code_index = _PyEval_RequestCodeExtraIndex(NULL);
540540
if (extra_code_index == -1) {
541541
return -1;
@@ -568,7 +568,7 @@ _PyPerfTrampoline_Fini(void)
568568
}
569569
PyThreadState *tstate = _PyThreadState_GET();
570570
if (tstate->interp->eval_frame == py_trampoline_evaluator) {
571-
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, NULL);
571+
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, NULL, 0);
572572
}
573573
if (perf_status == PERF_STATUS_OK) {
574574
trampoline_api.free_state(trampoline_api.state);

Python/pystate.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3010,12 +3010,14 @@ _PyInterpreterState_GetEvalFrameFunc(PyInterpreterState *interp)
30103010

30113011
void
30123012
_PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp,
3013-
_PyFrameEvalFunction eval_frame)
3013+
_PyFrameEvalFunction eval_frame,
3014+
int allow_specialization)
30143015
{
30153016
if (eval_frame == _PyEval_EvalFrameDefault) {
30163017
eval_frame = NULL;
30173018
}
30183019
if (eval_frame == interp->eval_frame) {
3020+
interp->eval_frame_allow_specialization = allow_specialization;
30193021
return;
30203022
}
30213023
#ifdef _Py_TIER2
@@ -3026,9 +3028,17 @@ _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp,
30263028
RARE_EVENT_INC(set_eval_frame_func);
30273029
_PyEval_StopTheWorld(interp);
30283030
interp->eval_frame = eval_frame;
3031+
interp->eval_frame_allow_specialization = allow_specialization;
30293032
_PyEval_StartTheWorld(interp);
30303033
}
30313034

3035+
int
3036+
_PyInterpreterState_IsSpecializationEnabled(PyInterpreterState *interp)
3037+
{
3038+
return interp->eval_frame == NULL
3039+
|| interp->eval_frame_allow_specialization;
3040+
}
3041+
30323042

30333043
const PyConfig*
30343044
_PyInterpreterState_GetConfig(PyInterpreterState *interp)

Python/specialize.c

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ do_specialize_instance_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject*
811811
return -1;
812812
}
813813
/* Don't specialize if PEP 523 is active */
814-
if (_PyInterpreterState_GET()->eval_frame) {
814+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
815815
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OTHER);
816816
return -1;
817817
}
@@ -890,7 +890,7 @@ do_specialize_instance_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject*
890890
return -1;
891891
}
892892
/* Don't specialize if PEP 523 is active */
893-
if (_PyInterpreterState_GET()->eval_frame) {
893+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
894894
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OTHER);
895895
return -1;
896896
}
@@ -1697,7 +1697,7 @@ specialize_py_call(PyFunctionObject *func, _Py_CODEUNIT *instr, int nargs,
16971697
PyCodeObject *code = (PyCodeObject *)func->func_code;
16981698
int kind = function_kind(code);
16991699
/* Don't specialize if PEP 523 is active */
1700-
if (_PyInterpreterState_GET()->eval_frame) {
1700+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
17011701
SPECIALIZATION_FAIL(CALL, SPEC_FAIL_CALL_PEP_523);
17021702
return -1;
17031703
}
@@ -1740,7 +1740,7 @@ specialize_py_call_kw(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
}
@@ -2003,7 +2003,7 @@ binary_op_fail_kind(int oparg, PyObject *lhs, PyObject *rhs)
20032003
return SPEC_FAIL_WRONG_NUMBER_ARGUMENTS;
20042004
}
20052005

2006-
if (_PyInterpreterState_GET()->eval_frame) {
2006+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
20072007
/* Don't specialize if PEP 523 is active */
20082008
Py_DECREF(descriptor);
20092009
return SPEC_FAIL_OTHER;
@@ -2312,7 +2312,7 @@ _Py_Specialize_BinaryOp(_PyStackRef lhs_st, _PyStackRef rhs_st, _Py_CODEUNIT *in
23122312
PyHeapTypeObject *ht = (PyHeapTypeObject *)container_type;
23132313
if (kind == SIMPLE_FUNCTION &&
23142314
fcode->co_argcount == 2 &&
2315-
!_PyInterpreterState_GET()->eval_frame && /* Don't specialize if PEP 523 is active */
2315+
_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET()) && /* Don't specialize if PEP 523 is active */
23162316
_PyType_CacheGetItemForSpecialization(ht, descriptor, (uint32_t)tp_version))
23172317
{
23182318
specialize(instr, BINARY_OP_SUBSCR_GETITEM);
@@ -2570,7 +2570,7 @@ _Py_Specialize_ForIter(_PyStackRef iter, _PyStackRef null_or_index, _Py_CODEUNIT
25702570
instr[oparg + INLINE_CACHE_ENTRIES_FOR_ITER + 1].op.code == INSTRUMENTED_END_FOR
25712571
);
25722572
/* Don't specialize if PEP 523 is active */
2573-
if (_PyInterpreterState_GET()->eval_frame) {
2573+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
25742574
goto failure;
25752575
}
25762576
specialize(instr, FOR_ITER_GEN);
@@ -2609,7 +2609,7 @@ _Py_Specialize_Send(_PyStackRef receiver_st, _Py_CODEUNIT *instr)
26092609
PyTypeObject *tp = Py_TYPE(receiver);
26102610
if (tp == &PyGen_Type || tp == &PyCoro_Type) {
26112611
/* Don't specialize if PEP 523 is active */
2612-
if (_PyInterpreterState_GET()->eval_frame) {
2612+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
26132613
SPECIALIZATION_FAIL(SEND, SPEC_FAIL_OTHER);
26142614
goto failure;
26152615
}
@@ -2632,7 +2632,7 @@ _Py_Specialize_CallFunctionEx(_PyStackRef func_st, _Py_CODEUNIT *instr)
26322632

26332633
if (Py_TYPE(func) == &PyFunction_Type &&
26342634
((PyFunctionObject *)func)->vectorcall == _PyFunction_Vectorcall) {
2635-
if (_PyInterpreterState_GET()->eval_frame) {
2635+
if (!_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET())) {
26362636
goto failure;
26372637
}
26382638
specialize(instr, CALL_EX_PY);

0 commit comments

Comments
 (0)