Skip to content

Commit 2073854

Browse files
committed
Allow keeping specialization enabled when specifying eval frame function
1 parent 3d0824a commit 2073854

File tree

8 files changed

+160
-18
lines changed

8 files changed

+160
-18
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,94 @@ 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_record_with_specialization([])
2902+
self.assertTrue(_testinternalcapi.is_specialization_enabled())
2903+
finally:
2904+
_testinternalcapi.set_eval_frame_default()
2905+
2906+
def test_allow_specialization_call(self):
2907+
# With allow_specialization, after enough iterations the CALL
2908+
# specialization should kick in and inline calls, so the eval frame
2909+
# function is NOT called for the inner function.
2910+
def inner(x=42):
2911+
pass
2912+
def func():
2913+
inner()
2914+
2915+
actual_calls = []
2916+
try:
2917+
_testinternalcapi.set_eval_frame_record_with_specialization(
2918+
actual_calls)
2919+
for i in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE * 2):
2920+
func()
2921+
finally:
2922+
_testinternalcapi.set_eval_frame_default()
2923+
2924+
# With specialization enabled, calls to inner() can be inlined
2925+
# so "inner" should NOT appear in every call record.
2926+
# At minimum, "func" should appear for every call.
2927+
func_calls = [c for c in actual_calls if c == "func"]
2928+
self.assertEqual(len(func_calls),
2929+
SUFFICIENT_TO_DEOPT_AND_SPECIALIZE * 2)
2930+
inner_calls = [c for c in actual_calls if c == "inner"]
2931+
# inner should appear fewer times than func because specialization
2932+
# inlines the call
2933+
self.assertLess(len(inner_calls), len(func_calls))
2934+
2935+
def test_no_specialization_call(self):
2936+
# Without allow_specialization, ALL calls go through the eval frame.
2937+
# This is the existing PEP 523 behavior.
2938+
def inner(x=42):
2939+
pass
2940+
def func():
2941+
inner()
2942+
2943+
# Pre-specialize
2944+
for _ in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE):
2945+
func()
2946+
2947+
actual_calls = []
2948+
try:
2949+
_testinternalcapi.set_eval_frame_record(actual_calls)
2950+
for _ in range(SUFFICIENT_TO_DEOPT_AND_SPECIALIZE):
2951+
func()
2952+
finally:
2953+
_testinternalcapi.set_eval_frame_default()
2954+
2955+
# Without allow_specialization, every call including inner() goes
2956+
# through the eval frame
2957+
expected = ["func", "inner"] * SUFFICIENT_TO_DEOPT_AND_SPECIALIZE
2958+
self.assertEqual(actual_calls, expected)
2959+
2960+
28732961
@unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED')
28742962
class TestPyThreadId(unittest.TestCase):
28752963
def test_py_thread_id(self):

Modules/_testinternalcapi.c

Lines changed: 26 additions & 3 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

@@ -998,10 +998,30 @@ get_eval_frame_stats(PyObject *self, PyObject *Py_UNUSED(args))
998998
static PyObject *
999999
set_eval_frame_interp(PyObject *self, PyObject *Py_UNUSED(args))
10001000
{
1001-
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), Test_EvalFrame);
1001+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), Test_EvalFrame, 0);
10021002
Py_RETURN_NONE;
10031003
}
10041004

1005+
static PyObject *
1006+
set_eval_frame_record_with_specialization(PyObject *self, PyObject *list)
1007+
{
1008+
module_state *state = get_module_state(self);
1009+
if (!PyList_Check(list)) {
1010+
PyErr_SetString(PyExc_TypeError, "argument must be a list");
1011+
return NULL;
1012+
}
1013+
Py_XSETREF(state->record_list, Py_NewRef(list));
1014+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), record_eval, 1);
1015+
Py_RETURN_NONE;
1016+
}
1017+
1018+
static PyObject *
1019+
is_specialization_enabled(PyObject *self, PyObject *Py_UNUSED(args))
1020+
{
1021+
return PyBool_FromLong(
1022+
_PyInterpreterState_IsSpecializationEnabled(_PyInterpreterState_GET()));
1023+
}
1024+
10051025
/*[clinic input]
10061026
10071027
_testinternalcapi.compiler_cleandoc -> object
@@ -2863,6 +2883,9 @@ static PyMethodDef module_functions[] = {
28632883
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
28642884
{"set_eval_frame_interp", set_eval_frame_interp, METH_NOARGS, NULL},
28652885
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
2886+
{"set_eval_frame_record_with_specialization",
2887+
set_eval_frame_record_with_specialization, METH_O, NULL},
2888+
{"is_specialization_enabled", is_specialization_enabled, METH_NOARGS, NULL},
28662889
_TESTINTERNALCAPI_COMPILER_CLEANDOC_METHODDEF
28672890
_TESTINTERNALCAPI_NEW_INSTRUCTION_SEQUENCE_METHODDEF
28682891
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF

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)