Visitar URL original
gh-157833: Fix specialized C calls with additional method flags (GH-1… · python/cpython@069c74a · GitHub
Skip to content

Commit 069c74a

Browse files
authored
gh-157833: Fix specialized C calls with additional method flags (GH-157834)
1 parent 83de667 commit 069c74a

15 files changed

Lines changed: 383 additions & 60 deletions

File tree

‎Include/internal/pycore_call.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ extern "C" {
1212
#include "pycore_pystate.h" // _PyThreadState_GET()
1313
#include "pycore_stats.h"
1414

15+
/* Flags that determine the C calling convention. */
16+
#define _Py_METH_CALL_FLAGS \
17+
(METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | \
18+
METH_KEYWORDS | METH_METHOD)
19+
1520
/* Suggested size (number of positional arguments) for arrays of PyObject*
1621
allocated on a C stack to avoid allocating memory on the heap memory. Such
1722
array is used to pass positional arguments to call functions of the

‎Lib/test/test_capi/test_opt.py‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3349,6 +3349,186 @@ def testfunc(n):
33493349
self.assertIn("_CALL_BUILTIN_FAST_WITH_KEYWORDS", uops)
33503350
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS", uops)
33513351

3352+
def test_call_builtin_o_extra_flags(self):
3353+
# Extra method flags must not prevent callable guard elimination.
3354+
_testcapi = import_helper.import_module("_testcapi")
3355+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3356+
3357+
namespace = {
3358+
"METH_CLASS_O": _testcapi.MethClass.meth_o,
3359+
"METH_STATIC_O": _testcapi.MethStatic.meth_o,
3360+
"METH_COEXIST_O": {}.__contains__,
3361+
}
3362+
3363+
@reset_code
3364+
def testfunc(n):
3365+
for _ in range(n):
3366+
class_result = METH_CLASS_O(1)
3367+
static_result = METH_STATIC_O(1)
3368+
coexist_result = METH_COEXIST_O(1)
3369+
return class_result, static_result, coexist_result
3370+
3371+
testfunc = types.FunctionType(testfunc.__code__, namespace)
3372+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3373+
self.assertEqual(res, ((_testcapi.MethClass, 1), (None, 1), False))
3374+
self.assertIsNotNone(ex)
3375+
uops = get_opnames(ex)
3376+
self.assertEqual(uops.count("_CALL_BUILTIN_O"), 3)
3377+
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_O", uops)
3378+
3379+
def test_call_builtin_fast_extra_flags(self):
3380+
_testcapi = import_helper.import_module("_testcapi")
3381+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3382+
3383+
obj = _testcapi.MethInstance()
3384+
namespace = {
3385+
"METH_CLASS_FASTCALL": _testcapi.MethClass.meth_fastcall,
3386+
"METH_STATIC_FASTCALL": _testcapi.MethStatic.meth_fastcall,
3387+
"METH_COEXIST_FASTCALL": obj.meth_fastcall_coexist,
3388+
}
3389+
3390+
@reset_code
3391+
def testfunc(n):
3392+
for _ in range(n):
3393+
class_result = METH_CLASS_FASTCALL(1, 2)
3394+
static_result = METH_STATIC_FASTCALL(1, 2)
3395+
coexist_result = METH_COEXIST_FASTCALL(1, 2)
3396+
return class_result, static_result, coexist_result
3397+
3398+
testfunc = types.FunctionType(testfunc.__code__, namespace)
3399+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3400+
self.assertEqual(res, (
3401+
(_testcapi.MethClass, (1, 2)),
3402+
(None, (1, 2)),
3403+
(obj, (1, 2)),
3404+
))
3405+
self.assertIsNotNone(ex)
3406+
uops = get_opnames(ex)
3407+
self.assertEqual(uops.count("_CALL_BUILTIN_FAST"), 3)
3408+
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_FAST", uops)
3409+
3410+
def test_call_builtin_fast_with_keywords_extra_flags(self):
3411+
_testcapi = import_helper.import_module("_testcapi")
3412+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3413+
3414+
obj = _testcapi.MethInstance()
3415+
namespace = {
3416+
"METH_CLASS_FASTCALL_KEYWORDS": (
3417+
_testcapi.MethClass.meth_fastcall_keywords),
3418+
"METH_STATIC_FASTCALL_KEYWORDS": (
3419+
_testcapi.MethStatic.meth_fastcall_keywords),
3420+
"METH_COEXIST_FASTCALL_KEYWORDS": (
3421+
obj.meth_fastcall_keywords_coexist),
3422+
}
3423+
3424+
@reset_code
3425+
def testfunc(n):
3426+
# Use positional arguments to exercise CALL, not CALL_KW.
3427+
for _ in range(n):
3428+
class_result = METH_CLASS_FASTCALL_KEYWORDS(1, 2)
3429+
static_result = METH_STATIC_FASTCALL_KEYWORDS(1, 2)
3430+
coexist_result = METH_COEXIST_FASTCALL_KEYWORDS(1, 2)
3431+
return class_result, static_result, coexist_result
3432+
3433+
testfunc = types.FunctionType(testfunc.__code__, namespace)
3434+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3435+
self.assertEqual(res, (
3436+
(_testcapi.MethClass, (1, 2), {}),
3437+
(None, (1, 2), {}),
3438+
(obj, (1, 2), {}),
3439+
))
3440+
self.assertIsNotNone(ex)
3441+
uops = get_opnames(ex)
3442+
self.assertEqual(uops.count("_CALL_BUILTIN_FAST_WITH_KEYWORDS"), 3)
3443+
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS", uops)
3444+
3445+
def test_call_method_descriptor_o_extra_flags(self):
3446+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3447+
3448+
@reset_code
3449+
def testfunc(n):
3450+
d = {1: None}
3451+
for _ in range(n):
3452+
result = d.__contains__(1)
3453+
return result
3454+
3455+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3456+
self.assertIs(res, True)
3457+
self.assertIsNotNone(ex)
3458+
uops = get_opnames(ex)
3459+
self.assertEqual(uops.count("_CALL_METHOD_DESCRIPTOR_O_INLINE"), 1)
3460+
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_O", uops)
3461+
3462+
def test_call_method_descriptor_noargs_extra_flags(self):
3463+
_testcapi = import_helper.import_module("_testcapi")
3464+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3465+
3466+
namespace = {
3467+
"METH_COEXIST_NOARGS_OBJECT": _testcapi.DocStringNoSignatureTest(),
3468+
}
3469+
3470+
@reset_code
3471+
def testfunc(n):
3472+
for _ in range(n):
3473+
result = METH_COEXIST_NOARGS_OBJECT.meth_noargs_coexist()
3474+
return result
3475+
3476+
testfunc = types.FunctionType(testfunc.__code__, namespace)
3477+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3478+
self.assertIsNone(res)
3479+
self.assertIsNotNone(ex)
3480+
uops = get_opnames(ex)
3481+
self.assertEqual(
3482+
uops.count("_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE"), 1)
3483+
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS", uops)
3484+
3485+
def test_call_method_descriptor_fast_extra_flags(self):
3486+
_testcapi = import_helper.import_module("_testcapi")
3487+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3488+
3489+
obj = _testcapi.MethInstance()
3490+
namespace = {"METH_COEXIST_FAST_OBJECT": obj}
3491+
3492+
@reset_code
3493+
def testfunc(n):
3494+
for _ in range(n):
3495+
result = METH_COEXIST_FAST_OBJECT.meth_fastcall_coexist(1, 2)
3496+
return result
3497+
3498+
testfunc = types.FunctionType(testfunc.__code__, namespace)
3499+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3500+
self.assertEqual(res, (obj, (1, 2)))
3501+
self.assertIsNotNone(ex)
3502+
uops = get_opnames(ex)
3503+
self.assertEqual(uops.count("_CALL_METHOD_DESCRIPTOR_FAST_INLINE"), 1)
3504+
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST", uops)
3505+
3506+
def test_call_method_descriptor_fast_with_keywords_extra_flags(self):
3507+
_testcapi = import_helper.import_module("_testcapi")
3508+
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)
3509+
3510+
obj = _testcapi.MethInstance()
3511+
namespace = {"METH_COEXIST_FAST_OBJECT": obj}
3512+
3513+
@reset_code
3514+
def testfunc(n):
3515+
# Use positional arguments to exercise CALL, not CALL_KW.
3516+
for _ in range(n):
3517+
result = (
3518+
METH_COEXIST_FAST_OBJECT.meth_fastcall_keywords_coexist(
3519+
1, 2))
3520+
return result
3521+
3522+
testfunc = types.FunctionType(testfunc.__code__, namespace)
3523+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3524+
self.assertEqual(res, (obj, (1, 2), {}))
3525+
self.assertIsNotNone(ex)
3526+
uops = get_opnames(ex)
3527+
self.assertEqual(
3528+
uops.count("_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE"), 1)
3529+
self.assertNotIn(
3530+
"_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS", uops)
3531+
33523532
def test_call_method_descriptor_o(self):
33533533
def testfunc(n):
33543534
x = 0

‎Lib/test/test_opcache.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,6 +2171,89 @@ class MyList(list): pass
21712171
self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND")
21722172
self.assert_no_opcode(my_list_append, "CALL")
21732173

2174+
@cpython_only
2175+
@requires_specialization
2176+
def test_call_c_function_extra_flags(self):
2177+
# METH_CLASS, METH_STATIC and METH_COEXIST do not change the C
2178+
# calling convention, so the specialized instructions must not
2179+
# miss because of them.
2180+
_testcapi = import_module("_testcapi")
2181+
2182+
def call_1(func, arg):
2183+
return func(arg)
2184+
2185+
def call_2(func, arg1, arg2):
2186+
return func(arg1, arg2)
2187+
2188+
def call_3(func, arg1, arg2, arg3):
2189+
return func(arg1, arg2, arg3)
2190+
2191+
def call_method(obj, arg):
2192+
return obj.__contains__(arg)
2193+
2194+
def call_method_noargs(obj):
2195+
return obj.meth_noargs_coexist()
2196+
2197+
def call_method_fast(obj, arg1, arg2):
2198+
return obj.meth_fastcall_coexist(arg1, arg2)
2199+
2200+
def call_method_fast_with_keywords(obj, arg1, arg2):
2201+
return obj.meth_fastcall_keywords_coexist(arg1, arg2)
2202+
2203+
def call_site(f):
2204+
[call] = [instr for instr in dis.get_instructions(f, adaptive=True)
2205+
if instr.baseopname == "CALL"]
2206+
cache = {name: data for name, _, data in call.cache_info}
2207+
return call.opname, cache["counter"]
2208+
2209+
def label(obj):
2210+
return getattr(obj, "__qualname__", type(obj).__name__)
2211+
2212+
coexist = _testcapi.MethInstance()
2213+
cases = [
2214+
# dict.__contains__ has METH_O | METH_COEXIST
2215+
(call_1, {}.__contains__, ("key",), "CALL_BUILTIN_O"),
2216+
(call_method, {}, ("key",), "CALL_METHOD_DESCRIPTOR_O"),
2217+
# meth_noargs_coexist has METH_NOARGS | METH_COEXIST
2218+
(call_method_noargs, _testcapi.DocStringNoSignatureTest(), (),
2219+
"CALL_METHOD_DESCRIPTOR_NOARGS"),
2220+
# METH_FASTCALL, with or without METH_KEYWORDS, and METH_COEXIST
2221+
(call_method_fast, coexist, (1, 2),
2222+
"CALL_METHOD_DESCRIPTOR_FAST"),
2223+
(call_method_fast_with_keywords, coexist, (1, 2),
2224+
"CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS"),
2225+
(call_3, _testcapi.MethInstance.meth_fastcall_coexist,
2226+
(coexist, 1, 2), "CALL_METHOD_DESCRIPTOR_FAST"),
2227+
(call_3, _testcapi.MethInstance.meth_fastcall_keywords_coexist,
2228+
(coexist, 1, 2), "CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS"),
2229+
(call_2, coexist.meth_fastcall_coexist, (1, 2),
2230+
"CALL_BUILTIN_FAST"),
2231+
(call_2, coexist.meth_fastcall_keywords_coexist, (1, 2),
2232+
"CALL_BUILTIN_FAST_WITH_KEYWORDS"),
2233+
]
2234+
for owner in (_testcapi.MethClass, _testcapi.MethStatic):
2235+
cases += [
2236+
(call_1, owner.meth_o, (1,), "CALL_BUILTIN_O"),
2237+
(call_2, owner.meth_fastcall, (1, 2), "CALL_BUILTIN_FAST"),
2238+
(call_2, owner.meth_fastcall_keywords, (1, 2),
2239+
"CALL_BUILTIN_FAST_WITH_KEYWORDS"),
2240+
]
2241+
2242+
for f, func, args, opname in cases:
2243+
with self.subTest(call=f.__name__, func=label(func)):
2244+
reset_code(f)
2245+
expected = f(func, *args)
2246+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
2247+
f(func, *args)
2248+
self.assertEqual(call_site(f)[0], opname)
2249+
2250+
# A hit leaves the counter of the call site unchanged.
2251+
# A miss decrements it.
2252+
before = call_site(f)
2253+
for _ in range(10):
2254+
self.assertEqual(f(func, *args), expected)
2255+
self.assertEqual(call_site(f), before)
2256+
21742257
@cpython_only
21752258
@requires_specialization
21762259
def test_load_attr_module_with_getattr(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix call specialization for C functions and methods with ``METH_CLASS``,
2+
``METH_STATIC``, or ``METH_COEXIST`` flags, improving their call performance.

‎Modules/_testcapimodule.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3360,6 +3360,11 @@ static PyMethodDef meth_instance_methods[] = {
33603360
{"meth_noargs", meth_noargs, METH_NOARGS},
33613361
{"meth_fastcall", _PyCFunction_CAST(meth_fastcall), METH_FASTCALL},
33623362
{"meth_fastcall_keywords", _PyCFunction_CAST(meth_fastcall_keywords), METH_FASTCALL|METH_KEYWORDS},
3363+
{"meth_fastcall_coexist", _PyCFunction_CAST(meth_fastcall),
3364+
METH_FASTCALL|METH_COEXIST},
3365+
{"meth_fastcall_keywords_coexist",
3366+
_PyCFunction_CAST(meth_fastcall_keywords),
3367+
METH_FASTCALL|METH_KEYWORDS|METH_COEXIST},
33633368
{NULL, NULL} /* sentinel */
33643369
};
33653370

‎Modules/_testinternalcapi/test_cases.c.h‎

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

‎Objects/descrobject.c‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -938,8 +938,7 @@ PyDescr_NewMethod(PyTypeObject *type, PyMethodDef *method)
938938
{
939939
/* Figure out correct vectorcall function to use */
940940
vectorcallfunc vectorcall;
941-
switch (method->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
942-
METH_O | METH_KEYWORDS | METH_METHOD))
941+
switch (method->ml_flags & _Py_METH_CALL_FLAGS)
943942
{
944943
case METH_VARARGS:
945944
vectorcall = method_vectorcall_VARARGS;

‎Objects/methodobject.c‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ PyCMethod_New(PyMethodDef *ml, PyObject *self, PyObject *module, PyTypeObject *c
4848
{
4949
/* Figure out correct vectorcall function to use */
5050
vectorcallfunc vectorcall;
51-
switch (ml->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
52-
METH_O | METH_KEYWORDS | METH_METHOD))
51+
switch (ml->ml_flags & _Py_METH_CALL_FLAGS)
5352
{
5453
case METH_VARARGS:
5554
case METH_VARARGS | METH_KEYWORDS:

0 commit comments

Comments
 (0)