Skip to content

Commit fc8bd83

Browse files
committed
Support building C extensions in benchmarks
Add ctypes and ctypes_argtypes benchmarks
1 parent 259edee commit fc8bd83

16 files changed

+228
-12
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# Created by setup.py sdist
1111
build/
1212
dist/
13-
pyperformance.egg-info/
13+
*.egg-info/
1414

1515
# Created by the pyperformance script
1616
venv/

MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ include pyperformance/data-files/benchmarks/MANIFEST
1515
include pyperformance/data-files/benchmarks/bm_*/*.toml
1616
include pyperformance/data-files/benchmarks/bm_*/*.py
1717
include pyperformance/data-files/benchmarks/bm_*/requirements.txt
18+
include pyperformance/data-files/benchmarks/bm_*/*.c
1819
recursive-include pyperformance/data-files/benchmarks/bm_*/data *
1920
recursive-exclude pyperformance/tests *

doc/benchmarks.rst

+10
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ deepcopy
130130
Benchmark the Python `copy.deepcopy` method. The `deepcopy` method is
131131
performed on a nested dictionary and a dataclass.
132132

133+
ctypes
134+
------
135+
136+
Benchmark to measure the function call overhead of calling C functions using ctypes.
137+
138+
The ``ctypes`` benchmark lets ``ctypes`` infer the argument types from the passed in
139+
values. The ``ctypes_argtypes`` benchmark `explicitly specifies the argument types
140+
<https://docs.python.org/3.10/library/ctypes.html?highlight=ctypes#specifying-the-required-argument-types-function-prototypes>`_,
141+
which is slower than inferred argument types.
142+
133143
deltablue
134144
---------
135145

doc/custom_benchmarks.rst

+14-10
Original file line numberDiff line numberDiff line change
@@ -324,16 +324,17 @@ All other PEP 621 fields are optional (e.g. ``requires-python = ">=3.8"``,
324324
The ``[tool.pyperformance]`` Section
325325
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
326326

327-
=============== ===== === === ===
328-
field type R B F
329-
=============== ===== === === ===
330-
tool.name str X X
331-
tool.tags [str] X
332-
tool.extra_opts [str] X
333-
tool.inherits file
334-
tool.runscript file X
335-
tool.datadir file X
336-
=============== ===== === === ===
327+
================== ===== === === ===
328+
field type R B F
329+
================== ===== === === ===
330+
tool.name str X X
331+
tool.tags [str] X
332+
tool.extra_opts [str] X
333+
tool.inherits file
334+
tool.runscript file X
335+
tool.datadir file X
336+
tool.install_setup bool
337+
================== ===== === === ===
337338

338339
"R": required
339340
"B": inferred from the inherited metadata
@@ -342,3 +343,6 @@ tool.datadir file X
342343
* tags: optional list of names to group benchmarks
343344
* extra_opts: optional list of args to pass to ``tool.runscript``
344345
* runscript: the benchmark script to use instead of run_benchmark.py.
346+
* install_setup: when ``true``, run ``pip install -e .`` in the
347+
benchmark directory to install it in the virtual environment. This has the
348+
effect of running a ``setup.py`` file, if present.

pyperformance/_benchmark.py

+7
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ def runscript(self):
164164
def extra_opts(self):
165165
return self._get_metadata_value('extra_opts', ())
166166

167+
@property
168+
def setup_py(self):
169+
if not self._get_metadata_value('install_setup', False):
170+
return None
171+
filename = os.path.join(os.path.dirname(self.metafile), 'setup.py')
172+
return filename if os.path.exists(filename) else None
173+
167174
# Other metadata keys:
168175
# * base
169176
# * python

pyperformance/_benchmark_metadata.py

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
'datadir': None,
3333
'runscript': None,
3434
'extra_opts': None,
35+
'install_setup': None,
3536
}
3637

3738

@@ -228,6 +229,9 @@ def _resolve_value(field, value, rootdir):
228229
for opt in value:
229230
if not opt or not isinstance(opt, str):
230231
raise TypeError(f'extra_opts should be a list of strings, got {value!r}')
232+
elif field == 'install_setup':
233+
if not isinstance(value, bool):
234+
raise TypeError(f'install_setup should be a bool, got {value!r}')
231235
else:
232236
raise NotImplementedError(field)
233237
return value

pyperformance/_pip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def install_requirements(reqs, *extra,
150150
if upgrade:
151151
args.append('-U') # --upgrade
152152
for reqs in [reqs, *extra]:
153-
if os.path.exists(reqs):
153+
if os.path.isfile(reqs):
154154
args.append('-r') # --requirement
155155
args.append(reqs)
156156
return run_pip('install', *args, **kwargs)

pyperformance/data-files/benchmarks/MANIFEST

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ generators <local>
1111
chameleon <local>
1212
chaos <local>
1313
crypto_pyaes <local>
14+
ctypes <local>
15+
ctypes_argtypes <local:ctypes>
1416
deepcopy <local>
1517
deltablue <local>
1618
django_template <local>
@@ -69,6 +71,7 @@ xml_etree <local>
6971
#apps
7072
#math
7173
#template
74+
#extension
7275

7376

7477
[group default]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "pyperformance_bm_ctypes_argtypes"
3+
requires-python = ">=3.7"
4+
dependencies = ["pyperf"]
5+
urls = {repository = "https://github.com/python/pyperformance"}
6+
dynamic = ["version"]
7+
8+
[tool.pyperformance]
9+
name = "ctypes_argtypes"
10+
tags = "extension"
11+
extra_opts = ["--argtypes"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#include <Python.h>
2+
3+
#if defined(_WIN32) || defined(__CYGWIN__)
4+
#define EXPORTED_SYMBOL __declspec(dllexport)
5+
#else
6+
#define EXPORTED_SYMBOL
7+
#endif
8+
9+
10+
EXPORTED_SYMBOL
11+
void void_foo_void(void) {
12+
13+
}
14+
15+
EXPORTED_SYMBOL
16+
int int_foo_int(int a) {
17+
return a + 1;
18+
}
19+
20+
EXPORTED_SYMBOL
21+
void void_foo_int(int a) {
22+
23+
}
24+
25+
EXPORTED_SYMBOL
26+
void void_foo_int_int(int a, int b) {
27+
28+
}
29+
30+
EXPORTED_SYMBOL
31+
void void_foo_int_int_int(int a, int b, int c) {
32+
33+
}
34+
35+
EXPORTED_SYMBOL
36+
void void_foo_int_int_int_int(int a, int b, int c, int d) {
37+
38+
}
39+
40+
EXPORTED_SYMBOL
41+
void void_foo_constchar(const char* str) {
42+
43+
}
44+
45+
PyMODINIT_FUNC
46+
PyInit_cmodule(void) {
47+
// DELIBERATELY EMPTY
48+
49+
// This isn't actually a Python extension module (it's used via ctypes), so
50+
// this entry point function will never be called. However, we are utilizing
51+
// setuptools to build it, and on Windows, setuptools explicitly passes the
52+
// flag /EXPORT:PyInit_cmodule, so it must be defined.
53+
return NULL;
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "pyperformance_bm_ctypes"
3+
requires-python = ">=3.7"
4+
dependencies = ["pyperf", "setuptools"]
5+
urls = {repository = "https://github.com/python/pyperformance"}
6+
dynamic = ["version"]
7+
8+
[tool.pyperformance]
9+
name = "ctypes"
10+
tags = "extension"
11+
install_setup = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
setuptools==62.4.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Test the function call overhead of ctypes.
3+
"""
4+
import pyperf
5+
6+
7+
import ctypes
8+
import importlib.util
9+
10+
11+
spec = importlib.util.find_spec("bm_ctypes.cmodule")
12+
if spec is None:
13+
raise ImportError("Can't find bm_ctypes.cmodule shared object file")
14+
ext = ctypes.cdll.LoadLibrary(spec.origin)
15+
16+
17+
def benchmark_argtypes(loops):
18+
void_foo_void = ext.void_foo_void
19+
void_foo_void.argtypes = []
20+
void_foo_void.restype = None
21+
22+
int_foo_int = ext.void_foo_int
23+
int_foo_int.argtypes = [ctypes.c_int]
24+
int_foo_int.restype = ctypes.c_int
25+
26+
void_foo_int = ext.void_foo_int
27+
void_foo_int.argtypes = [ctypes.c_int]
28+
void_foo_int.restype = None
29+
30+
void_foo_int_int = ext.void_foo_int_int
31+
void_foo_int_int.argtypes = [ctypes.c_int, ctypes.c_int]
32+
void_foo_int_int.restype = None
33+
34+
void_foo_int_int_int = ext.void_foo_int_int_int
35+
void_foo_int_int_int.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
36+
void_foo_int_int_int.restype = None
37+
38+
void_foo_int_int_int_int = ext.void_foo_int_int_int_int
39+
void_foo_int_int_int_int.argtypes = [
40+
ctypes.c_int,
41+
ctypes.c_int,
42+
ctypes.c_int,
43+
ctypes.c_int,
44+
]
45+
void_foo_int_int_int_int.restype = None
46+
47+
void_foo_constchar = ext.void_foo_constchar
48+
void_foo_constchar.argtypes = [ctypes.c_char_p]
49+
void_foo_constchar.restype = None
50+
51+
return benchmark(loops)
52+
53+
54+
def benchmark(loops):
55+
void_foo_void = ext.void_foo_void
56+
int_foo_int = ext.int_foo_int
57+
void_foo_int = ext.void_foo_int
58+
void_foo_int_int = ext.void_foo_int_int
59+
void_foo_int_int_int = ext.void_foo_int_int_int
60+
void_foo_int_int_int_int = ext.void_foo_int_int_int_int
61+
void_foo_constchar = ext.void_foo_constchar
62+
63+
range_it = range(loops)
64+
65+
# Test calling the functions using the implied arguments mechanism
66+
t0 = pyperf.perf_counter()
67+
68+
for _ in range_it:
69+
void_foo_void()
70+
int_foo_int(1)
71+
void_foo_int(1)
72+
void_foo_int_int(1, 2)
73+
void_foo_int_int_int(1, 2, 3)
74+
void_foo_int_int_int_int(1, 2, 3, 4)
75+
void_foo_constchar(b"bytes")
76+
77+
return pyperf.perf_counter() - t0
78+
79+
80+
def add_cmdline_args(cmd, args):
81+
if args.argtypes:
82+
cmd.append("--argtypes")
83+
84+
85+
if __name__ == "__main__":
86+
runner = pyperf.Runner(add_cmdline_args=add_cmdline_args)
87+
runner.metadata["description"] = "ctypes function call overhead benchmark"
88+
89+
runner.argparser.add_argument("--argtypes", action="store_true")
90+
options = runner.parse_args()
91+
92+
if options.argtypes:
93+
runner.bench_time_func("ctypes_argtypes", benchmark_argtypes)
94+
else:
95+
runner.bench_time_func("ctypes", benchmark)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from setuptools import setup, Extension
2+
3+
# Compile the C shared object containing functions to call through ctypes. It
4+
# isn't technically a Python C extension, but this is the easiest way to build
5+
# it in a cross-platform way.
6+
7+
setup(
8+
name="pyperformance_bm_ctypes",
9+
ext_modules=[Extension("bm_ctypes.cmodule", sources=["cmodule.c"])],
10+
package_dir={"bm_ctypes": "src"},
11+
)

pyperformance/data-files/benchmarks/bm_ctypes/src/__init__.py

Whitespace-only changes.

pyperformance/venv.py

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def from_benchmarks(cls, benchmarks):
2424
for bench in benchmarks or ():
2525
filename = bench.requirements_lockfile
2626
self._add_from_file(filename)
27+
if bench.setup_py:
28+
# pip doesn't support installing a setup.py,
29+
# but it does support installing from the directory it is in.
30+
self._add(os.path.dirname(bench.setup_py))
2731
return self
2832

2933
def __init__(self):

0 commit comments

Comments
 (0)