Skip to content

Commit 70767bd

Browse files
authored
Fix small regression in subtype cache performance (#5604)
Fixes #5514 The root cause is that #5474 added `type_parameter_checker` (a function) as one of the keys of subtype kinds. This function is a nested one, so in fact a _new_ function was created on every call to `is_subtype_ignoring_tvars()` causing many thousands parasitic keys created in subtype caches. In addition there are two smaller things listed below. The performance win is around 5% This PR: * Moves the mentioned function to global scope, this causes only 6 subtype kinds present during self-check. * Prevents similar regressions in future by switching only boolean flags as subtype kind keys. * Removes two one liners that stay in a very hot code path (cache lookup is hit 140K times during self-check). * Avoids creation of empty sets on cache look-up, create them only when writing to cache.
1 parent 62e6f51 commit 70767bd

File tree

2 files changed

+53
-51
lines changed

2 files changed

+53
-51
lines changed

mypy/subtypes.py

+41-45
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,21 @@
3636

3737
def check_type_parameter(lefta: Type, righta: Type, variance: int) -> bool:
3838
if variance == COVARIANT:
39-
return is_subtype(lefta, righta, check_type_parameter)
39+
return is_subtype(lefta, righta)
4040
elif variance == CONTRAVARIANT:
41-
return is_subtype(righta, lefta, check_type_parameter)
41+
return is_subtype(righta, lefta)
4242
else:
43-
return is_equivalent(lefta, righta, check_type_parameter)
43+
return is_equivalent(lefta, righta)
44+
45+
46+
def ignore_type_parameter(s: Type, t: Type, v: int) -> bool:
47+
return True
4448

4549

4650
def is_subtype(left: Type, right: Type,
47-
type_parameter_checker: Optional[TypeParameterChecker] = None,
48-
*, ignore_pos_arg_names: bool = False,
51+
*,
52+
ignore_type_params: bool = False,
53+
ignore_pos_arg_names: bool = False,
4954
ignore_declared_variance: bool = False,
5055
ignore_promotions: bool = False) -> bool:
5156
"""Is 'left' subtype of 'right'?
@@ -59,15 +64,15 @@ def is_subtype(left: Type, right: Type,
5964
between the type arguments (e.g., A and B), taking the variance of the
6065
type var into account.
6166
"""
62-
type_parameter_checker = type_parameter_checker or check_type_parameter
6367
if (isinstance(right, AnyType) or isinstance(right, UnboundType)
6468
or isinstance(right, ErasedType)):
6569
return True
6670
elif isinstance(right, UnionType) and not isinstance(left, UnionType):
6771
# Normally, when 'left' is not itself a union, the only way
6872
# 'left' can be a subtype of the union 'right' is if it is a
6973
# subtype of one of the items making up the union.
70-
is_subtype_of_item = any(is_subtype(left, item, type_parameter_checker,
74+
is_subtype_of_item = any(is_subtype(left, item,
75+
ignore_type_params=ignore_type_params,
7176
ignore_pos_arg_names=ignore_pos_arg_names,
7277
ignore_declared_variance=ignore_declared_variance,
7378
ignore_promotions=ignore_promotions)
@@ -83,69 +88,65 @@ def is_subtype(left: Type, right: Type,
8388
elif is_subtype_of_item:
8489
return True
8590
# otherwise, fall through
86-
return left.accept(SubtypeVisitor(right, type_parameter_checker,
91+
return left.accept(SubtypeVisitor(right,
92+
ignore_type_params=ignore_type_params,
8793
ignore_pos_arg_names=ignore_pos_arg_names,
8894
ignore_declared_variance=ignore_declared_variance,
8995
ignore_promotions=ignore_promotions))
9096

9197

9298
def is_subtype_ignoring_tvars(left: Type, right: Type) -> bool:
93-
def ignore_tvars(s: Type, t: Type, v: int) -> bool:
94-
return True
95-
return is_subtype(left, right, ignore_tvars)
99+
return is_subtype(left, right, ignore_type_params=True)
96100

97101

98-
def is_equivalent(a: Type,
99-
b: Type,
100-
type_parameter_checker: Optional[TypeParameterChecker] = None,
102+
def is_equivalent(a: Type, b: Type,
101103
*,
104+
ignore_type_params: bool = False,
102105
ignore_pos_arg_names: bool = False
103106
) -> bool:
104107
return (
105-
is_subtype(a, b, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names)
106-
and is_subtype(b, a, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names))
108+
is_subtype(a, b, ignore_type_params=ignore_type_params,
109+
ignore_pos_arg_names=ignore_pos_arg_names)
110+
and is_subtype(b, a, ignore_type_params=ignore_type_params,
111+
ignore_pos_arg_names=ignore_pos_arg_names))
107112

108113

109114
class SubtypeVisitor(TypeVisitor[bool]):
110115

111116
def __init__(self, right: Type,
112-
type_parameter_checker: TypeParameterChecker,
113-
*, ignore_pos_arg_names: bool = False,
117+
*,
118+
ignore_type_params: bool,
119+
ignore_pos_arg_names: bool = False,
114120
ignore_declared_variance: bool = False,
115121
ignore_promotions: bool = False) -> None:
116122
self.right = right
117-
self.check_type_parameter = type_parameter_checker
123+
self.ignore_type_params = ignore_type_params
118124
self.ignore_pos_arg_names = ignore_pos_arg_names
119125
self.ignore_declared_variance = ignore_declared_variance
120126
self.ignore_promotions = ignore_promotions
127+
self.check_type_parameter = (ignore_type_parameter if ignore_type_params else
128+
check_type_parameter)
121129
self._subtype_kind = SubtypeVisitor.build_subtype_kind(
122-
type_parameter_checker=type_parameter_checker,
130+
ignore_type_params=ignore_type_params,
123131
ignore_pos_arg_names=ignore_pos_arg_names,
124132
ignore_declared_variance=ignore_declared_variance,
125133
ignore_promotions=ignore_promotions)
126134

127135
@staticmethod
128136
def build_subtype_kind(*,
129-
type_parameter_checker: Optional[TypeParameterChecker] = None,
137+
ignore_type_params: bool = False,
130138
ignore_pos_arg_names: bool = False,
131139
ignore_declared_variance: bool = False,
132140
ignore_promotions: bool = False) -> SubtypeKind:
133-
type_parameter_checker = type_parameter_checker or check_type_parameter
134-
return ('subtype',
135-
type_parameter_checker,
141+
return (False, # is proper subtype?
142+
ignore_type_params,
136143
ignore_pos_arg_names,
137144
ignore_declared_variance,
138145
ignore_promotions)
139146

140-
def _lookup_cache(self, left: Instance, right: Instance) -> bool:
141-
return TypeState.is_cached_subtype_check(self._subtype_kind, left, right)
142-
143-
def _record_cache(self, left: Instance, right: Instance) -> None:
144-
TypeState.record_subtype_cache_entry(self._subtype_kind, left, right)
145-
146147
def _is_subtype(self, left: Type, right: Type) -> bool:
147148
return is_subtype(left, right,
148-
type_parameter_checker=self.check_type_parameter,
149+
ignore_type_params=self.ignore_type_params,
149150
ignore_pos_arg_names=self.ignore_pos_arg_names,
150151
ignore_declared_variance=self.ignore_declared_variance,
151152
ignore_promotions=self.ignore_promotions)
@@ -190,12 +191,12 @@ def visit_instance(self, left: Instance) -> bool:
190191
if isinstance(right, TupleType) and right.fallback.type.is_enum:
191192
return self._is_subtype(left, right.fallback)
192193
if isinstance(right, Instance):
193-
if self._lookup_cache(left, right):
194+
if TypeState.is_cached_subtype_check(self._subtype_kind, left, right):
194195
return True
195196
if not self.ignore_promotions:
196197
for base in left.type.mro:
197198
if base._promote and self._is_subtype(base._promote, self.right):
198-
self._record_cache(left, right)
199+
TypeState.record_subtype_cache_entry(self._subtype_kind, left, right)
199200
return True
200201
rname = right.type.fullname()
201202
# Always try a nominal check if possible,
@@ -208,7 +209,7 @@ def visit_instance(self, left: Instance) -> bool:
208209
for lefta, righta, tvar in
209210
zip(t.args, right.args, right.type.defn.type_vars))
210211
if nominal:
211-
self._record_cache(left, right)
212+
TypeState.record_subtype_cache_entry(self._subtype_kind, left, right)
212213
return nominal
213214
if right.type.is_protocol and is_protocol_implementation(left, right):
214215
return True
@@ -303,7 +304,8 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
303304
if not left.names_are_wider_than(right):
304305
return False
305306
for name, l, r in left.zip(right):
306-
if not is_equivalent(l, r, self.check_type_parameter):
307+
if not is_equivalent(l, r,
308+
ignore_type_params=self.ignore_type_params):
307309
return False
308310
# Non-required key is not compatible with a required key since
309311
# indexing may fail unexpectedly if a required key is missing.
@@ -1033,13 +1035,7 @@ def __init__(self, right: Type, *, ignore_promotions: bool = False) -> None:
10331035

10341036
@staticmethod
10351037
def build_subtype_kind(*, ignore_promotions: bool = False) -> SubtypeKind:
1036-
return ('subtype_proper', ignore_promotions)
1037-
1038-
def _lookup_cache(self, left: Instance, right: Instance) -> bool:
1039-
return TypeState.is_cached_subtype_check(self._subtype_kind, left, right)
1040-
1041-
def _record_cache(self, left: Instance, right: Instance) -> None:
1042-
TypeState.record_subtype_cache_entry(self._subtype_kind, left, right)
1038+
return (True, ignore_promotions)
10431039

10441040
def _is_proper_subtype(self, left: Type, right: Type) -> bool:
10451041
return is_proper_subtype(left, right, ignore_promotions=self.ignore_promotions)
@@ -1073,12 +1069,12 @@ def visit_deleted_type(self, left: DeletedType) -> bool:
10731069
def visit_instance(self, left: Instance) -> bool:
10741070
right = self.right
10751071
if isinstance(right, Instance):
1076-
if self._lookup_cache(left, right):
1072+
if TypeState.is_cached_subtype_check(self._subtype_kind, left, right):
10771073
return True
10781074
if not self.ignore_promotions:
10791075
for base in left.type.mro:
10801076
if base._promote and self._is_proper_subtype(base._promote, right):
1081-
self._record_cache(left, right)
1077+
TypeState.record_subtype_cache_entry(self._subtype_kind, left, right)
10821078
return True
10831079

10841080
if left.type.has_base(right.type.fullname()):
@@ -1095,7 +1091,7 @@ def check_argument(leftarg: Type, rightarg: Type, variance: int) -> bool:
10951091
nominal = all(check_argument(ta, ra, tvar.variance) for ta, ra, tvar in
10961092
zip(left.args, right.args, right.type.defn.type_vars))
10971093
if nominal:
1098-
self._record_cache(left, right)
1094+
TypeState.record_subtype_cache_entry(self._subtype_kind, left, right)
10991095
return nominal
11001096
if (right.type.is_protocol and
11011097
is_protocol_implementation(left, right, proper_subtype=True)):

mypy/typestate.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
# A tuple encoding the specific conditions under which we performed the subtype check.
1919
# (e.g. did we want a proper subtype? A regular subtype while ignoring variance?)
20-
SubtypeKind = Tuple[Any, ...]
20+
SubtypeKind = Tuple[bool, ...]
2121

2222
# A cache that keeps track of whether the given TypeInfo is a part of a particular
2323
# subtype relationship
@@ -83,7 +83,8 @@ def reset_all_subtype_caches(cls) -> None:
8383
@classmethod
8484
def reset_subtype_caches_for(cls, info: TypeInfo) -> None:
8585
"""Reset subtype caches (if any) for a given supertype TypeInfo."""
86-
cls._subtype_caches.setdefault(info, dict()).clear()
86+
if info in cls._subtype_caches:
87+
cls._subtype_caches[info].clear()
8788

8889
@classmethod
8990
def reset_all_subtype_caches_for(cls, info: TypeInfo) -> None:
@@ -93,14 +94,19 @@ def reset_all_subtype_caches_for(cls, info: TypeInfo) -> None:
9394

9495
@classmethod
9596
def is_cached_subtype_check(cls, kind: SubtypeKind, left: Instance, right: Instance) -> bool:
96-
subtype_kinds = cls._subtype_caches.setdefault(right.type, dict())
97-
return (left, right) in subtype_kinds.setdefault(kind, set())
97+
info = right.type
98+
if info not in cls._subtype_caches:
99+
return False
100+
cache = cls._subtype_caches[info]
101+
if kind not in cache:
102+
return False
103+
return (left, right) in cache[kind]
98104

99105
@classmethod
100106
def record_subtype_cache_entry(cls, kind: SubtypeKind,
101107
left: Instance, right: Instance) -> None:
102-
subtype_kinds = cls._subtype_caches.setdefault(right.type, dict())
103-
subtype_kinds.setdefault(kind, set()).add((left, right))
108+
cache = cls._subtype_caches.setdefault(right.type, dict())
109+
cache.setdefault(kind, set()).add((left, right))
104110

105111
@classmethod
106112
def reset_protocol_deps(cls) -> None:

0 commit comments

Comments
 (0)