Skip to content

Commit 3c04092

Browse files
authored
[mypyc] Borrow even more things (#12817)
Borrow operands of tagged integer operations to reduce the number of incref/decref operations (when it's safe to do so). Borrow the results in list get item operations, similar to what we've been doing with get attribute operations.
1 parent c986e54 commit 3c04092

17 files changed

+587
-123
lines changed

mypyc/irbuild/ast_helpers.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""IRBuilder AST transform helpers shared between expressions and statements.
2+
3+
Shared code that is tightly coupled to mypy ASTs can be put here instead of
4+
making mypyc.irbuild.builder larger.
5+
"""
6+
7+
from mypy.nodes import (
8+
Expression, MemberExpr, Var, IntExpr, FloatExpr, StrExpr, BytesExpr, NameExpr, OpExpr,
9+
UnaryExpr, ComparisonExpr, LDEF
10+
)
11+
from mypyc.ir.ops import BasicBlock
12+
from mypyc.ir.rtypes import is_tagged
13+
from mypyc.irbuild.builder import IRBuilder
14+
from mypyc.irbuild.constant_fold import constant_fold_expr
15+
16+
17+
def process_conditional(self: IRBuilder, e: Expression, true: BasicBlock,
18+
false: BasicBlock) -> None:
19+
if isinstance(e, OpExpr) and e.op in ['and', 'or']:
20+
if e.op == 'and':
21+
# Short circuit 'and' in a conditional context.
22+
new = BasicBlock()
23+
process_conditional(self, e.left, new, false)
24+
self.activate_block(new)
25+
process_conditional(self, e.right, true, false)
26+
else:
27+
# Short circuit 'or' in a conditional context.
28+
new = BasicBlock()
29+
process_conditional(self, e.left, true, new)
30+
self.activate_block(new)
31+
process_conditional(self, e.right, true, false)
32+
elif isinstance(e, UnaryExpr) and e.op == 'not':
33+
process_conditional(self, e.expr, false, true)
34+
else:
35+
res = maybe_process_conditional_comparison(self, e, true, false)
36+
if res:
37+
return
38+
# Catch-all for arbitrary expressions.
39+
reg = self.accept(e)
40+
self.add_bool_branch(reg, true, false)
41+
42+
43+
def maybe_process_conditional_comparison(self: IRBuilder,
44+
e: Expression,
45+
true: BasicBlock,
46+
false: BasicBlock) -> bool:
47+
"""Transform simple tagged integer comparisons in a conditional context.
48+
49+
Return True if the operation is supported (and was transformed). Otherwise,
50+
do nothing and return False.
51+
52+
Args:
53+
e: Arbitrary expression
54+
true: Branch target if comparison is true
55+
false: Branch target if comparison is false
56+
"""
57+
if not isinstance(e, ComparisonExpr) or len(e.operands) != 2:
58+
return False
59+
ltype = self.node_type(e.operands[0])
60+
rtype = self.node_type(e.operands[1])
61+
if not is_tagged(ltype) or not is_tagged(rtype):
62+
return False
63+
op = e.operators[0]
64+
if op not in ('==', '!=', '<', '<=', '>', '>='):
65+
return False
66+
left_expr = e.operands[0]
67+
right_expr = e.operands[1]
68+
borrow_left = is_borrow_friendly_expr(self, right_expr)
69+
left = self.accept(left_expr, can_borrow=borrow_left)
70+
right = self.accept(right_expr, can_borrow=True)
71+
# "left op right" for two tagged integers
72+
self.builder.compare_tagged_condition(left, right, op, true, false, e.line)
73+
return True
74+
75+
76+
def is_borrow_friendly_expr(self: IRBuilder, expr: Expression) -> bool:
77+
"""Can the result of the expression borrowed temporarily?
78+
79+
Borrowing means keeping a reference without incrementing the reference count.
80+
"""
81+
if isinstance(expr, (IntExpr, FloatExpr, StrExpr, BytesExpr)):
82+
# Literals are immortal and can always be borrowed
83+
return True
84+
if (isinstance(expr, (UnaryExpr, OpExpr, NameExpr, MemberExpr)) and
85+
constant_fold_expr(self, expr) is not None):
86+
# Literal expressions are similar to literals
87+
return True
88+
if isinstance(expr, NameExpr):
89+
if isinstance(expr.node, Var) and expr.kind == LDEF:
90+
# Local variable reference can be borrowed
91+
return True
92+
if isinstance(expr, MemberExpr) and self.is_native_attr_ref(expr):
93+
return True
94+
return False

mypyc/irbuild/builder.py

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
from mypyc.irbuild.prepare import RegisterImplInfo
1616
from typing import Callable, Dict, List, Tuple, Optional, Union, Sequence, Set, Any, Iterator
17-
from typing_extensions import overload
17+
from typing_extensions import overload, Final
1818
from mypy.backports import OrderedDict
1919

2020
from mypy.build import Graph
2121
from mypy.nodes import (
2222
MypyFile, SymbolNode, Statement, OpExpr, IntExpr, NameExpr, LDEF, Var, UnaryExpr,
2323
CallExpr, IndexExpr, Expression, MemberExpr, RefExpr, Lvalue, TupleExpr,
24-
TypeInfo, Decorator, OverloadedFuncDef, StarExpr, ComparisonExpr, GDEF,
25-
ArgKind, ARG_POS, ARG_NAMED, FuncDef,
24+
TypeInfo, Decorator, OverloadedFuncDef, StarExpr,
25+
GDEF, ArgKind, ARG_POS, ARG_NAMED, FuncDef,
2626
)
2727
from mypy.types import (
2828
Type, Instance, TupleType, UninhabitedType, get_proper_type
@@ -40,7 +40,7 @@
4040
from mypyc.ir.rtypes import (
4141
RType, RTuple, RInstance, c_int_rprimitive, int_rprimitive, dict_rprimitive,
4242
none_rprimitive, is_none_rprimitive, object_rprimitive, is_object_rprimitive,
43-
str_rprimitive, is_tagged, is_list_rprimitive, is_tuple_rprimitive, c_pyssize_t_rprimitive
43+
str_rprimitive, is_list_rprimitive, is_tuple_rprimitive, c_pyssize_t_rprimitive
4444
)
4545
from mypyc.ir.func_ir import FuncIR, INVALID_FUNC_DEF, RuntimeArg, FuncSignature, FuncDecl
4646
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
@@ -67,6 +67,11 @@
6767
from mypyc.irbuild.util import is_constant
6868

6969

70+
# These int binary operations can borrow their operands safely, since the
71+
# primitives take this into consideration.
72+
int_borrow_friendly_op: Final = {'+', '-', '==', '!=', '<', '<=', '>', '>='}
73+
74+
7075
class IRVisitor(ExpressionVisitor[Value], StatementVisitor[None]):
7176
pass
7277

@@ -287,7 +292,7 @@ def gen_method_call(self,
287292
arg_kinds: Optional[List[ArgKind]] = None,
288293
arg_names: Optional[List[Optional[str]]] = None) -> Value:
289294
return self.builder.gen_method_call(
290-
base, name, arg_values, result_type, line, arg_kinds, arg_names
295+
base, name, arg_values, result_type, line, arg_kinds, arg_names, self.can_borrow
291296
)
292297

293298
def load_module(self, name: str) -> Value:
@@ -515,7 +520,7 @@ def get_assignment_target(self, lvalue: Lvalue,
515520
# Attribute assignment x.y = e
516521
can_borrow = self.is_native_attr_ref(lvalue)
517522
obj = self.accept(lvalue.expr, can_borrow=can_borrow)
518-
return AssignmentTargetAttr(obj, lvalue.name)
523+
return AssignmentTargetAttr(obj, lvalue.name, can_borrow=can_borrow)
519524
elif isinstance(lvalue, TupleExpr):
520525
# Multiple assignment a, ..., b = e
521526
star_idx: Optional[int] = None
@@ -535,7 +540,10 @@ def get_assignment_target(self, lvalue: Lvalue,
535540

536541
assert False, 'Unsupported lvalue: %r' % lvalue
537542

538-
def read(self, target: Union[Value, AssignmentTarget], line: int = -1) -> Value:
543+
def read(self,
544+
target: Union[Value, AssignmentTarget],
545+
line: int = -1,
546+
can_borrow: bool = False) -> Value:
539547
if isinstance(target, Value):
540548
return target
541549
if isinstance(target, AssignmentTargetRegister):
@@ -548,7 +556,8 @@ def read(self, target: Union[Value, AssignmentTarget], line: int = -1) -> Value:
548556
assert False, target.base.type
549557
if isinstance(target, AssignmentTargetAttr):
550558
if isinstance(target.obj.type, RInstance) and target.obj.type.class_ir.is_ext_class:
551-
return self.add(GetAttr(target.obj, target.attr, line))
559+
borrow = can_borrow and target.can_borrow
560+
return self.add(GetAttr(target.obj, target.attr, line, borrow=borrow))
552561
else:
553562
return self.py_get_attr(target.obj, target.attr, line)
554563

@@ -915,61 +924,6 @@ def shortcircuit_expr(self, expr: OpExpr) -> Value:
915924
expr.line
916925
)
917926

918-
# Conditional expressions
919-
920-
def process_conditional(self, e: Expression, true: BasicBlock, false: BasicBlock) -> None:
921-
if isinstance(e, OpExpr) and e.op in ['and', 'or']:
922-
if e.op == 'and':
923-
# Short circuit 'and' in a conditional context.
924-
new = BasicBlock()
925-
self.process_conditional(e.left, new, false)
926-
self.activate_block(new)
927-
self.process_conditional(e.right, true, false)
928-
else:
929-
# Short circuit 'or' in a conditional context.
930-
new = BasicBlock()
931-
self.process_conditional(e.left, true, new)
932-
self.activate_block(new)
933-
self.process_conditional(e.right, true, false)
934-
elif isinstance(e, UnaryExpr) and e.op == 'not':
935-
self.process_conditional(e.expr, false, true)
936-
else:
937-
res = self.maybe_process_conditional_comparison(e, true, false)
938-
if res:
939-
return
940-
# Catch-all for arbitrary expressions.
941-
reg = self.accept(e)
942-
self.add_bool_branch(reg, true, false)
943-
944-
def maybe_process_conditional_comparison(self,
945-
e: Expression,
946-
true: BasicBlock,
947-
false: BasicBlock) -> bool:
948-
"""Transform simple tagged integer comparisons in a conditional context.
949-
950-
Return True if the operation is supported (and was transformed). Otherwise,
951-
do nothing and return False.
952-
953-
Args:
954-
e: Arbitrary expression
955-
true: Branch target if comparison is true
956-
false: Branch target if comparison is false
957-
"""
958-
if not isinstance(e, ComparisonExpr) or len(e.operands) != 2:
959-
return False
960-
ltype = self.node_type(e.operands[0])
961-
rtype = self.node_type(e.operands[1])
962-
if not is_tagged(ltype) or not is_tagged(rtype):
963-
return False
964-
op = e.operators[0]
965-
if op not in ('==', '!=', '<', '<=', '>', '>='):
966-
return False
967-
left = self.accept(e.operands[0])
968-
right = self.accept(e.operands[1])
969-
# "left op right" for two tagged integers
970-
self.builder.compare_tagged_condition(left, right, op, true, false, e.line)
971-
return True
972-
973927
# Basic helpers
974928

975929
def flatten_classes(self, arg: Union[RefExpr, TupleExpr]) -> Optional[List[ClassIR]]:

mypyc/irbuild/expression.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@
4040
from mypyc.primitives.str_ops import str_slice_op
4141
from mypyc.primitives.int_ops import int_comparison_op_mapping
4242
from mypyc.irbuild.specialize import apply_function_specialization, apply_method_specialization
43-
from mypyc.irbuild.builder import IRBuilder
43+
from mypyc.irbuild.builder import IRBuilder, int_borrow_friendly_op
4444
from mypyc.irbuild.for_helpers import (
4545
translate_list_comprehension, translate_set_comprehension,
4646
comprehension_helper
4747
)
4848
from mypyc.irbuild.constant_fold import constant_fold_expr
49+
from mypyc.irbuild.ast_helpers import is_borrow_friendly_expr, process_conditional
4950

5051

5152
# Name and attribute references
@@ -404,6 +405,15 @@ def transform_op_expr(builder: IRBuilder, expr: OpExpr) -> Value:
404405
if folded:
405406
return folded
406407

408+
# Special case some int ops to allow borrowing operands.
409+
if (is_int_rprimitive(builder.node_type(expr.left))
410+
and is_int_rprimitive(builder.node_type(expr.right))):
411+
if expr.op in int_borrow_friendly_op:
412+
borrow_left = is_borrow_friendly_expr(builder, expr.right)
413+
left = builder.accept(expr.left, can_borrow=borrow_left)
414+
right = builder.accept(expr.right, can_borrow=True)
415+
return builder.binary_op(left, right, expr.op, expr.line)
416+
407417
return builder.binary_op(
408418
builder.accept(expr.left), builder.accept(expr.right), expr.op, expr.line
409419
)
@@ -430,26 +440,6 @@ def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
430440
base, '__getitem__', [index_reg], builder.node_type(expr), expr.line)
431441

432442

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-
453443
def try_constant_fold(builder: IRBuilder, expr: Expression) -> Optional[Value]:
454444
"""Return the constant value of an expression if possible.
455445
@@ -504,7 +494,7 @@ def try_gen_slice_op(builder: IRBuilder, base: Value, index: SliceExpr) -> Optio
504494
def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Value:
505495
if_body, else_body, next_block = BasicBlock(), BasicBlock(), BasicBlock()
506496

507-
builder.process_conditional(expr.cond, if_body, else_body)
497+
process_conditional(builder, expr.cond, if_body, else_body)
508498
expr_type = builder.node_type(expr)
509499
# Having actual Phi nodes would be really nice here!
510500
target = Register(expr_type)
@@ -577,11 +567,22 @@ def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value:
577567
else:
578568
return builder.true()
579569

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')
570+
if len(e.operators) == 1:
571+
# Special some common simple cases
572+
if first_op in ('is', 'is not'):
573+
right_expr = e.operands[1]
574+
if isinstance(right_expr, NameExpr) and right_expr.fullname == 'builtins.None':
575+
# Special case 'is None' / 'is not None'.
576+
return translate_is_none(builder, e.operands[0], negated=first_op != 'is')
577+
left_expr = e.operands[0]
578+
if is_int_rprimitive(builder.node_type(left_expr)):
579+
right_expr = e.operands[1]
580+
if is_int_rprimitive(builder.node_type(right_expr)):
581+
if first_op in int_borrow_friendly_op:
582+
borrow_left = is_borrow_friendly_expr(builder, right_expr)
583+
left = builder.accept(left_expr, can_borrow=borrow_left)
584+
right = builder.accept(right_expr, can_borrow=True)
585+
return builder.compare_tagged(left, right, first_op, e.line)
585586

586587
# TODO: Don't produce an expression when used in conditional context
587588
# All of the trickiness here is due to support for chained conditionals

0 commit comments

Comments
 (0)