Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Include/internal/pycore_call.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ extern "C" {
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_stats.h"

/* Flags that determine the C calling convention. */
#define _Py_METH_CALL_FLAGS \
(METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | \
METH_KEYWORDS | METH_METHOD)

/* Suggested size (number of positional arguments) for arrays of PyObject*
allocated on a C stack to avoid allocating memory on the heap memory. Such
array is used to pass positional arguments to call functions of the
Expand Down
180 changes: 180 additions & 0 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3307,6 +3307,186 @@ def testfunc(n):
self.assertIn("_CALL_BUILTIN_FAST_WITH_KEYWORDS", uops)
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS", uops)

def test_call_builtin_o_extra_flags(self):
# Extra method flags must not prevent callable guard elimination.
_testcapi = import_helper.import_module("_testcapi")
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

namespace = {
"METH_CLASS_O": _testcapi.MethClass.meth_o,
"METH_STATIC_O": _testcapi.MethStatic.meth_o,
"METH_COEXIST_O": {}.__contains__,
}

@reset_code
def testfunc(n):
for _ in range(n):
class_result = METH_CLASS_O(1)
static_result = METH_STATIC_O(1)
coexist_result = METH_COEXIST_O(1)
return class_result, static_result, coexist_result

testfunc = types.FunctionType(testfunc.__code__, namespace)
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertEqual(res, ((_testcapi.MethClass, 1), (None, 1), False))
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(uops.count("_CALL_BUILTIN_O"), 3)
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_O", uops)

def test_call_builtin_fast_extra_flags(self):
_testcapi = import_helper.import_module("_testcapi")
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

obj = _testcapi.MethInstance()
namespace = {
"METH_CLASS_FASTCALL": _testcapi.MethClass.meth_fastcall,
"METH_STATIC_FASTCALL": _testcapi.MethStatic.meth_fastcall,
"METH_COEXIST_FASTCALL": obj.meth_fastcall_coexist,
}

@reset_code
def testfunc(n):
for _ in range(n):
class_result = METH_CLASS_FASTCALL(1, 2)
static_result = METH_STATIC_FASTCALL(1, 2)
coexist_result = METH_COEXIST_FASTCALL(1, 2)
return class_result, static_result, coexist_result

testfunc = types.FunctionType(testfunc.__code__, namespace)
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertEqual(res, (
(_testcapi.MethClass, (1, 2)),
(None, (1, 2)),
(obj, (1, 2)),
))
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(uops.count("_CALL_BUILTIN_FAST"), 3)
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_FAST", uops)

def test_call_builtin_fast_with_keywords_extra_flags(self):
_testcapi = import_helper.import_module("_testcapi")
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

obj = _testcapi.MethInstance()
namespace = {
"METH_CLASS_FASTCALL_KEYWORDS": (
_testcapi.MethClass.meth_fastcall_keywords),
"METH_STATIC_FASTCALL_KEYWORDS": (
_testcapi.MethStatic.meth_fastcall_keywords),
"METH_COEXIST_FASTCALL_KEYWORDS": (
obj.meth_fastcall_keywords_coexist),
}

@reset_code
def testfunc(n):
# Use positional arguments to exercise CALL, not CALL_KW.
for _ in range(n):
class_result = METH_CLASS_FASTCALL_KEYWORDS(1, 2)
static_result = METH_STATIC_FASTCALL_KEYWORDS(1, 2)
coexist_result = METH_COEXIST_FASTCALL_KEYWORDS(1, 2)
return class_result, static_result, coexist_result

testfunc = types.FunctionType(testfunc.__code__, namespace)
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertEqual(res, (
(_testcapi.MethClass, (1, 2), {}),
(None, (1, 2), {}),
(obj, (1, 2), {}),
))
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(uops.count("_CALL_BUILTIN_FAST_WITH_KEYWORDS"), 3)
self.assertNotIn("_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS", uops)

def test_call_method_descriptor_o_extra_flags(self):
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

@reset_code
def testfunc(n):
d = {1: None}
for _ in range(n):
result = d.__contains__(1)
return result

res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertIs(res, True)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(uops.count("_CALL_METHOD_DESCRIPTOR_O_INLINE"), 1)
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_O", uops)

def test_call_method_descriptor_noargs_extra_flags(self):
_testcapi = import_helper.import_module("_testcapi")
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

namespace = {
"METH_COEXIST_NOARGS_OBJECT": _testcapi.DocStringNoSignatureTest(),
}

@reset_code
def testfunc(n):
for _ in range(n):
result = METH_COEXIST_NOARGS_OBJECT.meth_noargs_coexist()
return result

testfunc = types.FunctionType(testfunc.__code__, namespace)
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertIsNone(res)
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(
uops.count("_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE"), 1)
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS", uops)

def test_call_method_descriptor_fast_extra_flags(self):
_testcapi = import_helper.import_module("_testcapi")
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

obj = _testcapi.MethInstance()
namespace = {"METH_COEXIST_FAST_OBJECT": obj}

@reset_code
def testfunc(n):
for _ in range(n):
result = METH_COEXIST_FAST_OBJECT.meth_fastcall_coexist(1, 2)
return result

testfunc = types.FunctionType(testfunc.__code__, namespace)
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertEqual(res, (obj, (1, 2)))
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(uops.count("_CALL_METHOD_DESCRIPTOR_FAST_INLINE"), 1)
self.assertNotIn("_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST", uops)

def test_call_method_descriptor_fast_with_keywords_extra_flags(self):
_testcapi = import_helper.import_module("_testcapi")
self.addCleanup(_testinternalcapi.clear_executor_deletion_list)

obj = _testcapi.MethInstance()
namespace = {"METH_COEXIST_FAST_OBJECT": obj}

@reset_code
def testfunc(n):
# Use positional arguments to exercise CALL, not CALL_KW.
for _ in range(n):
result = (
METH_COEXIST_FAST_OBJECT.meth_fastcall_keywords_coexist(
1, 2))
return result

testfunc = types.FunctionType(testfunc.__code__, namespace)
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
self.assertEqual(res, (obj, (1, 2), {}))
self.assertIsNotNone(ex)
uops = get_opnames(ex)
self.assertEqual(
uops.count("_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE"), 1)
self.assertNotIn(
"_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS", uops)

def test_call_method_descriptor_o(self):
def testfunc(n):
x = 0
Expand Down
83 changes: 83 additions & 0 deletions Lib/test/test_opcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2171,6 +2171,89 @@ class MyList(list): pass
self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND")
self.assert_no_opcode(my_list_append, "CALL")

@cpython_only
@requires_specialization
def test_call_c_function_extra_flags(self):
# METH_CLASS, METH_STATIC and METH_COEXIST do not change the C
# calling convention, so the specialized instructions must not
# miss because of them.
_testcapi = import_module("_testcapi")

def call_1(func, arg):
return func(arg)

def call_2(func, arg1, arg2):
return func(arg1, arg2)

def call_3(func, arg1, arg2, arg3):
return func(arg1, arg2, arg3)

def call_method(obj, arg):
return obj.__contains__(arg)

def call_method_noargs(obj):
return obj.meth_noargs_coexist()

def call_method_fast(obj, arg1, arg2):
return obj.meth_fastcall_coexist(arg1, arg2)

def call_method_fast_with_keywords(obj, arg1, arg2):
return obj.meth_fastcall_keywords_coexist(arg1, arg2)

def call_site(f):
[call] = [instr for instr in dis.get_instructions(f, adaptive=True)
if instr.baseopname == "CALL"]
cache = {name: data for name, _, data in call.cache_info}
return call.opname, cache["counter"]

def label(obj):
return getattr(obj, "__qualname__", type(obj).__name__)

coexist = _testcapi.MethInstance()
cases = [
# dict.__contains__ has METH_O | METH_COEXIST
(call_1, {}.__contains__, ("key",), "CALL_BUILTIN_O"),
(call_method, {}, ("key",), "CALL_METHOD_DESCRIPTOR_O"),
# meth_noargs_coexist has METH_NOARGS | METH_COEXIST
(call_method_noargs, _testcapi.DocStringNoSignatureTest(), (),
"CALL_METHOD_DESCRIPTOR_NOARGS"),
# METH_FASTCALL, with or without METH_KEYWORDS, and METH_COEXIST
(call_method_fast, coexist, (1, 2),
"CALL_METHOD_DESCRIPTOR_FAST"),
(call_method_fast_with_keywords, coexist, (1, 2),
"CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS"),
(call_3, _testcapi.MethInstance.meth_fastcall_coexist,
(coexist, 1, 2), "CALL_METHOD_DESCRIPTOR_FAST"),
(call_3, _testcapi.MethInstance.meth_fastcall_keywords_coexist,
(coexist, 1, 2), "CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS"),
(call_2, coexist.meth_fastcall_coexist, (1, 2),
"CALL_BUILTIN_FAST"),
(call_2, coexist.meth_fastcall_keywords_coexist, (1, 2),
"CALL_BUILTIN_FAST_WITH_KEYWORDS"),
]
for owner in (_testcapi.MethClass, _testcapi.MethStatic):
cases += [
(call_1, owner.meth_o, (1,), "CALL_BUILTIN_O"),
(call_2, owner.meth_fastcall, (1, 2), "CALL_BUILTIN_FAST"),
(call_2, owner.meth_fastcall_keywords, (1, 2),
"CALL_BUILTIN_FAST_WITH_KEYWORDS"),
]

for f, func, args, opname in cases:
with self.subTest(call=f.__name__, func=label(func)):
reset_code(f)
expected = f(func, *args)
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
f(func, *args)
self.assertEqual(call_site(f)[0], opname)

# A hit leaves the counter of the call site unchanged.
# A miss decrements it.
before = call_site(f)
for _ in range(10):
self.assertEqual(f(func, *args), expected)
self.assertEqual(call_site(f), before)

@cpython_only
@requires_specialization
def test_load_attr_module_with_getattr(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix call specialization for C functions and methods with ``METH_CLASS``,
``METH_STATIC``, or ``METH_COEXIST`` flags, improving their call performance.
5 changes: 5 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3360,6 +3360,11 @@ static PyMethodDef meth_instance_methods[] = {
{"meth_noargs", meth_noargs, METH_NOARGS},
{"meth_fastcall", _PyCFunction_CAST(meth_fastcall), METH_FASTCALL},
{"meth_fastcall_keywords", _PyCFunction_CAST(meth_fastcall_keywords), METH_FASTCALL|METH_KEYWORDS},
{"meth_fastcall_coexist", _PyCFunction_CAST(meth_fastcall),
METH_FASTCALL|METH_COEXIST},
{"meth_fastcall_keywords_coexist",
_PyCFunction_CAST(meth_fastcall_keywords),
METH_FASTCALL|METH_KEYWORDS|METH_COEXIST},
{NULL, NULL} /* sentinel */
};

Expand Down
21 changes: 14 additions & 7 deletions Modules/_testinternalcapi/test_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -936,8 +936,7 @@ PyDescr_NewMethod(PyTypeObject *type, PyMethodDef *method)
{
/* Figure out correct vectorcall function to use */
vectorcallfunc vectorcall;
switch (method->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
METH_O | METH_KEYWORDS | METH_METHOD))
switch (method->ml_flags & _Py_METH_CALL_FLAGS)
{
case METH_VARARGS:
vectorcall = method_vectorcall_VARARGS;
Expand Down
3 changes: 1 addition & 2 deletions Objects/methodobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ PyCMethod_New(PyMethodDef *ml, PyObject *self, PyObject *module, PyTypeObject *c
{
/* Figure out correct vectorcall function to use */
vectorcallfunc vectorcall;
switch (ml->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
METH_O | METH_KEYWORDS | METH_METHOD))
switch (ml->ml_flags & _Py_METH_CALL_FLAGS)
{
case METH_VARARGS:
case METH_VARARGS | METH_KEYWORDS:
Expand Down
Loading
Loading