Skip to content

Commit 781dd69

Browse files
authored
[mypyc] Faster dict iteration (#8725)
Fixes mypyc/mypyc#167 The implementation is pretty straightforward and follows the idea proposed in the issue. The perf impact is actually pretty small, around 1% Note: we don't apply the fast path to subclasses here to make dict subclassing safe.
1 parent 77e6b19 commit 781dd69

File tree

10 files changed

+825
-97
lines changed

10 files changed

+825
-97
lines changed

mypyc/ir/rtypes.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,16 @@ def deserialize(cls, data: JsonDict, ctx: 'DeserMaps') -> 'RTuple':
399399
# Exception tuple: (exception class, exception instance, traceback object)
400400
exc_rtuple = RTuple([object_rprimitive, object_rprimitive, object_rprimitive])
401401

402+
# Dictionary iterator tuple: (should continue, internal offset, key, value)
403+
# See mypyc.irbuild.for_helpers.ForDictionaryCommon for more details.
404+
dict_next_rtuple_pair = RTuple(
405+
[bool_rprimitive, int_rprimitive, object_rprimitive, object_rprimitive]
406+
)
407+
# Same as above but just for key or value.
408+
dict_next_rtuple_single = RTuple(
409+
[bool_rprimitive, int_rprimitive, object_rprimitive]
410+
)
411+
402412

403413
class RInstance(RType):
404414
"""Instance of user-defined class (compiled to C extension class).

mypyc/irbuild/builder.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from mypy.types import (
2525
Type, Instance, TupleType, UninhabitedType, get_proper_type
2626
)
27+
from mypy.maptype import map_instance_to_supertype
2728
from mypy.visitor import ExpressionVisitor, StatementVisitor
2829
from mypy.util import split_target
2930

@@ -604,6 +605,30 @@ def get_sequence_type(self, expr: Expression) -> RType:
604605
else:
605606
return self.type_to_rtype(target_type.args[0])
606607

608+
def get_dict_base_type(self, expr: Expression) -> Instance:
609+
"""Find dict type of a dict-like expression.
610+
611+
This is useful for dict subclasses like SymbolTable.
612+
"""
613+
target_type = get_proper_type(self.types[expr])
614+
assert isinstance(target_type, Instance)
615+
dict_base = next(base for base in target_type.type.mro
616+
if base.fullname == 'builtins.dict')
617+
return map_instance_to_supertype(target_type, dict_base)
618+
619+
def get_dict_key_type(self, expr: Expression) -> RType:
620+
dict_base_type = self.get_dict_base_type(expr)
621+
return self.type_to_rtype(dict_base_type.args[0])
622+
623+
def get_dict_value_type(self, expr: Expression) -> RType:
624+
dict_base_type = self.get_dict_base_type(expr)
625+
return self.type_to_rtype(dict_base_type.args[1])
626+
627+
def get_dict_item_type(self, expr: Expression) -> RType:
628+
key_type = self.get_dict_key_type(expr)
629+
value_type = self.get_dict_value_type(expr)
630+
return RTuple([key_type, value_type])
631+
607632
def _analyze_iterable_item_type(self, expr: Expression) -> Type:
608633
"""Return the item type given by 'expr' in an iterable context."""
609634
# This logic is copied from mypy's TypeChecker.analyze_iterable_item_type.

mypyc/irbuild/for_helpers.py

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66
"""
77

88
from typing import Union, List, Optional, Tuple, Callable
9+
from typing_extensions import Type, ClassVar
910

10-
from mypy.nodes import Lvalue, Expression, TupleExpr, CallExpr, RefExpr, GeneratorExpr, ARG_POS
11+
from mypy.nodes import (
12+
Lvalue, Expression, TupleExpr, CallExpr, RefExpr, GeneratorExpr, ARG_POS, MemberExpr
13+
)
1114
from mypyc.ir.ops import (
12-
Value, BasicBlock, LoadInt, Branch, Register, AssignmentTarget
15+
Value, BasicBlock, LoadInt, Branch, Register, AssignmentTarget, TupleGet,
16+
AssignmentTargetTuple, TupleSet, OpDescription
1317
)
1418
from mypyc.ir.rtypes import (
15-
RType, is_short_int_rprimitive, is_list_rprimitive, is_sequence_rprimitive
19+
RType, is_short_int_rprimitive, is_list_rprimitive, is_sequence_rprimitive,
20+
RTuple, is_dict_rprimitive
21+
)
22+
from mypyc.primitives.dict_ops import (
23+
dict_next_key_op, dict_next_value_op, dict_next_item_op, dict_check_size_op,
24+
dict_key_iter_op, dict_value_iter_op, dict_item_iter_op
1625
)
1726
from mypyc.primitives.int_ops import unsafe_short_add
1827
from mypyc.primitives.list_ops import new_list_op, list_append_op, list_get_item_unsafe_op
@@ -170,6 +179,15 @@ def make_for_loop_generator(builder: IRBuilder,
170179
for_list.init(expr_reg, target_type, reverse=False)
171180
return for_list
172181

182+
if is_dict_rprimitive(rtyp):
183+
# Special case "for k in <dict>".
184+
expr_reg = builder.accept(expr)
185+
target_type = builder.get_dict_key_type(expr)
186+
187+
for_dict = ForDictionaryKeys(builder, index, body_block, loop_exit, line, nested)
188+
for_dict.init(expr_reg, target_type)
189+
return for_dict
190+
173191
if (isinstance(expr, CallExpr)
174192
and isinstance(expr.callee, RefExpr)):
175193
if (expr.callee.fullname == 'builtins.range'
@@ -233,6 +251,27 @@ def make_for_loop_generator(builder: IRBuilder,
233251
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
234252
for_list.init(expr_reg, target_type, reverse=True)
235253
return for_list
254+
if (isinstance(expr, CallExpr)
255+
and isinstance(expr.callee, MemberExpr)
256+
and not expr.args):
257+
# Special cases for dictionary iterator methods, like dict.items().
258+
rtype = builder.node_type(expr.callee.expr)
259+
if (is_dict_rprimitive(rtype)
260+
and expr.callee.name in ('keys', 'values', 'items')):
261+
expr_reg = builder.accept(expr.callee.expr)
262+
for_dict_type = None # type: Optional[Type[ForGenerator]]
263+
if expr.callee.name == 'keys':
264+
target_type = builder.get_dict_key_type(expr.callee.expr)
265+
for_dict_type = ForDictionaryKeys
266+
elif expr.callee.name == 'values':
267+
target_type = builder.get_dict_value_type(expr.callee.expr)
268+
for_dict_type = ForDictionaryValues
269+
else:
270+
target_type = builder.get_dict_item_type(expr.callee.expr)
271+
for_dict_type = ForDictionaryItems
272+
for_dict_gen = for_dict_type(builder, index, body_block, loop_exit, line, nested)
273+
for_dict_gen.init(expr_reg, target_type)
274+
return for_dict_gen
236275

237276
# Default to a generic for loop.
238277
expr_reg = builder.accept(expr)
@@ -292,6 +331,14 @@ def gen_step(self) -> None:
292331
def gen_cleanup(self) -> None:
293332
"""Generate post-loop cleanup (if needed)."""
294333

334+
def load_len(self, expr: Union[Value, AssignmentTarget]) -> Value:
335+
"""A helper to get collection length, used by several subclasses."""
336+
return self.builder.builder.builtin_call(
337+
[self.builder.read(expr, self.line)],
338+
'builtins.len',
339+
self.line,
340+
)
341+
295342

296343
class ForIterable(ForGenerator):
297344
"""Generate IR for a for loop over an arbitrary iterable (the normal case)."""
@@ -371,17 +418,11 @@ def init(self, expr_reg: Value, target_type: RType, reverse: bool) -> None:
371418
if not reverse:
372419
index_reg = builder.add(LoadInt(0))
373420
else:
374-
index_reg = builder.binary_op(self.load_len(), builder.add(LoadInt(1)), '-', self.line)
421+
index_reg = builder.binary_op(self.load_len(self.expr_target),
422+
builder.add(LoadInt(1)), '-', self.line)
375423
self.index_target = builder.maybe_spill_assignable(index_reg)
376424
self.target_type = target_type
377425

378-
def load_len(self) -> Value:
379-
return self.builder.builder.builtin_call(
380-
[self.builder.read(self.expr_target, self.line)],
381-
'builtins.len',
382-
self.line,
383-
)
384-
385426
def gen_condition(self) -> None:
386427
builder = self.builder
387428
line = self.line
@@ -398,7 +439,7 @@ def gen_condition(self) -> None:
398439
builder.activate_block(second_check)
399440
# For compatibility with python semantics we recalculate the length
400441
# at every iteration.
401-
len_reg = self.load_len()
442+
len_reg = self.load_len(self.expr_target)
402443
comparison = builder.binary_op(builder.read(self.index_target, line), len_reg, '<', line)
403444
builder.add_bool_branch(comparison, self.body_block, self.loop_exit)
404445

@@ -430,6 +471,136 @@ def gen_step(self) -> None:
430471
builder.add(LoadInt(step))], line), line)
431472

432473

474+
class ForDictionaryCommon(ForGenerator):
475+
"""Generate optimized IR for a for loop over dictionary keys/values.
476+
477+
The logic is pretty straightforward, we use PyDict_Next() API wrapped in
478+
a tuple, so that we can modify only a single register. The layout of the tuple:
479+
* f0: are there more items (bool)
480+
* f1: current offset (int)
481+
* f2: next key (object)
482+
* f3: next value (object)
483+
For more info see https://docs.python.org/3/c-api/dict.html#c.PyDict_Next.
484+
485+
Note that for subclasses we fall back to generic PyObject_GetIter() logic,
486+
since they may override some iteration methods in subtly incompatible manner.
487+
The fallback logic is implemented in CPy.h via dynamic type check.
488+
"""
489+
dict_next_op = None # type: ClassVar[OpDescription]
490+
dict_iter_op = None # type: ClassVar[OpDescription]
491+
492+
def need_cleanup(self) -> bool:
493+
# Technically, a dict subclass can raise an unrelated exception
494+
# in __next__(), so we need this.
495+
return True
496+
497+
def init(self, expr_reg: Value, target_type: RType) -> None:
498+
builder = self.builder
499+
self.target_type = target_type
500+
501+
# We add some variables to environment class, so they can be read across yield.
502+
self.expr_target = builder.maybe_spill(expr_reg)
503+
offset_reg = builder.add(LoadInt(0))
504+
self.offset_target = builder.maybe_spill_assignable(offset_reg)
505+
self.size = builder.maybe_spill(self.load_len(self.expr_target))
506+
507+
# For dict class (not a subclass) this is the dictionary itself.
508+
iter_reg = builder.primitive_op(self.dict_iter_op, [expr_reg], self.line)
509+
self.iter_target = builder.maybe_spill(iter_reg)
510+
511+
def gen_condition(self) -> None:
512+
"""Get next key/value pair, set new offset, and check if we should continue."""
513+
builder = self.builder
514+
line = self.line
515+
self.next_tuple = self.builder.primitive_op(
516+
self.dict_next_op, [builder.read(self.iter_target, line),
517+
builder.read(self.offset_target, line)], line)
518+
519+
# Do this here instead of in gen_step() to minimize variables in environment.
520+
new_offset = builder.add(TupleGet(self.next_tuple, 1, line))
521+
builder.assign(self.offset_target, new_offset, line)
522+
523+
should_continue = builder.add(TupleGet(self.next_tuple, 0, line))
524+
builder.add(
525+
Branch(should_continue, self.body_block, self.loop_exit, Branch.BOOL_EXPR)
526+
)
527+
528+
def gen_step(self) -> None:
529+
"""Check that dictionary didn't change size during iteration.
530+
531+
Raise RuntimeError if it is not the case to match CPython behavior.
532+
"""
533+
builder = self.builder
534+
line = self.line
535+
# Technically, we don't need a new primitive for this, but it is simpler.
536+
builder.primitive_op(dict_check_size_op,
537+
[builder.read(self.expr_target, line),
538+
builder.read(self.size, line)], line)
539+
540+
def gen_cleanup(self) -> None:
541+
# Same as for generic ForIterable.
542+
self.builder.primitive_op(no_err_occurred_op, [], self.line)
543+
544+
545+
class ForDictionaryKeys(ForDictionaryCommon):
546+
"""Generate optimized IR for a for loop over dictionary keys."""
547+
dict_next_op = dict_next_key_op
548+
dict_iter_op = dict_key_iter_op
549+
550+
def begin_body(self) -> None:
551+
builder = self.builder
552+
line = self.line
553+
554+
# Key is stored at the third place in the tuple.
555+
key = builder.add(TupleGet(self.next_tuple, 2, line))
556+
builder.assign(builder.get_assignment_target(self.index),
557+
builder.coerce(key, self.target_type, line), line)
558+
559+
560+
class ForDictionaryValues(ForDictionaryCommon):
561+
"""Generate optimized IR for a for loop over dictionary values."""
562+
dict_next_op = dict_next_value_op
563+
dict_iter_op = dict_value_iter_op
564+
565+
def begin_body(self) -> None:
566+
builder = self.builder
567+
line = self.line
568+
569+
# Value is stored at the third place in the tuple.
570+
value = builder.add(TupleGet(self.next_tuple, 2, line))
571+
builder.assign(builder.get_assignment_target(self.index),
572+
builder.coerce(value, self.target_type, line), line)
573+
574+
575+
class ForDictionaryItems(ForDictionaryCommon):
576+
"""Generate optimized IR for a for loop over dictionary items."""
577+
dict_next_op = dict_next_item_op
578+
dict_iter_op = dict_item_iter_op
579+
580+
def begin_body(self) -> None:
581+
builder = self.builder
582+
line = self.line
583+
584+
key = builder.add(TupleGet(self.next_tuple, 2, line))
585+
value = builder.add(TupleGet(self.next_tuple, 3, line))
586+
587+
# Coerce just in case e.g. key is itself a tuple to be unpacked.
588+
assert isinstance(self.target_type, RTuple)
589+
key = builder.coerce(key, self.target_type.types[0], line)
590+
value = builder.coerce(value, self.target_type.types[1], line)
591+
592+
target = builder.get_assignment_target(self.index)
593+
if isinstance(target, AssignmentTargetTuple):
594+
# Simpler code for common case: for k, v in d.items().
595+
if len(target.items) != 2:
596+
builder.error("Expected a pair for dict item iteration", line)
597+
builder.assign(target.items[0], key, line)
598+
builder.assign(target.items[1], value, line)
599+
else:
600+
rvalue = builder.add(TupleSet([key, value], line))
601+
builder.assign(target, rvalue, line)
602+
603+
433604
class ForRange(ForGenerator):
434605
"""Generate optimized IR for a for loop over an integer range."""
435606

0 commit comments

Comments
 (0)