gh-117045: Add code object to function version cache (#117028)

Changes to the function version cache:

- In addition to the function object, also store the code object,
  and allow the latter to be retrieved even if the function has been evicted.
- Stop assigning new function versions after a critical attribute (e.g. `__code__`)
  has been modified; the version is permanently reset to zero in this case.
- Changes to `__annotations__` are no longer considered critical. (This fixes gh-109998.)

Changes to the Tier 2 optimization machinery:

- If we cannot map a function version to a function, but it is still mapped to a code object,
  we continue projecting the trace.
  The operand of the `_PUSH_FRAME` and `_POP_FRAME` opcodes can be either NULL,
  a function object, or a code object with the lowest bit set.

This allows us to trace through code that calls an ephemeral function,
i.e., a function that may not be alive when we are constructing the executor,
e.g. a generator expression or certain nested functions.
We will lose globals removal inside such functions,
but we can still do other peephole operations
(and even possibly [call inlining](https://github.com/python/cpython/pull/116290),
if we decide to do it), which only need the code object.
As before, if we cannot retrieve the code object from the cache, we stop projecting.
This commit is contained in:
Guido van Rossum 2024-03-21 12:37:41 -07:00 committed by GitHub
parent c85d84166a
commit 570a82d46a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 208 additions and 95 deletions

View file

@ -228,7 +228,12 @@ remove_globals(_PyInterpreterFrame *frame, _PyUOpInstruction *buffer,
builtins_watched <<= 1;
globals_watched <<= 1;
function_checked <<= 1;
PyFunctionObject *func = (PyFunctionObject *)buffer[pc].operand;
uint64_t operand = buffer[pc].operand;
if (operand == 0 || (operand & 1)) {
// It's either a code object or NULL, so bail
return 1;
}
PyFunctionObject *func = (PyFunctionObject *)operand;
if (func == NULL) {
return 1;
}
@ -251,7 +256,15 @@ remove_globals(_PyInterpreterFrame *frame, _PyUOpInstruction *buffer,
builtins_watched >>= 1;
globals_watched >>= 1;
function_checked >>= 1;
PyFunctionObject *func = (PyFunctionObject *)buffer[pc].operand;
uint64_t operand = buffer[pc].operand;
if (operand == 0 || (operand & 1)) {
// It's either a code object or NULL, so bail
return 1;
}
PyFunctionObject *func = (PyFunctionObject *)operand;
if (func == NULL) {
return 1;
}
assert(PyFunction_Check(func));
function_version = func->func_version;
globals = func->func_globals;
@ -522,7 +535,7 @@ remove_unneeded_uops(_PyUOpInstruction *buffer, int buffer_size)
static void
peephole_opt(_PyInterpreterFrame *frame, _PyUOpInstruction *buffer, int buffer_size)
{
PyCodeObject *co = (PyCodeObject *)frame->f_executable;
PyCodeObject *co = _PyFrame_GetCode(frame);
for (int pc = 0; pc < buffer_size; pc++) {
int opcode = buffer[pc].opcode;
switch(opcode) {
@ -545,11 +558,16 @@ peephole_opt(_PyInterpreterFrame *frame, _PyUOpInstruction *buffer, int buffer_s
case _PUSH_FRAME:
case _POP_FRAME:
{
PyFunctionObject *func = (PyFunctionObject *)buffer[pc].operand;
if (func == NULL) {
uint64_t operand = buffer[pc].operand;
if (operand & 1) {
co = (PyCodeObject *)(operand & ~1);
assert(PyCode_Check(co));
}
else if (operand == 0) {
co = NULL;
}
else {
PyFunctionObject *func = (PyFunctionObject *)operand;
assert(PyFunction_Check(func));
co = (PyCodeObject *)func->func_code;
}
@ -587,7 +605,7 @@ _Py_uop_analyze_and_optimize(
peephole_opt(frame, buffer, buffer_size);
err = optimize_uops(
(PyCodeObject *)frame->f_executable, buffer,
_PyFrame_GetCode(frame), buffer,
buffer_size, curr_stacklen, dependencies);
if (err == 0) {