Skip to content

Commit 4f872d7

Browse files
[mypyc] feat: cache len for container creation from expressions with length known at compile time (#19503)
Currently, if a user uses an immutable type as the sequence input for a for loop, the length is checked once at each iteration which, while necessary for some container types such as list and dictionaries, is not necessary for iterating over immutable types tuple, str, and bytes. This PR modifies the codebase such that the length is only checked at the first iteration, and reused from there. Also, in cases where a simple genexp is the input argument for a tuple, the length is currently checked one additional time before entering the iteration (this is done to determine how to size the new tuple). In those cases, we don't even need a length check at the first iteration step, and can reuse the result of that first `len` call (or compile-time determined constant) instead. Lastly, in cases where a tuple is created from a genexp and the length of the genexp is knowable at compile time, this PR replaces PyList_AsTuple with the tuple constructor fast-path.
1 parent ccd290c commit 4f872d7

File tree

6 files changed

+529
-382
lines changed

6 files changed

+529
-382
lines changed

mypyc/irbuild/for_helpers.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@
1111

1212
from mypy.nodes import (
1313
ARG_POS,
14+
BytesExpr,
1415
CallExpr,
1516
DictionaryComprehension,
1617
Expression,
1718
GeneratorExpr,
19+
ListExpr,
1820
Lvalue,
1921
MemberExpr,
2022
NameExpr,
2123
RefExpr,
2224
SetExpr,
25+
StarExpr,
26+
StrExpr,
2327
TupleExpr,
2428
TypeAlias,
29+
Var,
2530
)
2631
from mypyc.ir.ops import (
2732
ERR_NEVER,
@@ -152,6 +157,7 @@ def for_loop_helper_with_index(
152157
expr_reg: Value,
153158
body_insts: Callable[[Value], None],
154159
line: int,
160+
length: Value,
155161
) -> None:
156162
"""Generate IR for a sequence iteration.
157163
@@ -173,7 +179,7 @@ def for_loop_helper_with_index(
173179
condition_block = BasicBlock()
174180

175181
for_gen = ForSequence(builder, index, body_block, exit_block, line, False)
176-
for_gen.init(expr_reg, target_type, reverse=False)
182+
for_gen.init(expr_reg, target_type, reverse=False, length=length)
177183

178184
builder.push_loop_stack(step_block, exit_block)
179185

@@ -227,15 +233,17 @@ def sequence_from_generator_preallocate_helper(
227233
rtype = builder.node_type(gen.sequences[0])
228234
if is_sequence_rprimitive(rtype):
229235
sequence = builder.accept(gen.sequences[0])
230-
length = builder.builder.builtin_len(sequence, gen.line, use_pyssize_t=True)
236+
length = get_expr_length_value(
237+
builder, gen.sequences[0], sequence, gen.line, use_pyssize_t=True
238+
)
231239
target_op = empty_op_llbuilder(length, gen.line)
232240

233241
def set_item(item_index: Value) -> None:
234242
e = builder.accept(gen.left_expr)
235243
builder.call_c(set_item_op, [target_op, item_index, e], gen.line)
236244

237245
for_loop_helper_with_index(
238-
builder, gen.indices[0], gen.sequences[0], sequence, set_item, gen.line
246+
builder, gen.indices[0], gen.sequences[0], sequence, set_item, gen.line, length
239247
)
240248

241249
return target_op
@@ -788,17 +796,21 @@ class ForSequence(ForGenerator):
788796

789797
length_reg: Value | AssignmentTarget | None
790798

791-
def init(self, expr_reg: Value, target_type: RType, reverse: bool) -> None:
799+
def init(
800+
self, expr_reg: Value, target_type: RType, reverse: bool, length: Value | None = None
801+
) -> None:
792802
assert is_sequence_rprimitive(expr_reg.type), expr_reg
793803
builder = self.builder
804+
# Record a Value indicating the length of the sequence, if known at compile time.
805+
self.length = length
794806
self.reverse = reverse
795807
# Define target to contain the expression, along with the index that will be used
796808
# for the for-loop. If we are inside of a generator function, spill these into the
797809
# environment class.
798810
self.expr_target = builder.maybe_spill(expr_reg)
799811
if is_immutable_rprimitive(expr_reg.type):
800812
# If the expression is an immutable type, we can load the length just once.
801-
self.length_reg = builder.maybe_spill(self.load_len(self.expr_target))
813+
self.length_reg = builder.maybe_spill(self.length or self.load_len(self.expr_target))
802814
else:
803815
# Otherwise, even if the length is known, we must recalculate the length
804816
# at every iteration for compatibility with python semantics.
@@ -1166,3 +1178,43 @@ def gen_step(self) -> None:
11661178
def gen_cleanup(self) -> None:
11671179
for gen in self.gens:
11681180
gen.gen_cleanup()
1181+
1182+
1183+
def get_expr_length(expr: Expression) -> int | None:
1184+
if isinstance(expr, (StrExpr, BytesExpr)):
1185+
return len(expr.value)
1186+
elif isinstance(expr, (ListExpr, TupleExpr)):
1187+
# if there are no star expressions, or we know the length of them,
1188+
# we know the length of the expression
1189+
stars = [get_expr_length(i) for i in expr.items if isinstance(i, StarExpr)]
1190+
if None not in stars:
1191+
other = sum(not isinstance(i, StarExpr) for i in expr.items)
1192+
return other + sum(stars) # type: ignore [arg-type]
1193+
elif isinstance(expr, StarExpr):
1194+
return get_expr_length(expr.expr)
1195+
elif (
1196+
isinstance(expr, RefExpr)
1197+
and isinstance(expr.node, Var)
1198+
and expr.node.is_final
1199+
and isinstance(expr.node.final_value, str)
1200+
and expr.node.has_explicit_value
1201+
):
1202+
return len(expr.node.final_value)
1203+
# TODO: extend this, passing length of listcomp and genexp should have worthwhile
1204+
# performance boost and can be (sometimes) figured out pretty easily. set and dict
1205+
# comps *can* be done as well but will need special logic to consider the possibility
1206+
# of key conflicts. Range, enumerate, zip are all simple logic.
1207+
return None
1208+
1209+
1210+
def get_expr_length_value(
1211+
builder: IRBuilder, expr: Expression, expr_reg: Value, line: int, use_pyssize_t: bool
1212+
) -> Value:
1213+
rtype = builder.node_type(expr)
1214+
assert is_sequence_rprimitive(rtype), rtype
1215+
length = get_expr_length(expr)
1216+
if length is None:
1217+
# We cannot compute the length at compile time, so we will fetch it.
1218+
return builder.builder.builtin_len(expr_reg, line, use_pyssize_t=use_pyssize_t)
1219+
# The expression result is known at compile time, so we can use a constant.
1220+
return Integer(length, c_pyssize_t_rprimitive if use_pyssize_t else short_int_rprimitive)

mypyc/test-data/irbuild-generics.test

Lines changed: 58 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -678,84 +678,83 @@ def inner_deco_obj.__call__(__mypyc_self__, args, kwargs):
678678
r0 :: __main__.deco_env
679679
r1 :: native_int
680680
r2 :: list
681-
r3, r4 :: native_int
682-
r5 :: bit
683-
r6, x :: object
684-
r7 :: native_int
681+
r3 :: native_int
682+
r4 :: bit
683+
r5, x :: object
684+
r6 :: native_int
685685
can_listcomp :: list
686-
r8 :: dict
687-
r9 :: short_int
688-
r10 :: native_int
689-
r11 :: object
690-
r12 :: tuple[bool, short_int, object, object]
691-
r13 :: short_int
692-
r14 :: bool
693-
r15, r16 :: object
694-
r17, k :: str
686+
r7 :: dict
687+
r8 :: short_int
688+
r9 :: native_int
689+
r10 :: object
690+
r11 :: tuple[bool, short_int, object, object]
691+
r12 :: short_int
692+
r13 :: bool
693+
r14, r15 :: object
694+
r16, k :: str
695695
v :: object
696-
r18 :: i32
697-
r19, r20, r21 :: bit
696+
r17 :: i32
697+
r18, r19, r20 :: bit
698698
can_dictcomp :: dict
699-
r22, can_iter, r23, can_use_keys, r24, can_use_values :: list
700-
r25 :: object
701-
r26 :: dict
702-
r27 :: object
703-
r28 :: int
699+
r21, can_iter, r22, can_use_keys, r23, can_use_values :: list
700+
r24 :: object
701+
r25 :: dict
702+
r26 :: object
703+
r27 :: int
704704
L0:
705705
r0 = __mypyc_self__.__mypyc_env__
706706
r1 = var_object_size args
707707
r2 = PyList_New(r1)
708-
r3 = var_object_size args
709-
r4 = 0
708+
r3 = 0
710709
L1:
711-
r5 = r4 < r3 :: signed
712-
if r5 goto L2 else goto L4 :: bool
710+
r4 = r3 < r1 :: signed
711+
if r4 goto L2 else goto L4 :: bool
713712
L2:
714-
r6 = CPySequenceTuple_GetItemUnsafe(args, r4)
715-
x = r6
716-
CPyList_SetItemUnsafe(r2, r4, x)
713+
r5 = CPySequenceTuple_GetItemUnsafe(args, r3)
714+
x = r5
715+
CPyList_SetItemUnsafe(r2, r3, x)
717716
L3:
718-
r7 = r4 + 1
719-
r4 = r7
717+
r6 = r3 + 1
718+
r3 = r6
720719
goto L1
721720
L4:
722721
can_listcomp = r2
723-
r8 = PyDict_New()
724-
r9 = 0
725-
r10 = PyDict_Size(kwargs)
726-
r11 = CPyDict_GetItemsIter(kwargs)
722+
r7 = PyDict_New()
723+
r8 = 0
724+
r9 = PyDict_Size(kwargs)
725+
r10 = CPyDict_GetItemsIter(kwargs)
727726
L5:
728-
r12 = CPyDict_NextItem(r11, r9)
729-
r13 = r12[1]
730-
r9 = r13
731-
r14 = r12[0]
732-
if r14 goto L6 else goto L8 :: bool
727+
r11 = CPyDict_NextItem(r10, r8)
728+
r12 = r11[1]
729+
r8 = r12
730+
r13 = r11[0]
731+
if r13 goto L6 else goto L8 :: bool
733732
L6:
734-
r15 = r12[2]
735-
r16 = r12[3]
736-
r17 = cast(str, r15)
737-
k = r17
738-
v = r16
739-
r18 = PyDict_SetItem(r8, k, v)
740-
r19 = r18 >= 0 :: signed
733+
r14 = r11[2]
734+
r15 = r11[3]
735+
r16 = cast(str, r14)
736+
k = r16
737+
v = r15
738+
r17 = PyDict_SetItem(r7, k, v)
739+
r18 = r17 >= 0 :: signed
741740
L7:
742-
r20 = CPyDict_CheckSize(kwargs, r10)
741+
r19 = CPyDict_CheckSize(kwargs, r9)
743742
goto L5
744743
L8:
745-
r21 = CPy_NoErrOccurred()
744+
r20 = CPy_NoErrOccurred()
746745
L9:
747-
can_dictcomp = r8
748-
r22 = PySequence_List(kwargs)
749-
can_iter = r22
750-
r23 = CPyDict_Keys(kwargs)
751-
can_use_keys = r23
752-
r24 = CPyDict_Values(kwargs)
753-
can_use_values = r24
754-
r25 = r0.func
755-
r26 = PyDict_Copy(kwargs)
756-
r27 = PyObject_Call(r25, args, r26)
757-
r28 = unbox(int, r27)
758-
return r28
746+
can_dictcomp = r7
747+
r21 = PySequence_List(kwargs)
748+
can_iter = r21
749+
r22 = CPyDict_Keys(kwargs)
750+
can_use_keys = r22
751+
r23 = CPyDict_Values(kwargs)
752+
can_use_values = r23
753+
r24 = r0.func
754+
r25 = PyDict_Copy(kwargs)
755+
r26 = PyObject_Call(r24, args, r25)
756+
r27 = unbox(int, r26)
757+
return r27
759758
def deco(func):
760759
func :: object
761760
r0 :: __main__.deco_env

0 commit comments

Comments
 (0)