Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5d2ea16

Browse files
authored
[mypyc] Use faster METH_FASTCALL wrapper functions on Python 3.7+ (python#9894)
Implement faster argument parsing based on METH_FASTCALL on supported Python versions. Use `vgetargskeywordsfast` extracted from Python 3.9 with some modifications: * Support required keyword-only arguments, `*args` and `**kwargs` * Only support the 'O' type (to reduce code size and speed things up) The modifications are very similar to what we have in the old-style argument parsing logic. The legacy calling convention is still used for `__init__` and `__call__`. I'll add `__call__` support in a separate PR. I haven't looked into supporting `__init__` yet. Here are some benchmark results (on Python 3.8) * keyword_args_from_interpreted: 3.5x faster than before * positional_args_from_interpreted: 1.4x faster than before However, the above benchmarks are still slower when compiled. I'll continue working on further improvements after this PR. Fixes mypyc/mypyc#578.
1 parent b535072 commit 5d2ea16

8 files changed

Lines changed: 941 additions & 43 deletions

File tree

LICENSE

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ DEALINGS IN THE SOFTWARE.
2828

2929
Portions of mypy and mypyc are licensed under different licenses. The
3030
files under stdlib-samples as well as the files
31-
mypyc/lib-rt/pythonsupport.h and mypyc/lib-rt/getargs.c are licensed
32-
under the PSF 2 License, reproduced below.
31+
mypyc/lib-rt/pythonsupport.h, mypyc/lib-rt/getargs.c and
32+
mypyc/lib-rt/getargsfast.c are licensed under the PSF 2 License, reproduced
33+
below.
3334

3435
= = = = =
3536

mypyc/codegen/emitclass.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Optional, List, Tuple, Dict, Callable, Mapping, Set
55
from mypy.ordered_dict import OrderedDict
66

7-
from mypyc.common import PREFIX, NATIVE_PREFIX, REG_PREFIX
7+
from mypyc.common import PREFIX, NATIVE_PREFIX, REG_PREFIX, USE_FASTCALL
88
from mypyc.codegen.emit import Emitter, HeaderDeclaration
99
from mypyc.codegen.emitfunc import native_function_header
1010
from mypyc.codegen.emitwrapper import (
@@ -644,7 +644,11 @@ def generate_methods_table(cl: ClassIR,
644644
continue
645645
emitter.emit_line('{{"{}",'.format(fn.name))
646646
emitter.emit_line(' (PyCFunction){}{},'.format(PREFIX, fn.cname(emitter.names)))
647-
flags = ['METH_VARARGS', 'METH_KEYWORDS']
647+
if USE_FASTCALL:
648+
flags = ['METH_FASTCALL']
649+
else:
650+
flags = ['METH_VARARGS']
651+
flags.append('METH_KEYWORDS')
648652
if fn.decl.kind == FUNC_STATICMETHOD:
649653
flags.append('METH_STATIC')
650654
elif fn.decl.kind == FUNC_CLASSMETHOD:

mypyc/codegen/emitmodule.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323
from mypyc.irbuild.prepare import load_type_map
2424
from mypyc.irbuild.mapper import Mapper
2525
from mypyc.common import (
26-
PREFIX, TOP_LEVEL_NAME, INT_PREFIX, MODULE_PREFIX, RUNTIME_C_FILES, shared_lib_name,
26+
PREFIX, TOP_LEVEL_NAME, INT_PREFIX, MODULE_PREFIX, RUNTIME_C_FILES, USE_FASTCALL,
27+
shared_lib_name,
2728
)
2829
from mypyc.codegen.cstring import encode_as_c_string, encode_bytes_as_c_string
2930
from mypyc.codegen.emit import EmitterContext, Emitter, HeaderDeclaration
3031
from mypyc.codegen.emitfunc import generate_native_function, native_function_header
3132
from mypyc.codegen.emitclass import generate_class_type_decl, generate_class
3233
from mypyc.codegen.emitwrapper import (
3334
generate_wrapper_function, wrapper_function_header,
35+
generate_legacy_wrapper_function, legacy_wrapper_function_header,
3436
)
3537
from mypyc.ir.ops import LiteralsMap, DeserMaps
3638
from mypyc.ir.rtypes import RType, RTuple
@@ -419,8 +421,12 @@ def generate_function_declaration(fn: FuncIR, emitter: Emitter) -> None:
419421
'{};'.format(native_function_header(fn.decl, emitter)),
420422
needs_export=True)
421423
if fn.name != TOP_LEVEL_NAME:
422-
emitter.context.declarations[PREFIX + fn.cname(emitter.names)] = HeaderDeclaration(
423-
'{};'.format(wrapper_function_header(fn, emitter.names)))
424+
if is_fastcall_supported(fn):
425+
emitter.context.declarations[PREFIX + fn.cname(emitter.names)] = HeaderDeclaration(
426+
'{};'.format(wrapper_function_header(fn, emitter.names)))
427+
else:
428+
emitter.context.declarations[PREFIX + fn.cname(emitter.names)] = HeaderDeclaration(
429+
'{};'.format(legacy_wrapper_function_header(fn, emitter.names)))
424430

425431

426432
def pointerize(decl: str, name: str) -> str:
@@ -529,9 +535,12 @@ def generate_c_for_modules(self) -> List[Tuple[str, str]]:
529535
generate_native_function(fn, emitter, self.source_paths[module_name], module_name)
530536
if fn.name != TOP_LEVEL_NAME:
531537
emitter.emit_line()
532-
generate_wrapper_function(
533-
fn, emitter, self.source_paths[module_name], module_name)
534-
538+
if is_fastcall_supported(fn):
539+
generate_wrapper_function(
540+
fn, emitter, self.source_paths[module_name], module_name)
541+
else:
542+
generate_legacy_wrapper_function(
543+
fn, emitter, self.source_paths[module_name], module_name)
535544
if multi_file:
536545
name = ('__native_{}.c'.format(emitter.names.private_name(module_name)))
537546
file_contents.append((name, ''.join(emitter.fragments)))
@@ -837,12 +846,17 @@ def generate_module_def(self, emitter: Emitter, module_name: str, module: Module
837846
for fn in module.functions:
838847
if fn.class_name is not None or fn.name == TOP_LEVEL_NAME:
839848
continue
849+
if is_fastcall_supported(fn):
850+
flag = 'METH_FASTCALL'
851+
else:
852+
flag = 'METH_VARARGS'
840853
emitter.emit_line(
841-
('{{"{name}", (PyCFunction){prefix}{cname}, METH_VARARGS | METH_KEYWORDS, '
854+
('{{"{name}", (PyCFunction){prefix}{cname}, {flag} | METH_KEYWORDS, '
842855
'NULL /* docstring */}},').format(
843-
name=fn.name,
844-
cname=fn.cname(emitter.names),
845-
prefix=PREFIX))
856+
name=fn.name,
857+
cname=fn.cname(emitter.names),
858+
prefix=PREFIX,
859+
flag=flag))
846860
emitter.emit_line('{NULL, NULL, 0, NULL}')
847861
emitter.emit_line('};')
848862
emitter.emit_line()
@@ -1054,3 +1068,8 @@ def visit(item: T) -> None:
10541068
visit(item)
10551069

10561070
return result
1071+
1072+
1073+
def is_fastcall_supported(fn: FuncIR) -> bool:
1074+
# TODO: Support METH_FASTCALL for all methods.
1075+
return USE_FASTCALL and (fn.class_name is None or fn.name not in ('__init__', '__call__'))

mypyc/codegen/emitwrapper.py

Lines changed: 165 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
"""Generate CPython API wrapper function for a native function."""
1+
"""Generate CPython API wrapper functions for native functions.
2+
3+
The wrapper functions are used by the CPython runtime when calling
4+
native functions from interpreted code, and when the called function
5+
can't be determined statically in compiled code. They validate, match,
6+
unbox and type check function arguments, and box return values as
7+
needed. All wrappers accept and return 'PyObject *' (boxed) values.
8+
9+
The wrappers aren't used for most calls between two native functions
10+
or methods in a single compilation unit.
11+
"""
212

313
from typing import List, Optional
414

@@ -14,15 +24,85 @@
1424
from mypyc.namegen import NameGenerator
1525

1626

27+
# Generic vectorcall wrapper functions (Python 3.7+)
28+
#
29+
# A wrapper function has a signature like this:
30+
#
31+
# PyObject *fn(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
32+
#
33+
# The function takes a self object, pointer to an array of arguments,
34+
# the number of positional arguments, and a tuple of keyword argument
35+
# names (that are stored starting in args[nargs]).
36+
#
37+
# It returns the returned object, or NULL on an exception.
38+
#
39+
# These are more efficient than legacy wrapper functions, since
40+
# usually no tuple or dict objects need to be created for the
41+
# arguments. Vectorcalls also use pre-constructed str objects for
42+
# keyword argument names and other pre-computed information, instead
43+
# of processing the argument format string on each call.
44+
45+
1746
def wrapper_function_header(fn: FuncIR, names: NameGenerator) -> str:
18-
return 'PyObject *{prefix}{name}(PyObject *self, PyObject *args, PyObject *kw)'.format(
19-
prefix=PREFIX,
20-
name=fn.cname(names))
47+
"""Return header of a vectorcall wrapper function.
48+
49+
See comment above for a summary of the arguments.
50+
"""
51+
return (
52+
'PyObject *{prefix}{name}('
53+
'PyObject *self, PyObject *const *args, size_t nargs, PyObject *kwnames)').format(
54+
prefix=PREFIX,
55+
name=fn.cname(names))
56+
57+
58+
def generate_traceback_code(fn: FuncIR,
59+
emitter: Emitter,
60+
source_path: str,
61+
module_name: str) -> str:
62+
# If we hit an error while processing arguments, then we emit a
63+
# traceback frame to make it possible to debug where it happened.
64+
# Unlike traceback frames added for exceptions seen in IR, we do this
65+
# even if there is no `traceback_name`. This is because the error will
66+
# have originated here and so we need it in the traceback.
67+
globals_static = emitter.static_name('globals', module_name)
68+
traceback_code = 'CPy_AddTraceback("%s", "%s", %d, %s);' % (
69+
source_path.replace("\\", "\\\\"),
70+
fn.traceback_name or fn.name,
71+
fn.line,
72+
globals_static)
73+
return traceback_code
74+
75+
76+
def make_arg_groups(args: List[RuntimeArg]) -> List[List[RuntimeArg]]:
77+
"""Group arguments by kind."""
78+
return [[arg for arg in args if arg.kind == k] for k in range(ARG_NAMED_OPT + 1)]
79+
80+
81+
def reorder_arg_groups(groups: List[List[RuntimeArg]]) -> List[RuntimeArg]:
82+
"""Reorder argument groups to match their order in a format string."""
83+
return groups[ARG_POS] + groups[ARG_OPT] + groups[ARG_NAMED_OPT] + groups[ARG_NAMED]
84+
85+
86+
def make_static_kwlist(args: List[RuntimeArg]) -> str:
87+
arg_names = ''.join('"{}", '.format(arg.name) for arg in args)
88+
return 'static const char * const kwlist[] = {{{}0}};'.format(arg_names)
2189

2290

2391
def make_format_string(func_name: str, groups: List[List[RuntimeArg]]) -> str:
24-
# Construct the format string. Each group requires the previous
25-
# groups delimiters to be present first.
92+
"""Return a format string that specifies the accepted arguments.
93+
94+
The format string is an extended subset of what is supported by
95+
PyArg_ParseTupleAndKeywords(). Only the type 'O' is used, and we
96+
also support some extensions:
97+
98+
- Required keyword-only arguments are introduced after '@'
99+
- If the function receives *args or **kwargs, we add a '%' prefix
100+
101+
Each group requires the previous groups' delimiters to be present
102+
first.
103+
104+
These are used by both vectorcall and legacy wrapper functions.
105+
"""
26106
main_format = ''
27107
if groups[ARG_STAR] or groups[ARG_STAR2]:
28108
main_format += '%'
@@ -40,24 +120,80 @@ def generate_wrapper_function(fn: FuncIR,
40120
emitter: Emitter,
41121
source_path: str,
42122
module_name: str) -> None:
43-
"""Generates a CPython-compatible wrapper function for a native function.
123+
"""Generate a CPython-compatible vectorcall wrapper for a native function.
44124
45125
In particular, this handles unboxing the arguments, calling the native function, and
46126
then boxing the return value.
47127
"""
48128
emitter.emit_line('{} {{'.format(wrapper_function_header(fn, emitter.names)))
49129

50-
# If we hit an error while processing arguments, then we emit a
51-
# traceback frame to make it possible to debug where it happened.
52-
# Unlike traceback frames added for exceptions seen in IR, we do this
53-
# even if there is no `traceback_name`. This is because the error will
54-
# have originated here and so we need it in the traceback.
55-
globals_static = emitter.static_name('globals', module_name)
56-
traceback_code = 'CPy_AddTraceback("%s", "%s", %d, %s);' % (
57-
source_path.replace("\\", "\\\\"),
58-
fn.traceback_name or fn.name,
59-
fn.line,
60-
globals_static)
130+
# If fn is a method, then the first argument is a self param
131+
real_args = list(fn.args)
132+
if fn.class_name and not fn.decl.kind == FUNC_STATICMETHOD:
133+
arg = real_args.pop(0)
134+
emitter.emit_line('PyObject *obj_{} = self;'.format(arg.name))
135+
136+
# Need to order args as: required, optional, kwonly optional, kwonly required
137+
# This is because CPyArg_ParseStackAndKeywords format string requires
138+
# them grouped in that way.
139+
groups = make_arg_groups(real_args)
140+
reordered_args = reorder_arg_groups(groups)
141+
142+
emitter.emit_line(make_static_kwlist(reordered_args))
143+
fmt = make_format_string(fn.name, groups)
144+
# Define the arguments the function accepts (but no types yet)
145+
emitter.emit_line('static CPyArg_Parser parser = {{"{}", kwlist, 0}};'.format(fmt))
146+
147+
for arg in real_args:
148+
emitter.emit_line('PyObject *obj_{}{};'.format(
149+
arg.name, ' = NULL' if arg.optional else ''))
150+
151+
cleanups = ['CPy_DECREF(obj_{});'.format(arg.name)
152+
for arg in groups[ARG_STAR] + groups[ARG_STAR2]]
153+
154+
arg_ptrs = [] # type: List[str]
155+
if groups[ARG_STAR] or groups[ARG_STAR2]:
156+
arg_ptrs += ['&obj_{}'.format(groups[ARG_STAR][0].name) if groups[ARG_STAR] else 'NULL']
157+
arg_ptrs += ['&obj_{}'.format(groups[ARG_STAR2][0].name) if groups[ARG_STAR2] else 'NULL']
158+
arg_ptrs += ['&obj_{}'.format(arg.name) for arg in reordered_args]
159+
160+
emitter.emit_lines(
161+
'if (!CPyArg_ParseStackAndKeywords(args, nargs, kwnames, &parser{})) {{'.format(
162+
''.join(', ' + n for n in arg_ptrs)),
163+
'return NULL;',
164+
'}')
165+
traceback_code = generate_traceback_code(fn, emitter, source_path, module_name)
166+
generate_wrapper_core(fn, emitter, groups[ARG_OPT] + groups[ARG_NAMED_OPT],
167+
cleanups=cleanups,
168+
traceback_code=traceback_code)
169+
170+
emitter.emit_line('}')
171+
172+
173+
# Legacy generic wrapper functions
174+
#
175+
# These take a self object, a Python tuple of positional arguments,
176+
# and a dict of keyword arguments. These are a lot slower than
177+
# vectorcall wrappers, especially in calls involving keyword
178+
# arguments.
179+
180+
181+
def legacy_wrapper_function_header(fn: FuncIR, names: NameGenerator) -> str:
182+
return 'PyObject *{prefix}{name}(PyObject *self, PyObject *args, PyObject *kw)'.format(
183+
prefix=PREFIX,
184+
name=fn.cname(names))
185+
186+
187+
def generate_legacy_wrapper_function(fn: FuncIR,
188+
emitter: Emitter,
189+
source_path: str,
190+
module_name: str) -> None:
191+
"""Generates a CPython-compatible legacy wrapper for a native function.
192+
193+
In particular, this handles unboxing the arguments, calling the native function, and
194+
then boxing the return value.
195+
"""
196+
emitter.emit_line('{} {{'.format(legacy_wrapper_function_header(fn, emitter.names)))
61197

62198
# If fn is a method, then the first argument is a self param
63199
real_args = list(fn.args)
@@ -68,11 +204,10 @@ def generate_wrapper_function(fn: FuncIR,
68204
# Need to order args as: required, optional, kwonly optional, kwonly required
69205
# This is because CPyArg_ParseTupleAndKeywords format string requires
70206
# them grouped in that way.
71-
groups = [[arg for arg in real_args if arg.kind == k] for k in range(ARG_NAMED_OPT + 1)]
72-
reordered_args = groups[ARG_POS] + groups[ARG_OPT] + groups[ARG_NAMED_OPT] + groups[ARG_NAMED]
207+
groups = make_arg_groups(real_args)
208+
reordered_args = reorder_arg_groups(groups)
73209

74-
arg_names = ''.join('"{}", '.format(arg.name) for arg in reordered_args)
75-
emitter.emit_line('static char *kwlist[] = {{{}0}};'.format(arg_names))
210+
emitter.emit_line(make_static_kwlist(reordered_args))
76211
for arg in real_args:
77212
emitter.emit_line('PyObject *obj_{}{};'.format(
78213
arg.name, ' = NULL' if arg.optional else ''))
@@ -91,13 +226,17 @@ def generate_wrapper_function(fn: FuncIR,
91226
make_format_string(fn.name, groups), ''.join(', ' + n for n in arg_ptrs)),
92227
'return NULL;',
93228
'}')
229+
traceback_code = generate_traceback_code(fn, emitter, source_path, module_name)
94230
generate_wrapper_core(fn, emitter, groups[ARG_OPT] + groups[ARG_NAMED_OPT],
95231
cleanups=cleanups,
96232
traceback_code=traceback_code)
97233

98234
emitter.emit_line('}')
99235

100236

237+
# Specialized wrapper functions
238+
239+
101240
def generate_dunder_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
102241
"""Generates a wrapper for native __dunder__ methods to be able to fit into the mapping
103242
protocol slot. This specifically means that the arguments are taken as *PyObjects and returned
@@ -214,12 +353,16 @@ def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
214353
return name
215354

216355

356+
# Helpers
357+
358+
217359
def generate_wrapper_core(fn: FuncIR, emitter: Emitter,
218360
optional_args: Optional[List[RuntimeArg]] = None,
219361
arg_names: Optional[List[str]] = None,
220362
cleanups: Optional[List[str]] = None,
221363
traceback_code: Optional[str] = None) -> None:
222364
"""Generates the core part of a wrapper function for a native function.
365+
223366
This expects each argument as a PyObject * named obj_{arg} as a precondition.
224367
It converts the PyObject *s to the necessary types, checking and unboxing if necessary,
225368
makes the call, then boxes the result if necessary and returns it.

mypyc/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@
5050
MAX_LITERAL_SHORT_INT = (sys.maxsize >> 1 if not IS_MIXED_32_64_BIT_BUILD
5151
else 2**30 - 1) # type: Final
5252

53+
# We can use faster wrapper functions on Python 3.7+ (fastcall/vectorcall).
54+
USE_FASTCALL = sys.version_info >= (3, 7)
55+
5356
# Runtime C library files
5457
RUNTIME_C_FILES = [
5558
'init.c',
5659
'getargs.c',
60+
'getargsfast.c',
5761
'int_ops.c',
5862
'list_ops.c',
5963
'dict_ops.c',

0 commit comments

Comments
 (0)