Skip to content

Commit f71dba7

Browse files
authored
[mypyc] Borrow operands of several primitives (#12810)
Borrow an operand such as `x.y` (attribute of a native class) in various contexts when it's safe to do so. This reduces the number of incref/decref operations we need to perform. This continues work started in #12805. These cases now support borrowing (for some subexpressions, in some contexts): * `x.y is None` * Cast source value * `len(x.y)` (if the operand is a list) * `isinstance(x.y, C)` * `x.y[a.b]` * `x.y.z = 1`
1 parent 8e7e817 commit f71dba7

14 files changed

+539
-143
lines changed

mypyc/ir/ops.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,15 +775,18 @@ class Cast(RegisterOp):
775775

776776
error_kind = ERR_MAGIC
777777

778-
def __init__(self, src: Value, typ: RType, line: int) -> None:
778+
def __init__(self, src: Value, typ: RType, line: int, *, borrow: bool = False) -> None:
779779
super().__init__(line)
780780
self.src = src
781781
self.type = typ
782+
self.is_borrowed = borrow
782783

783784
def sources(self) -> List[Value]:
784785
return [self.src]
785786

786787
def stolen(self) -> List[Value]:
788+
if self.is_borrowed:
789+
return []
787790
return [self.src]
788791

789792
def accept(self, visitor: 'OpVisitor[T]') -> T:

mypyc/ir/pprint.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,12 @@ 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 = %s%r.%s', op, self.borrow_prefix(op), op.obj, op.attr)
81+
82+
def borrow_prefix(self, op: Op) -> str:
8083
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)
84+
return 'borrow '
85+
return ''
8586

8687
def visit_set_attr(self, op: SetAttr) -> str:
8788
if op.is_init:
@@ -142,7 +143,7 @@ def visit_method_call(self, op: MethodCall) -> str:
142143
return s
143144

144145
def visit_cast(self, op: Cast) -> str:
145-
return self.format('%r = cast(%s, %r)', op, op.type, op.src)
146+
return self.format('%r = %scast(%s, %r)', op, self.borrow_prefix(op), op.type, op.src)
146147

147148
def visit_box(self, op: Box) -> str:
148149
return self.format('%r = box(%s, %r)', op, op.src.type, op.src)

mypyc/irbuild/builder.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def accept(self, node: Union[Statement, Expression], *,
182182
res = Register(self.node_type(node))
183183
self.can_borrow = old_can_borrow
184184
if not can_borrow:
185-
self.builder.flush_keep_alives()
185+
self.flush_keep_alives()
186186
return res
187187
else:
188188
try:
@@ -191,6 +191,9 @@ def accept(self, node: Union[Statement, Expression], *,
191191
pass
192192
return None
193193

194+
def flush_keep_alives(self) -> None:
195+
self.builder.flush_keep_alives()
196+
194197
# Pass through methods for the most common low-level builder ops, for convenience.
195198

196199
def add(self, op: Op) -> Value:
@@ -234,7 +237,7 @@ def binary_op(self, lreg: Value, rreg: Value, expr_op: str, line: int) -> Value:
234237
return self.builder.binary_op(lreg, rreg, expr_op, line)
235238

236239
def coerce(self, src: Value, target_type: RType, line: int, force: bool = False) -> Value:
237-
return self.builder.coerce(src, target_type, line, force)
240+
return self.builder.coerce(src, target_type, line, force, can_borrow=self.can_borrow)
238241

239242
def none_object(self) -> Value:
240243
return self.builder.none_object()
@@ -510,7 +513,8 @@ def get_assignment_target(self, lvalue: Lvalue,
510513
return AssignmentTargetIndex(base, index)
511514
elif isinstance(lvalue, MemberExpr):
512515
# Attribute assignment x.y = e
513-
obj = self.accept(lvalue.expr)
516+
can_borrow = self.is_native_attr_ref(lvalue)
517+
obj = self.accept(lvalue.expr, can_borrow=can_borrow)
514518
return AssignmentTargetAttr(obj, lvalue.name)
515519
elif isinstance(lvalue, TupleExpr):
516520
# Multiple assignment a, ..., b = e
@@ -1176,6 +1180,14 @@ def load_module_attr_by_fullname(self, fullname: str, line: int) -> Value:
11761180
left = self.load_module(module)
11771181
return self.py_get_attr(left, name, line)
11781182

1183+
def is_native_attr_ref(self, expr: MemberExpr) -> bool:
1184+
"""Is expr a direct reference to a native (struct) attribute of an instance?"""
1185+
obj_rtype = self.node_type(expr.expr)
1186+
return (isinstance(obj_rtype, RInstance)
1187+
and obj_rtype.class_ir.is_ext_class
1188+
and obj_rtype.class_ir.has_attr(expr.name)
1189+
and not obj_rtype.class_ir.get_method(expr.name))
1190+
11791191
# Lacks a good type because there wasn't a reasonable type in 3.5 :(
11801192
def catch_errors(self, line: int) -> Any:
11811193
return catch_errors(self.module_path, line)

mypyc/irbuild/expression.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
Value, Register, TupleGet, TupleSet, BasicBlock, Assign, LoadAddress, RaiseStandardError
2222
)
2323
from mypyc.ir.rtypes import (
24-
RTuple, RInstance, object_rprimitive, is_none_rprimitive, int_rprimitive, is_int_rprimitive
24+
RTuple, object_rprimitive, is_none_rprimitive, int_rprimitive, is_int_rprimitive,
25+
is_list_rprimitive
2526
)
2627
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD
2728
from mypyc.irbuild.format_str_tokenizer import (
@@ -130,17 +131,8 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
130131
if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports:
131132
return builder.load_module(expr.node.fullname)
132133

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
134+
can_borrow = builder.is_native_attr_ref(expr)
142135
obj = builder.accept(expr.expr, can_borrow=can_borrow)
143-
144136
rtype = builder.node_type(expr)
145137

146138
# Special case: for named tuples transform attribute access to faster index access.
@@ -418,8 +410,12 @@ def transform_op_expr(builder: IRBuilder, expr: OpExpr) -> Value:
418410

419411

420412
def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
421-
base = builder.accept(expr.base)
422413
index = expr.index
414+
base_type = builder.node_type(expr.base)
415+
is_list = is_list_rprimitive(base_type)
416+
can_borrow_base = is_list and is_borrow_friendly_expr(builder, index)
417+
418+
base = builder.accept(expr.base, can_borrow=can_borrow_base)
423419

424420
if isinstance(base.type, RTuple) and isinstance(index, IntExpr):
425421
return builder.add(TupleGet(base, index.value, expr.line))
@@ -429,11 +425,31 @@ def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
429425
if value:
430426
return value
431427

432-
index_reg = builder.accept(expr.index)
428+
index_reg = builder.accept(expr.index, can_borrow=is_list)
433429
return builder.gen_method_call(
434430
base, '__getitem__', [index_reg], builder.node_type(expr), expr.line)
435431

436432

433+
def is_borrow_friendly_expr(builder: IRBuilder, expr: Expression) -> bool:
434+
"""Can the result of the expression borrowed temporarily?
435+
436+
Borrowing means keeping a reference without incrementing the reference count.
437+
"""
438+
if isinstance(expr, (IntExpr, FloatExpr, StrExpr, BytesExpr)):
439+
# Literals are immportal and can always be borrowed
440+
return True
441+
if isinstance(expr, (UnaryExpr, OpExpr)) and constant_fold_expr(builder, expr) is not None:
442+
# Literal expressions are similar to literals
443+
return True
444+
if isinstance(expr, NameExpr):
445+
if isinstance(expr.node, Var) and expr.kind == LDEF:
446+
# Local variable reference can be borrowed
447+
return True
448+
if isinstance(expr, MemberExpr) and builder.is_native_attr_ref(expr):
449+
return True
450+
return False
451+
452+
437453
def try_constant_fold(builder: IRBuilder, expr: Expression) -> Optional[Value]:
438454
"""Return the constant value of an expression if possible.
439455
@@ -513,7 +529,8 @@ def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Val
513529
def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value:
514530
# x in (...)/[...]
515531
# x not in (...)/[...]
516-
if (e.operators[0] in ['in', 'not in']
532+
first_op = e.operators[0]
533+
if (first_op in ['in', 'not in']
517534
and len(e.operators) == 1
518535
and isinstance(e.operands[1], (TupleExpr, ListExpr))):
519536
items = e.operands[1].items
@@ -560,6 +577,12 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value:
560577
else:
561578
return builder.true()
562579

580+
if first_op in ('is', 'is not') and len(e.operators) == 1:
581+
right = e.operands[1]
582+
if isinstance(right, NameExpr) and right.fullname == 'builtins.None':
583+
# Special case 'is None' / 'is not None'.
584+
return translate_is_none(builder, e.operands[0], negated=first_op != 'is')
585+
563586
# TODO: Don't produce an expression when used in conditional context
564587
# All of the trickiness here is due to support for chained conditionals
565588
# (`e1 < e2 > e3`, etc). `e1 < e2 > e3` is approximately equivalent to
@@ -584,6 +607,11 @@ def go(i: int, prev: Value) -> Value:
584607
return go(0, builder.accept(e.operands[0]))
585608

586609

610+
def translate_is_none(builder: IRBuilder, expr: Expression, negated: bool) -> Value:
611+
v = builder.accept(expr, can_borrow=True)
612+
return builder.binary_op(v, builder.none_object(), 'is not' if negated else 'is', expr.line)
613+
614+
587615
def transform_basic_comparison(builder: IRBuilder,
588616
op: str,
589617
left: Value,

mypyc/irbuild/ll_builder.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,17 @@ def box(self, src: Value) -> Value:
163163
else:
164164
return src
165165

166-
def unbox_or_cast(self, src: Value, target_type: RType, line: int) -> Value:
166+
def unbox_or_cast(self, src: Value, target_type: RType, line: int, *,
167+
can_borrow: bool = False) -> Value:
167168
if target_type.is_unboxed:
168169
return self.add(Unbox(src, target_type, line))
169170
else:
170-
return self.add(Cast(src, target_type, line))
171+
if can_borrow:
172+
self.keep_alives.append(src)
173+
return self.add(Cast(src, target_type, line, borrow=can_borrow))
171174

172-
def coerce(self, src: Value, target_type: RType, line: int, force: bool = False) -> Value:
175+
def coerce(self, src: Value, target_type: RType, line: int, force: bool = False, *,
176+
can_borrow: bool = False) -> Value:
173177
"""Generate a coercion/cast from one type to other (only if needed).
174178
175179
For example, int -> object boxes the source int; int -> int emits nothing;
@@ -190,7 +194,7 @@ def coerce(self, src: Value, target_type: RType, line: int, force: bool = False)
190194
return self.unbox_or_cast(tmp, target_type, line)
191195
if ((not src.type.is_unboxed and target_type.is_unboxed)
192196
or not is_subtype(src.type, target_type)):
193-
return self.unbox_or_cast(src, target_type, line)
197+
return self.unbox_or_cast(src, target_type, line, can_borrow=can_borrow)
194198
elif force:
195199
tmp = Register(target_type)
196200
self.add(Assign(tmp, src))

mypyc/irbuild/specialize.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626
from mypyc.ir.rtypes import (
2727
RType, RTuple, str_rprimitive, list_rprimitive, dict_rprimitive, set_rprimitive,
28-
bool_rprimitive, c_int_rprimitive, is_dict_rprimitive
28+
bool_rprimitive, c_int_rprimitive, is_dict_rprimitive, is_list_rprimitive
2929
)
3030
from mypyc.irbuild.format_str_tokenizer import (
3131
tokenizer_format_call, join_formatted_strings, convert_format_expr_to_str, FormatOp
@@ -113,14 +113,19 @@ def translate_len(
113113
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
114114
if (len(expr.args) == 1
115115
and expr.arg_kinds == [ARG_POS]):
116-
expr_rtype = builder.node_type(expr.args[0])
116+
arg = expr.args[0]
117+
expr_rtype = builder.node_type(arg)
117118
if isinstance(expr_rtype, RTuple):
118119
# len() of fixed-length tuple can be trivially determined
119120
# statically, though we still need to evaluate it.
120-
builder.accept(expr.args[0])
121+
builder.accept(arg)
121122
return Integer(len(expr_rtype.types))
122123
else:
123-
obj = builder.accept(expr.args[0])
124+
if is_list_rprimitive(builder.node_type(arg)):
125+
borrow = True
126+
else:
127+
borrow = False
128+
obj = builder.accept(arg, can_borrow=borrow)
124129
return builder.builtin_len(obj, expr.line)
125130
return None
126131

@@ -429,7 +434,12 @@ def translate_isinstance(builder: IRBuilder, expr: CallExpr, callee: RefExpr) ->
429434

430435
irs = builder.flatten_classes(expr.args[1])
431436
if irs is not None:
432-
return builder.builder.isinstance_helper(builder.accept(expr.args[0]), irs, expr.line)
437+
can_borrow = all(ir.is_ext_class
438+
and not ir.inherits_python
439+
and not ir.allow_interpreted_subclasses
440+
for ir in irs)
441+
obj = builder.accept(expr.args[0], can_borrow=can_borrow)
442+
return builder.builder.isinstance_helper(obj, irs, expr.line)
433443
return None
434444

435445

mypyc/irbuild/statement.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ def transform_expression_stmt(builder: IRBuilder, stmt: ExpressionStmt) -> None:
5858
if isinstance(stmt.expr, StrExpr):
5959
# Docstring. Ignore
6060
return
61-
# ExpressionStmts do not need to be coerced like other Expressions.
61+
# ExpressionStmts do not need to be coerced like other Expressions, so we shouldn't
62+
# call builder.accept here.
6263
stmt.expr.accept(builder.visitor)
64+
builder.flush_keep_alives()
6365

6466

6567
def transform_return_stmt(builder: IRBuilder, stmt: ReturnStmt) -> None:
@@ -107,6 +109,7 @@ def transform_assignment_stmt(builder: IRBuilder, stmt: AssignmentStmt) -> None:
107109
for lvalue in lvalues:
108110
target = builder.get_assignment_target(lvalue)
109111
builder.assign(target, rvalue_reg, line)
112+
builder.flush_keep_alives()
110113

111114

112115
def is_simple_lvalue(expr: Expression) -> bool:

mypyc/test-data/exceptions.test

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,31 +75,28 @@ def f(x):
7575
r1 :: bit
7676
r2 :: __main__.A
7777
r3 :: object
78-
r4, r5 :: bit
79-
r6 :: int
78+
r4 :: bit
79+
r5 :: int
8080
L0:
81-
r0 = box(None, 1)
81+
r0 = load_address _Py_NoneStruct
8282
r1 = x == r0
8383
if r1 goto L1 else goto L2 :: bool
8484
L1:
8585
return 2
8686
L2:
87-
inc_ref x
88-
r2 = cast(__main__.A, x)
87+
r2 = borrow cast(__main__.A, x)
8988
if is_error(r2) goto L6 (error at f:8) else goto L3
9089
L3:
91-
r3 = box(None, 1)
92-
r4 = r2 == r3
93-
dec_ref r2
94-
r5 = r4 ^ 1
95-
if r5 goto L4 else goto L5 :: bool
90+
r3 = load_address _Py_NoneStruct
91+
r4 = r2 != r3
92+
if r4 goto L4 else goto L5 :: bool
9693
L4:
9794
return 4
9895
L5:
9996
return 6
10097
L6:
101-
r6 = <error> :: int
102-
return r6
98+
r5 = <error> :: int
99+
return r5
103100

104101
[case testListSum]
105102
from typing import List
@@ -518,4 +515,3 @@ L13:
518515
L14:
519516
dec_ref r9
520517
goto L8
521-

mypyc/test-data/irbuild-classes.test

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,22 @@ def Node.length(self):
116116
self :: __main__.Node
117117
r0 :: union[__main__.Node, None]
118118
r1 :: object
119-
r2, r3 :: bit
120-
r4 :: union[__main__.Node, None]
121-
r5 :: __main__.Node
122-
r6, r7 :: int
119+
r2 :: bit
120+
r3 :: union[__main__.Node, None]
121+
r4 :: __main__.Node
122+
r5, r6 :: int
123123
L0:
124-
r0 = self.next
125-
r1 = box(None, 1)
126-
r2 = r0 == r1
127-
r3 = r2 ^ 1
128-
if r3 goto L1 else goto L2 :: bool
124+
r0 = borrow self.next
125+
r1 = load_address _Py_NoneStruct
126+
r2 = r0 != r1
127+
keep_alive self
128+
if r2 goto L1 else goto L2 :: bool
129129
L1:
130-
r4 = self.next
131-
r5 = cast(__main__.Node, r4)
132-
r6 = r5.length()
133-
r7 = CPyTagged_Add(2, r6)
134-
return r7
130+
r3 = self.next
131+
r4 = cast(__main__.Node, r3)
132+
r5 = r4.length()
133+
r6 = CPyTagged_Add(2, r5)
134+
return r6
135135
L2:
136136
return 2
137137

0 commit comments

Comments
 (0)