Skip to content

Commit 58330d1

Browse files
authored
Add a benchmark for runtime-checkable protocols (#280)
1 parent 708265b commit 58330d1

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed

pyperformance/data-files/benchmarks/MANIFEST

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ sympy <local>
6969
telco <local>
7070
tomli_loads <local>
7171
tornado_http <local>
72+
typing_runtime_protocols <local>
7273
unpack_sequence <local>
7374
unpickle <local:pickle>
7475
unpickle_list <local:pickle>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "pyperformance_bm_typing_runtime_protocols"
3+
requires-python = ">=3.8"
4+
dependencies = ["pyperf"]
5+
urls = {repository = "https://github.com/python/pyperformance"}
6+
dynamic = ["version"]
7+
8+
[tool.pyperformance]
9+
name = "typing_runtime_protocols"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""
2+
Test the performance of isinstance() checks against runtime-checkable protocols.
3+
4+
For programmes that make extensive use of this feature,
5+
these calls can easily become a bottleneck.
6+
See https://github.com/python/cpython/issues/74690
7+
8+
The following situations all exercise different code paths
9+
in typing._ProtocolMeta.__instancecheck__,
10+
so each is tested in this benchmark:
11+
12+
(1) Comparing an instance of a class that directly inherits
13+
from a protocol to that protocol.
14+
(2) Comparing an instance of a class that fulfils the interface
15+
of a protocol using instance attributes.
16+
(3) Comparing an instance of a class that fulfils the interface
17+
of a protocol using class attributes.
18+
(4) Comparing an instance of a class that fulfils the interface
19+
of a protocol using properties.
20+
21+
Protocols with callable and non-callable members also
22+
exercise different code paths in _ProtocolMeta.__instancecheck__,
23+
so are also tested separately.
24+
"""
25+
26+
import time
27+
from typing import Protocol, runtime_checkable
28+
29+
import pyperf
30+
31+
32+
##################################################
33+
# Protocols to call isinstance() against
34+
##################################################
35+
36+
37+
@runtime_checkable
38+
class HasX(Protocol):
39+
"""A runtime-checkable protocol with a single non-callable member"""
40+
x: int
41+
42+
@runtime_checkable
43+
class HasManyAttributes(Protocol):
44+
"""A runtime-checkable protocol with many non-callable members"""
45+
a: int
46+
b: int
47+
c: int
48+
d: int
49+
e: int
50+
51+
@runtime_checkable
52+
class SupportsInt(Protocol):
53+
"""A runtime-checkable protocol with a single callable member"""
54+
def __int__(self) -> int: ...
55+
56+
@runtime_checkable
57+
class SupportsManyMethods(Protocol):
58+
"""A runtime-checkable protocol with many callable members"""
59+
def one(self) -> int: ...
60+
def two(self) -> str: ...
61+
def three(self) -> bytes: ...
62+
def four(self) -> memoryview: ...
63+
def five(self) -> bytearray: ...
64+
65+
@runtime_checkable
66+
class SupportsIntAndX(Protocol):
67+
"""A runtime-checkable protocol with a mix of callable and non-callable members"""
68+
x: int
69+
def __int__(self) -> int: ...
70+
71+
72+
##################################################
73+
# Classes for comparing against the protocols
74+
##################################################
75+
76+
77+
class Empty:
78+
"""Empty class with no attributes"""
79+
80+
class PropertyX:
81+
"""Class with a property x"""
82+
@property
83+
def x(self) -> int: return 42
84+
85+
class HasIntMethod:
86+
"""Class with an __int__ method"""
87+
def __int__(self): return 42
88+
89+
class HasManyMethods:
90+
"""Class with many methods"""
91+
def one(self): return 42
92+
def two(self): return "42"
93+
def three(self): return b"42"
94+
def four(self): return memoryview(b"42")
95+
def five(self): return bytearray(b"42")
96+
97+
class PropertyXWithInt:
98+
"""Class with a property x and an __int__ method"""
99+
@property
100+
def x(self) -> int: return 42
101+
def __int__(self): return 42
102+
103+
class ClassVarX:
104+
"""Class with a ClassVar x"""
105+
x = 42
106+
107+
class ClassVarXWithInt:
108+
"""Class with a ClassVar x and an __int__ method"""
109+
x = 42
110+
def __int__(self): return 42
111+
112+
class InstanceVarX:
113+
"""Class with an instance var x"""
114+
def __init__(self):
115+
self.x = 42
116+
117+
class ManyInstanceVars:
118+
"""Class with many instance vars"""
119+
def __init__(self):
120+
for attr in 'abcde':
121+
setattr(self, attr, 42)
122+
123+
class InstanceVarXWithInt:
124+
"""Class with an instance var x and an __int__ method"""
125+
def __init__(self):
126+
self.x = 42
127+
def __int__(self):
128+
return 42
129+
130+
class NominalX(HasX):
131+
"""Class that explicitly subclasses HasX"""
132+
def __init__(self):
133+
self.x = 42
134+
135+
class NominalSupportsInt(SupportsInt):
136+
"""Class that explicitly subclasses SupportsInt"""
137+
def __int__(self):
138+
return 42
139+
140+
class NominalXWithInt(SupportsIntAndX):
141+
"""Class that explicitly subclasses NominalXWithInt"""
142+
def __init__(self):
143+
self.x = 42
144+
145+
146+
##################################################
147+
# Time to test the performance of isinstance()!
148+
##################################################
149+
150+
151+
def bench_protocols(loops: int) -> float:
152+
protocols = [
153+
HasX, HasManyAttributes, SupportsInt, SupportsManyMethods, SupportsIntAndX
154+
]
155+
instances = [
156+
cls()
157+
for cls in (
158+
Empty, PropertyX, HasIntMethod, HasManyMethods, PropertyXWithInt,
159+
ClassVarX, ClassVarXWithInt, InstanceVarX, ManyInstanceVars,
160+
InstanceVarXWithInt, NominalX, NominalSupportsInt, NominalXWithInt
161+
)
162+
]
163+
164+
t0 = time.perf_counter()
165+
166+
for _ in range(loops):
167+
for protocol in protocols:
168+
for instance in instances:
169+
isinstance(instance, protocol)
170+
171+
return time.perf_counter() - t0
172+
173+
174+
if __name__ == "__main__":
175+
runner = pyperf.Runner()
176+
runner.metadata["description"] = (
177+
"Test the performance of isinstance() checks "
178+
"against runtime-checkable protocols"
179+
)
180+
runner.bench_time_func("typing_runtime_protocols", bench_protocols)

0 commit comments

Comments
 (0)