Skip to content

Commit 74cfa3d

Browse files
authored
[mypyc] Borrow references during chained attribute access (#12805)
If we have multiple native attribute access operations in succession, we can borrow the temporaries. This avoids an incref and decref. For example, when evaluating x.y.z, we don't need to incref the result of x.y. We need to make sure that the objects from which we borrow values are not freed too early by adding keep_alive ops. This is part of a wider reference counting optimization workstream. All the improvements together produced around 5% performance improvement in the richards benchmark. In carefully constructed microbenchmarks 50+% improvements are possible.
1 parent 7bd6fdd commit 74cfa3d

File tree

8 files changed

+133
-11
lines changed

8 files changed

+133
-11
lines changed

mypyc/codegen/emitfunc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def visit_get_attr(self, op: GetAttr) -> None:
333333
'PyErr_SetString({}, "attribute {} of {} undefined");'.format(
334334
exc_class, repr(op.attr), repr(cl.name)))
335335

336-
if attr_rtype.is_refcounted:
336+
if attr_rtype.is_refcounted and not op.is_borrowed:
337337
if not merged_branch and not always_defined:
338338
self.emitter.emit_line('} else {')
339339
self.emitter.emit_inc_ref(dest, attr_rtype)

mypyc/ir/ops.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,13 +599,14 @@ class GetAttr(RegisterOp):
599599

600600
error_kind = ERR_MAGIC
601601

602-
def __init__(self, obj: Value, attr: str, line: int) -> None:
602+
def __init__(self, obj: Value, attr: str, line: int, *, borrow: bool = False) -> None:
603603
super().__init__(line)
604604
self.obj = obj
605605
self.attr = attr
606606
assert isinstance(obj.type, RInstance), 'Attribute access not supported: %s' % obj.type
607607
self.class_type = obj.type
608608
self.type = obj.type.attr_type(attr)
609+
self.is_borrowed = borrow
609610

610611
def sources(self) -> List[Value]:
611612
return [self.obj]

mypyc/ir/pprint.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ def visit_load_literal(self, op: LoadLiteral) -> str:
7777
return self.format('%r = %s%s', op, prefix, repr(op.value))
7878

7979
def visit_get_attr(self, op: GetAttr) -> str:
80-
return self.format('%r = %r.%s', op, op.obj, op.attr)
80+
if op.is_borrowed:
81+
borrow = 'borrow '
82+
else:
83+
borrow = ''
84+
return self.format('%r = %s%r.%s', op, borrow, op.obj, op.attr)
8185

8286
def visit_set_attr(self, op: SetAttr) -> str:
8387
if op.is_init:

mypyc/irbuild/builder.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ def __init__(self,
141141
# can also do quick lookups.
142142
self.imports: OrderedDict[str, None] = OrderedDict()
143143

144+
self.can_borrow = False
145+
144146
# High-level control
145147

146148
def set_module(self, module_name: str, module_path: str) -> None:
@@ -152,15 +154,23 @@ def set_module(self, module_name: str, module_path: str) -> None:
152154
self.module_path = module_path
153155

154156
@overload
155-
def accept(self, node: Expression) -> Value: ...
157+
def accept(self, node: Expression, *, can_borrow: bool = False) -> Value: ...
156158

157159
@overload
158160
def accept(self, node: Statement) -> None: ...
159161

160-
def accept(self, node: Union[Statement, Expression]) -> Optional[Value]:
161-
"""Transform an expression or a statement."""
162+
def accept(self, node: Union[Statement, Expression], *,
163+
can_borrow: bool = False) -> Optional[Value]:
164+
"""Transform an expression or a statement.
165+
166+
If can_borrow is true, prefer to generate a borrowed reference.
167+
Borrowed references are faster since they don't require reference count
168+
manipulation, but they are only safe to use in specific contexts.
169+
"""
162170
with self.catch_errors(node.line):
163171
if isinstance(node, Expression):
172+
old_can_borrow = self.can_borrow
173+
self.can_borrow = can_borrow
164174
try:
165175
res = node.accept(self.visitor)
166176
res = self.coerce(res, self.node_type(node), node.line)
@@ -170,6 +180,9 @@ def accept(self, node: Union[Statement, Expression]) -> Optional[Value]:
170180
# from causing more downstream trouble.
171181
except UnsupportedException:
172182
res = Register(self.node_type(node))
183+
self.can_borrow = old_can_borrow
184+
if not can_borrow:
185+
self.builder.flush_keep_alives()
173186
return res
174187
else:
175188
try:

mypyc/irbuild/expression.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
Value, Register, TupleGet, TupleSet, BasicBlock, Assign, LoadAddress, RaiseStandardError
2222
)
2323
from mypyc.ir.rtypes import (
24-
RTuple, object_rprimitive, is_none_rprimitive, int_rprimitive, is_int_rprimitive
24+
RTuple, RInstance, object_rprimitive, is_none_rprimitive, int_rprimitive, is_int_rprimitive
2525
)
2626
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD
2727
from mypyc.irbuild.format_str_tokenizer import (
@@ -130,8 +130,19 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
130130
if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports:
131131
return builder.load_module(expr.node.fullname)
132132

133-
obj = builder.accept(expr.expr)
133+
obj_rtype = builder.node_type(expr.expr)
134+
if (isinstance(obj_rtype, RInstance)
135+
and obj_rtype.class_ir.is_ext_class
136+
and obj_rtype.class_ir.has_attr(expr.name)
137+
and not obj_rtype.class_ir.get_method(expr.name)):
138+
# Direct attribute access -> can borrow object
139+
can_borrow = True
140+
else:
141+
can_borrow = False
142+
obj = builder.accept(expr.expr, can_borrow=can_borrow)
143+
134144
rtype = builder.node_type(expr)
145+
135146
# Special case: for named tuples transform attribute access to faster index access.
136147
typ = get_proper_type(builder.types.get(expr.expr))
137148
if isinstance(typ, TupleType) and typ.partial_fallback.type.is_named_tuple:
@@ -142,7 +153,8 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
142153

143154
check_instance_attribute_access_through_class(builder, expr, typ)
144155

145-
return builder.builder.get_attr(obj, expr.name, rtype, expr.line)
156+
borrow = can_borrow and builder.can_borrow
157+
return builder.builder.get_attr(obj, expr.name, rtype, expr.line, borrow=borrow)
146158

147159

148160
def check_instance_attribute_access_through_class(builder: IRBuilder,

mypyc/irbuild/ll_builder.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ def __init__(
105105
self.blocks: List[BasicBlock] = []
106106
# Stack of except handler entry blocks
107107
self.error_handlers: List[Optional[BasicBlock]] = [None]
108+
# Values that we need to keep alive as long as we have borrowed
109+
# temporaries. Use flush_keep_alives() to mark the end of the live range.
110+
self.keep_alives: List[Value] = []
108111

109112
# Basic operations
110113

@@ -145,6 +148,11 @@ def self(self) -> Register:
145148
"""
146149
return self.args[0]
147150

151+
def flush_keep_alives(self) -> None:
152+
if self.keep_alives:
153+
self.add(KeepAlive(self.keep_alives[:]))
154+
self.keep_alives = []
155+
148156
# Type conversions
149157

150158
def box(self, src: Value) -> Value:
@@ -219,11 +227,14 @@ def coerce_nullable(self, src: Value, target_type: RType, line: int) -> Value:
219227

220228
# Attribute access
221229

222-
def get_attr(self, obj: Value, attr: str, result_type: RType, line: int) -> Value:
230+
def get_attr(self, obj: Value, attr: str, result_type: RType, line: int, *,
231+
borrow: bool = False) -> Value:
223232
"""Get a native or Python attribute of an object."""
224233
if (isinstance(obj.type, RInstance) and obj.type.class_ir.is_ext_class
225234
and obj.type.class_ir.has_attr(attr)):
226-
return self.add(GetAttr(obj, attr, line))
235+
if borrow:
236+
self.keep_alives.append(obj)
237+
return self.add(GetAttr(obj, attr, line, borrow=borrow))
227238
elif isinstance(obj.type, RUnion):
228239
return self.union_get_attr(obj, obj.type, attr, result_type, line)
229240
else:

mypyc/test-data/irbuild-classes.test

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,3 +1240,51 @@ L0:
12401240
r0 = ''
12411241
__mypyc_self__.s = r0
12421242
return 1
1243+
1244+
[case testBorrowAttribute]
1245+
def f(d: D) -> int:
1246+
return d.c.x
1247+
1248+
class C:
1249+
x: int
1250+
class D:
1251+
c: C
1252+
[out]
1253+
def f(d):
1254+
d :: __main__.D
1255+
r0 :: __main__.C
1256+
r1 :: int
1257+
L0:
1258+
r0 = borrow d.c
1259+
r1 = r0.x
1260+
keep_alive d
1261+
return r1
1262+
1263+
[case testNoBorrowOverPropertyAccess]
1264+
class C:
1265+
d: D
1266+
class D:
1267+
@property
1268+
def e(self) -> E:
1269+
return E()
1270+
class E:
1271+
x: int
1272+
def f(c: C) -> int:
1273+
return c.d.e.x
1274+
[out]
1275+
def D.e(self):
1276+
self :: __main__.D
1277+
r0 :: __main__.E
1278+
L0:
1279+
r0 = E()
1280+
return r0
1281+
def f(c):
1282+
c :: __main__.C
1283+
r0 :: __main__.D
1284+
r1 :: __main__.E
1285+
r2 :: int
1286+
L0:
1287+
r0 = c.d
1288+
r1 = r0.e
1289+
r2 = r1.x
1290+
return r2

mypyc/test-data/refcount.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,3 +917,36 @@ L0:
917917
r5 = unbox(int, r4)
918918
dec_ref r4
919919
return r5
920+
921+
[case testBorrowAttribute]
922+
def g() -> int:
923+
d = D()
924+
return d.c.x
925+
926+
def f(d: D) -> int:
927+
return d.c.x
928+
929+
class C:
930+
x: int
931+
class D:
932+
c: C
933+
[out]
934+
def g():
935+
r0, d :: __main__.D
936+
r1 :: __main__.C
937+
r2 :: int
938+
L0:
939+
r0 = D()
940+
d = r0
941+
r1 = borrow d.c
942+
r2 = r1.x
943+
dec_ref d
944+
return r2
945+
def f(d):
946+
d :: __main__.D
947+
r0 :: __main__.C
948+
r1 :: int
949+
L0:
950+
r0 = borrow d.c
951+
r1 = r0.x
952+
return r1

0 commit comments

Comments
 (0)