GH-91049: Introduce set vectorcall field API for PyFunctionObject (GH-92257)

Co-authored-by: Andrew Frost <adfrost@fb.com>
Co-authored-by: Itamar Ostricher <itamarost@gmail.com>
This commit is contained in:
adphrost 2022-09-15 08:42:37 -07:00 committed by GitHub
parent e37ac5fbb6
commit a41ed975e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 115 additions and 3 deletions

View file

@ -83,6 +83,15 @@ There are a few functions specific to Python functions.
Raises :exc:`SystemError` and returns ``-1`` on failure.
.. c:function:: void PyFunction_SetVectorcall(PyFunctionObject *func, vectorcallfunc vectorcall)
Set the vectorcall field of a given function object *func*.
Warning: extensions using this API must preserve the behavior
of the unaltered (default) vectorcall function!
.. versionadded:: 3.12
.. c:function:: PyObject* PyFunction_GetClosure(PyObject *op)
Return the closure associated with the function object *op*. This can be ``NULL``

View file

@ -493,6 +493,10 @@ New Features
functions in all running threads in addition to the calling one. (Contributed
by Pablo Galindo in :gh:`93503`.)
* Added new function :c:func:`PyFunction_SetVectorcall` to the C API
which sets the vectorcall field of a given :c:type:`PyFunctionObject`.
(Contributed by Andrew Frost in :gh:`92257`.)
Porting to Python 3.12
----------------------

View file

@ -48,7 +48,8 @@ typedef struct {
* defaults
* kwdefaults (only if the object changes, not the contents of the dict)
* code
* annotations */
* annotations
* vectorcall function pointer */
uint32_t func_version;
/* Invariant:
@ -69,6 +70,7 @@ PyAPI_FUNC(PyObject *) PyFunction_GetGlobals(PyObject *);
PyAPI_FUNC(PyObject *) PyFunction_GetModule(PyObject *);
PyAPI_FUNC(PyObject *) PyFunction_GetDefaults(PyObject *);
PyAPI_FUNC(int) PyFunction_SetDefaults(PyObject *, PyObject *);
PyAPI_FUNC(void) PyFunction_SetVectorcall(PyFunctionObject *, vectorcallfunc);
PyAPI_FUNC(PyObject *) PyFunction_GetKwDefaults(PyObject *);
PyAPI_FUNC(int) PyFunction_SetKwDefaults(PyObject *, PyObject *);
PyAPI_FUNC(PyObject *) PyFunction_GetClosure(PyObject *);

View file

@ -580,6 +580,9 @@ def testfunction_kw(self, *, kw):
return self
QUICKENING_WARMUP_DELAY = 8
class TestPEP590(unittest.TestCase):
def test_method_descriptor_flag(self):
@ -760,6 +763,54 @@ class TestPEP590(unittest.TestCase):
self.assertEqual(expected, meth(*args1, **kwargs))
self.assertEqual(expected, wrapped(*args, **kwargs))
def test_setvectorcall(self):
from _testcapi import function_setvectorcall
def f(num): return num + 1
assert_equal = self.assertEqual
num = 10
assert_equal(11, f(num))
function_setvectorcall(f)
# make sure specializer is triggered by running > 50 times
for _ in range(10 * QUICKENING_WARMUP_DELAY):
assert_equal("overridden", f(num))
def test_setvectorcall_load_attr_specialization_skip(self):
from _testcapi import function_setvectorcall
class X:
def __getattribute__(self, attr):
return attr
assert_equal = self.assertEqual
x = X()
assert_equal("a", x.a)
function_setvectorcall(X.__getattribute__)
# make sure specialization doesn't trigger
# when vectorcall is overridden
for _ in range(QUICKENING_WARMUP_DELAY):
assert_equal("overridden", x.a)
def test_setvectorcall_load_attr_specialization_deopt(self):
from _testcapi import function_setvectorcall
class X:
def __getattribute__(self, attr):
return attr
def get_a(x):
return x.a
assert_equal = self.assertEqual
x = X()
# trigger LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN specialization
for _ in range(QUICKENING_WARMUP_DELAY):
assert_equal("a", get_a(x))
function_setvectorcall(X.__getattribute__)
# make sure specialized LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN
# gets deopted due to overridden vectorcall
for _ in range(QUICKENING_WARMUP_DELAY):
assert_equal("overridden", get_a(x))
@requires_limited_api
def test_vectorcall_limited(self):
from _testcapi import pyobject_vectorcall

View file

@ -0,0 +1,5 @@
Add new function :c:func:`PyFunction_SetVectorcall` to the C API
which sets the vectorcall field of a given :c:type:`PyFunctionObject`.
Warning: extensions using this API must preserve the behavior
of the unaltered function!

View file

@ -102,6 +102,24 @@ test_pyobject_vectorcall(PyObject *self, PyObject *args)
return PyObject_Vectorcall(func, stack, nargs, kwnames);
}
static PyObject *
override_vectorcall(PyObject *callable, PyObject *const *args, size_t nargsf,
PyObject *kwnames)
{
return PyUnicode_FromString("overridden");
}
static PyObject *
function_setvectorcall(PyObject *self, PyObject *func)
{
if (!PyFunction_Check(func)) {
PyErr_SetString(PyExc_TypeError, "'func' must be a function");
return NULL;
}
PyFunction_SetVectorcall((PyFunctionObject *)func, (vectorcallfunc)override_vectorcall);
Py_RETURN_NONE;
}
static PyObject *
test_pyvectorcall_call(PyObject *self, PyObject *args)
{
@ -244,6 +262,7 @@ static PyMethodDef TestMethods[] = {
{"pyobject_fastcall", test_pyobject_fastcall, METH_VARARGS},
{"pyobject_fastcalldict", test_pyobject_fastcalldict, METH_VARARGS},
{"pyobject_vectorcall", test_pyobject_vectorcall, METH_VARARGS},
{"function_setvectorcall", function_setvectorcall, METH_O},
{"pyvectorcall_call", test_pyvectorcall_call, METH_VARARGS},
_TESTCAPI_MAKE_VECTORCALL_CLASS_METHODDEF
_TESTCAPI_HAS_VECTORCALL_FLAG_METHODDEF

View file

@ -134,6 +134,9 @@ uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func)
if (func->func_version != 0) {
return func->func_version;
}
if (func->vectorcall != _PyFunction_Vectorcall) {
return 0;
}
if (next_func_version == 0) {
return 0;
}
@ -209,6 +212,14 @@ PyFunction_SetDefaults(PyObject *op, PyObject *defaults)
return 0;
}
void
PyFunction_SetVectorcall(PyFunctionObject *func, vectorcallfunc vectorcall)
{
assert(func != NULL);
func->func_version = 0;
func->vectorcall = vectorcall;
}
PyObject *
PyFunction_GetKwDefaults(PyObject *op)
{

View file

@ -3126,8 +3126,11 @@ handle_eval_breaker:
PyObject *getattribute = read_obj(cache->descr);
assert(Py_IS_TYPE(getattribute, &PyFunction_Type));
PyFunctionObject *f = (PyFunctionObject *)getattribute;
uint32_t func_version = read_u32(cache->keys_version);
assert(func_version != 0);
DEOPT_IF(f->func_version != func_version, LOAD_ATTR);
PyCodeObject *code = (PyCodeObject *)f->func_code;
DEOPT_IF(((PyCodeObject *)f->func_code)->co_argcount != 2, LOAD_ATTR);
assert(code->co_argcount == 2);
DEOPT_IF(!_PyThreadState_HasStackSpace(tstate, code->co_framesize), CALL);
STAT_INC(LOAD_ATTR, hit);
@ -4133,7 +4136,10 @@ handle_eval_breaker:
function = PEEK(total_args + 1);
int positional_args = total_args - KWNAMES_LEN();
// Check if the call can be inlined or not
if (Py_TYPE(function) == &PyFunction_Type && tstate->interp->eval_frame == NULL) {
if (Py_TYPE(function) == &PyFunction_Type &&
tstate->interp->eval_frame == NULL &&
((PyFunctionObject *)function)->vectorcall == _PyFunction_Vectorcall)
{
int code_flags = ((PyCodeObject*)PyFunction_GET_CODE(function))->co_flags;
PyObject *locals = code_flags & CO_OPTIMIZED ? NULL : Py_NewRef(PyFunction_GET_GLOBALS(function));
STACK_SHRINK(total_args);

View file

@ -837,6 +837,11 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
if (!function_check_args(descr, 2, LOAD_ATTR)) {
goto fail;
}
uint32_t version = function_get_version(descr, LOAD_ATTR);
if (version == 0) {
goto fail;
}
write_u32(lm_cache->keys_version, version);
/* borrowed */
write_obj(lm_cache->descr, descr);
write_u32(lm_cache->type_version, type->tp_version_tag);