Skip to content

Commit 61c7249

Browse files
authored
gh-106581: Project through calls (#108067)
This finishes the work begun in gh-107760. When, while projecting a superblock, we encounter a call to a short, simple function, the superblock will now enter the function using `_PUSH_FRAME`, continue through it, and leave it using `_POP_FRAME`, and then continue through the original code. Multiple frame pushes and pops are even possible. It is also possible to stop appending to the superblock in the middle of a called function, when running out of space or encountering an unsupported bytecode.
1 parent 292a22b commit 61c7249

16 files changed

+409
-109
lines changed

Diff for: Include/internal/pycore_ceval.h

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ void _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *
171171
PyObject *_PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, Py_ssize_t nargs, PyObject *kwargs);
172172
PyObject *_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys);
173173
int _PyEval_UnpackIterable(PyThreadState *tstate, PyObject *v, int argcnt, int argcntafter, PyObject **sp);
174+
void _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame);
174175

175176

176177
#ifdef __cplusplus

Diff for: Include/internal/pycore_function.h

+9
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,22 @@ extern PyObject* _PyFunction_Vectorcall(
1616

1717
#define FUNC_MAX_WATCHERS 8
1818

19+
#define FUNC_VERSION_CACHE_SIZE (1<<12) /* Must be a power of 2 */
1920
struct _py_func_state {
2021
uint32_t next_version;
22+
// Borrowed references to function objects whose
23+
// func_version % FUNC_VERSION_CACHE_SIZE
24+
// once was equal to the index in the table.
25+
// They are cleared when the function is deallocated.
26+
PyFunctionObject *func_version_cache[FUNC_VERSION_CACHE_SIZE];
2127
};
2228

2329
extern PyFunctionObject* _PyFunction_FromConstructor(PyFrameConstructor *constr);
2430

2531
extern uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func);
32+
extern void _PyFunction_SetVersion(PyFunctionObject *func, uint32_t version);
33+
PyFunctionObject *_PyFunction_LookupByVersion(uint32_t version);
34+
2635
extern PyObject *_Py_set_function_type_params(
2736
PyThreadState* unused, PyObject *func, PyObject *type_params);
2837

Diff for: Include/internal/pycore_opcode_metadata.h

+48-39
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Lib/test/test_capi/test_misc.py

+1
Original file line numberDiff line numberDiff line change
@@ -2633,6 +2633,7 @@ def dummy(x):
26332633
self.assertIsNotNone(ex)
26342634
uops = {opname for opname, _, _ in ex}
26352635
self.assertIn("_PUSH_FRAME", uops)
2636+
self.assertIn("_BINARY_OP_ADD_INT", uops)
26362637

26372638

26382639

Diff for: Lib/test/test_code.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def func2():
264264
("co_posonlyargcount", 0),
265265
("co_kwonlyargcount", 0),
266266
("co_nlocals", 1),
267-
("co_stacksize", 0),
267+
("co_stacksize", 1),
268268
("co_flags", code.co_flags | inspect.CO_COROUTINE),
269269
("co_firstlineno", 100),
270270
("co_code", code2.co_code),

Diff for: Objects/codeobject.c

+3
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,9 @@ init_code(PyCodeObject *co, struct _PyCodeConstructor *con)
396396
int nlocals, ncellvars, nfreevars;
397397
get_localsplus_counts(con->localsplusnames, con->localspluskinds,
398398
&nlocals, &ncellvars, &nfreevars);
399+
if (con->stacksize == 0) {
400+
con->stacksize = 1;
401+
}
399402

400403
co->co_filename = Py_NewRef(con->filename);
401404
co->co_name = Py_NewRef(con->name);

Diff for: Objects/funcobject.c

+77-2
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,73 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
223223
return NULL;
224224
}
225225

226-
uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func)
226+
/*
227+
Function versions
228+
-----------------
229+
230+
Function versions are used to detect when a function object has been
231+
updated, invalidating inline cache data used by the `CALL` bytecode
232+
(notably `CALL_PY_EXACT_ARGS` and a few other `CALL` specializations).
233+
234+
They are also used by the Tier 2 superblock creation code to find
235+
the function being called (and from there the code object).
236+
237+
How does a function's `func_version` field get initialized?
238+
239+
- `PyFunction_New` and friends initialize it to 0.
240+
- The `MAKE_FUNCTION` instruction sets it from the code's `co_version`.
241+
- It is reset to 0 when various attributes like `__code__` are set.
242+
- A new version is allocated by `_PyFunction_GetVersionForCurrentState`
243+
when the specializer needs a version and the version is 0.
244+
245+
The latter allocates versions using a counter in the interpreter state;
246+
when the counter wraps around to 0, no more versions are allocated.
247+
There is one other special case: functions with a non-standard
248+
`vectorcall` field are not given a version.
249+
250+
When the function version is 0, the `CALL` bytecode is not specialized.
251+
252+
Code object versions
253+
--------------------
254+
255+
So where to code objects get their `co_version`? There is a single
256+
static global counter, `_Py_next_func_version`. This is initialized in
257+
the generated (!) file `Python/deepfreeze/deepfreeze.c`, to 1 plus the
258+
number of deep-frozen function objects in that file.
259+
(In `_bootstrap_python.c` and `freeze_module.c` it is initialized to 1.)
260+
261+
Code objects get a new `co_version` allocated from this counter upon
262+
creation. Since code objects are nominally immutable, `co_version` can
263+
not be invalidated. The only way it can be 0 is when 2**32 or more
264+
code objects have been created during the process's lifetime.
265+
(The counter isn't reset by `fork()`, extending the lifetime.)
266+
*/
267+
268+
void
269+
_PyFunction_SetVersion(PyFunctionObject *func, uint32_t version)
270+
{
271+
func->func_version = version;
272+
if (version != 0) {
273+
PyInterpreterState *interp = _PyInterpreterState_GET();
274+
interp->func_state.func_version_cache[
275+
version % FUNC_VERSION_CACHE_SIZE] = func;
276+
}
277+
}
278+
279+
PyFunctionObject *
280+
_PyFunction_LookupByVersion(uint32_t version)
281+
{
282+
PyInterpreterState *interp = _PyInterpreterState_GET();
283+
PyFunctionObject *func = interp->func_state.func_version_cache[
284+
version % FUNC_VERSION_CACHE_SIZE];
285+
if (func != NULL && func->func_version == version) {
286+
return (PyFunctionObject *)Py_NewRef(func);
287+
}
288+
return NULL;
289+
}
290+
291+
uint32_t
292+
_PyFunction_GetVersionForCurrentState(PyFunctionObject *func)
227293
{
228294
if (func->func_version != 0) {
229295
return func->func_version;
@@ -236,7 +302,7 @@ uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func)
236302
return 0;
237303
}
238304
uint32_t v = interp->func_state.next_version++;
239-
func->func_version = v;
305+
_PyFunction_SetVersion(func, v);
240306
return v;
241307
}
242308

@@ -851,6 +917,15 @@ func_dealloc(PyFunctionObject *op)
851917
if (op->func_weakreflist != NULL) {
852918
PyObject_ClearWeakRefs((PyObject *) op);
853919
}
920+
if (op->func_version != 0) {
921+
PyInterpreterState *interp = _PyInterpreterState_GET();
922+
PyFunctionObject **slot =
923+
interp->func_state.func_version_cache
924+
+ (op->func_version % FUNC_VERSION_CACHE_SIZE);
925+
if (*slot == op) {
926+
*slot = NULL;
927+
}
928+
}
854929
(void)func_clear(op);
855930
// These aren't cleared by func_clear().
856931
Py_DECREF(op->func_code);

Diff for: Python/abstract_interp_cases.c.h

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)