Skip to content

Commit 6f7bb29

Browse files
pythongh-146261: JIT: protect against function version changes (python#146300)
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
1 parent 1e79bf6 commit 6f7bb29

File tree

8 files changed

+64
-189
lines changed

8 files changed

+64
-189
lines changed

Include/internal/pycore_optimizer.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,6 @@ extern PyCodeObject *_Py_uop_sym_get_probable_func_code(JitOptRef sym);
424424
extern PyObject *_Py_uop_sym_get_probable_value(JitOptRef sym);
425425
extern PyTypeObject *_Py_uop_sym_get_probable_type(JitOptRef sym);
426426
extern JitOptRef *_Py_uop_sym_set_stack_depth(JitOptContext *ctx, int stack_depth, JitOptRef *current_sp);
427-
extern uint32_t _Py_uop_sym_get_func_version(JitOptRef ref);
428-
bool _Py_uop_sym_set_func_version(JitOptContext *ctx, JitOptRef ref, uint32_t version);
429427

430428
extern void _Py_uop_abstractcontext_init(JitOptContext *ctx, _PyBloomFilter *dependencies);
431429
extern void _Py_uop_abstractcontext_fini(JitOptContext *ctx);

Include/internal/pycore_optimizer_types.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ typedef enum _JitSymType {
3636
JIT_SYM_NON_NULL_TAG = 3,
3737
JIT_SYM_BOTTOM_TAG = 4,
3838
JIT_SYM_TYPE_VERSION_TAG = 5,
39-
JIT_SYM_FUNC_VERSION_TAG = 6,
4039
JIT_SYM_KNOWN_CLASS_TAG = 7,
4140
JIT_SYM_KNOWN_VALUE_TAG = 8,
4241
JIT_SYM_TUPLE_TAG = 9,

Lib/test/test_capi/test_opt.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,9 +1622,8 @@ def testfunc(n):
16221622
self.assertEqual(uops.count("_PUSH_FRAME"), 2)
16231623
# Type version propagation: one guard covers both method lookups
16241624
self.assertEqual(uops.count("_GUARD_TYPE_VERSION"), 1)
1625-
# Function checks eliminated (type info resolves the callable)
1626-
self.assertNotIn("_CHECK_FUNCTION_VERSION", uops)
1627-
self.assertNotIn("_CHECK_FUNCTION_EXACT_ARGS", uops)
1625+
# Function checks cannot be eliminated for safety reasons.
1626+
self.assertIn("_CHECK_FUNCTION_VERSION", uops)
16281627

16291628
def test_method_chain_guard_elimination(self):
16301629
"""
@@ -1669,10 +1668,7 @@ def testfunc(n):
16691668
self.assertIsNotNone(ex)
16701669
uops = get_opnames(ex)
16711670
self.assertIn("_PUSH_FRAME", uops)
1672-
# Both should be not present, as this is a call
1673-
# to a simple function with a known function version.
1674-
self.assertNotIn("_CHECK_FUNCTION_VERSION_INLINE", uops)
1675-
self.assertNotIn("_CHECK_FUNCTION_VERSION", uops)
1671+
self.assertIn("_CHECK_FUNCTION_VERSION", uops)
16761672
# Removed guard
16771673
self.assertNotIn("_CHECK_FUNCTION_EXACT_ARGS", uops)
16781674

@@ -5178,6 +5174,27 @@ def g():
51785174
PYTHON_JIT="1", PYTHON_JIT_STRESS="1")
51795175
self.assertEqual(result[0].rc, 0, result)
51805176

5177+
def test_func_version_guarded_on_change(self):
5178+
def testfunc(n):
5179+
for i in range(n):
5180+
# Only works on functions promoted to constants
5181+
global_identity_code_will_be_modified(i)
5182+
5183+
testfunc(TIER2_THRESHOLD)
5184+
5185+
ex = get_first_executor(testfunc)
5186+
self.assertIsNotNone(ex)
5187+
uops = get_opnames(ex)
5188+
self.assertIn("_PUSH_FRAME", uops)
5189+
self.assertIn("_CHECK_FUNCTION_VERSION", uops)
5190+
5191+
global_identity_code_will_be_modified.__code__ = (lambda a: 0xdeadead).__code__
5192+
_testinternalcapi.clear_executor_deletion_list()
5193+
ex = get_first_executor(testfunc)
5194+
self.assertIsNone(ex)
5195+
# JItted code should've deopted.
5196+
self.assertEqual(global_identity_code_will_be_modified(None), 0xdeadead)
5197+
51815198
def test_call_super(self):
51825199
class A:
51835200
def method1(self):
@@ -5224,6 +5241,9 @@ def testfunc(n):
52245241
def global_identity(x):
52255242
return x
52265243

5244+
def global_identity_code_will_be_modified(x):
5245+
return x
5246+
52275247
class TestObject:
52285248
def test(self, *args, **kwargs):
52295249
return args[0]

Objects/funcobject.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
99
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
1010
#include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount()
11+
#include "pycore_optimizer.h" // _Py_Executors_InvalidateDependency()
1112
#include "pycore_pyerrors.h" // _PyErr_Occurred()
1213
#include "pycore_setobject.h" // _PySet_NextEntry()
1314
#include "pycore_stats.h"
@@ -63,6 +64,13 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func,
6364
case PyFunction_EVENT_MODIFY_DEFAULTS:
6465
case PyFunction_EVENT_MODIFY_KWDEFAULTS:
6566
case PyFunction_EVENT_MODIFY_QUALNAME:
67+
#if _Py_TIER2
68+
// Note: we only invalidate JIT code if a function version changes.
69+
// Not when the function is deallocated.
70+
// Function deallocation occurs frequently (think: lambdas),
71+
// so we want to minimize dependency invalidation there.
72+
_Py_Executors_InvalidateDependency(interp, func, 1);
73+
#endif
6674
RARE_EVENT_INTERP_INC(interp, func_modification);
6775
break;
6876
default:

Python/optimizer_analysis.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,6 @@ add_op(JitOptContext *ctx, _PyUOpInstruction *this_instr,
283283
#define sym_get_probable_func_code _Py_uop_sym_get_probable_func_code
284284
#define sym_get_probable_value _Py_uop_sym_get_probable_value
285285
#define sym_set_stack_depth(DEPTH, SP) _Py_uop_sym_set_stack_depth(ctx, DEPTH, SP)
286-
#define sym_get_func_version _Py_uop_sym_get_func_version
287-
#define sym_set_func_version _Py_uop_sym_set_func_version
288286

289287
/* Comparison oparg masks */
290288
#define COMPARE_LT_MASK 2

Python/optimizer_bytecodes.c

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,12 +1001,15 @@ dummy_func(void) {
10011001
}
10021002

10031003
op(_CHECK_FUNCTION_VERSION, (func_version/2, callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) {
1004-
if (sym_get_func_version(callable) == func_version) {
1005-
REPLACE_OP(this_instr, _NOP, 0, 0);
1006-
}
1007-
else {
1008-
sym_set_func_version(ctx, callable, func_version);
1004+
PyObject *func = sym_get_probable_value(callable);
1005+
if (func == NULL || !PyFunction_Check(func) || ((PyFunctionObject *)func)->func_version != func_version) {
1006+
ctx->contradiction = true;
1007+
ctx->done = true;
1008+
break;
10091009
}
1010+
// Guarded on this, so it can be promoted.
1011+
sym_set_const(callable, func);
1012+
_Py_BloomFilter_Add(dependencies, func);
10101013
}
10111014

10121015
op(_CHECK_METHOD_VERSION, (func_version/2, callable, null, unused[oparg] -- callable, null, unused[oparg])) {
@@ -2229,7 +2232,8 @@ dummy_func(void) {
22292232
if (co->co_version == version) {
22302233
_Py_BloomFilter_Add(dependencies, co);
22312234
// Functions derive their version from code objects.
2232-
if (sym_get_func_version(ctx->frame->callable) == version) {
2235+
PyFunctionObject *func = (PyFunctionObject *)sym_get_const(ctx, ctx->frame->callable);
2236+
if (func != NULL && func->func_version == version) {
22332237
REPLACE_OP(this_instr, _NOP, 0, 0);
22342238
}
22352239
}
@@ -2262,7 +2266,8 @@ dummy_func(void) {
22622266
op(_GUARD_IP__PUSH_FRAME, (ip/4 --)) {
22632267
(void)ip;
22642268
stack_pointer = sym_set_stack_depth((int)this_instr->operand1, stack_pointer);
2265-
if (sym_get_func_version(ctx->frame->callable) != 0 &&
2269+
PyFunctionObject *func = (PyFunctionObject *)sym_get_const(ctx, ctx->frame->callable);
2270+
if (func != NULL && func->func_version != 0 &&
22662271
// We can remove this guard for simple function call targets.
22672272
(((PyCodeObject *)ctx->frame->func->func_code)->co_flags &
22682273
(CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) == 0) {

Python/optimizer_cases.c.h

Lines changed: 11 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)