Skip to content

Commit ada0d2a

Browse files
authored
[mypyc] Call generator helper method directly in await expression (#19376)
Previously calls like `await foo()` were compiled to code that included code like this (in Python-like pseudocode): ``` a = foo() ... b = get_coro(a) ... c = next(b) ``` In the above code, `get_coro(a)` just returns `a` if `foo` is a native async function, so we now optimize this call away. Also `next(b)` calls `b.__next__()`, which calls the generated generator helper method `__mypyc_generator_helper__`. Now we call the helper method directly, which saves some unnecessary calls. More importantly, in a follow-up PR I can easily change the way `__mypyc_generator_helper__` is called, since we now call it directly. This makes it possible to avoid raising a `StopIteration` exception in many await expressions. The goal of this PR is to prepare for the latter optimization. This PR doesn't help performance significantly by itself. In order to call the helper method directly, I had to generate the declaration of this method and the generated generator class before the main irbuild pass, since otherwise a call site could be processed before we have processed the called generator. I also improved test coverage of related functionality. We don't have an IR test for async calls, since the IR is very verbose. I manually inspected the generated IR to verify that the new code path works both when calling a top-level function and when calling a method. I'll later add a mypyc benchmark to ensure that we will notice if the performance of async calls is degraded.
1 parent 526fec3 commit ada0d2a

File tree

8 files changed

+251
-70
lines changed

8 files changed

+251
-70
lines changed

mypyc/irbuild/function.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def c() -> None:
261261
)
262262

263263
# Re-enter the FuncItem and visit the body of the function this time.
264-
gen_generator_func_body(builder, fn_info, sig, func_reg)
264+
gen_generator_func_body(builder, fn_info, func_reg)
265265
else:
266266
func_ir, func_reg = gen_func_body(builder, sig, cdef, is_singledispatch)
267267

mypyc/irbuild/generator.py

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
from typing import Callable
1414

1515
from mypy.nodes import ARG_OPT, FuncDef, Var
16-
from mypyc.common import ENV_ATTR_NAME, NEXT_LABEL_ATTR_NAME, SELF_NAME
16+
from mypyc.common import ENV_ATTR_NAME, NEXT_LABEL_ATTR_NAME
1717
from mypyc.ir.class_ir import ClassIR
18-
from mypyc.ir.func_ir import FuncDecl, FuncIR, FuncSignature, RuntimeArg
18+
from mypyc.ir.func_ir import FuncDecl, FuncIR
1919
from mypyc.ir.ops import (
2020
NO_TRACEBACK_LINE_NO,
2121
BasicBlock,
@@ -78,17 +78,15 @@ def gen_generator_func(
7878
return func_ir, func_reg
7979

8080

81-
def gen_generator_func_body(
82-
builder: IRBuilder, fn_info: FuncInfo, sig: FuncSignature, func_reg: Value | None
83-
) -> None:
81+
def gen_generator_func_body(builder: IRBuilder, fn_info: FuncInfo, func_reg: Value | None) -> None:
8482
"""Generate IR based on the body of a generator function.
8583
8684
Add "__next__", "__iter__" and other generator methods to the generator
8785
class that implements the function (each function gets a separate class).
8886
8987
Return the symbol table for the body.
9088
"""
91-
builder.enter(fn_info, ret_type=sig.ret_type)
89+
builder.enter(fn_info, ret_type=object_rprimitive)
9290
setup_env_for_generator_class(builder)
9391

9492
load_outer_envs(builder, builder.fn_info.generator_class)
@@ -117,7 +115,7 @@ class that implements the function (each function gets a separate class).
117115

118116
args, _, blocks, ret_type, fn_info = builder.leave()
119117

120-
add_methods_to_generator_class(builder, fn_info, sig, args, blocks, fitem.is_coroutine)
118+
add_methods_to_generator_class(builder, fn_info, args, blocks, fitem.is_coroutine)
121119

122120
# Evaluate argument defaults in the surrounding scope, since we
123121
# calculate them *once* when the function definition is evaluated.
@@ -153,10 +151,9 @@ def instantiate_generator_class(builder: IRBuilder) -> Value:
153151

154152

155153
def setup_generator_class(builder: IRBuilder) -> ClassIR:
156-
name = f"{builder.fn_info.namespaced_name()}_gen"
157-
158-
generator_class_ir = ClassIR(name, builder.module_name, is_generated=True, is_final_class=True)
159-
generator_class_ir.reuse_freed_instance = True
154+
mapper = builder.mapper
155+
assert isinstance(builder.fn_info.fitem, FuncDef)
156+
generator_class_ir = mapper.fdef_to_generator[builder.fn_info.fitem]
160157
if builder.fn_info.can_merge_generator_and_env_classes():
161158
builder.fn_info.env_class = generator_class_ir
162159
else:
@@ -216,46 +213,25 @@ def add_raise_exception_blocks_to_generator_class(builder: IRBuilder, line: int)
216213
def add_methods_to_generator_class(
217214
builder: IRBuilder,
218215
fn_info: FuncInfo,
219-
sig: FuncSignature,
220216
arg_regs: list[Register],
221217
blocks: list[BasicBlock],
222218
is_coroutine: bool,
223219
) -> None:
224-
helper_fn_decl = add_helper_to_generator_class(builder, arg_regs, blocks, sig, fn_info)
225-
add_next_to_generator_class(builder, fn_info, helper_fn_decl, sig)
226-
add_send_to_generator_class(builder, fn_info, helper_fn_decl, sig)
220+
helper_fn_decl = add_helper_to_generator_class(builder, arg_regs, blocks, fn_info)
221+
add_next_to_generator_class(builder, fn_info, helper_fn_decl)
222+
add_send_to_generator_class(builder, fn_info, helper_fn_decl)
227223
add_iter_to_generator_class(builder, fn_info)
228-
add_throw_to_generator_class(builder, fn_info, helper_fn_decl, sig)
224+
add_throw_to_generator_class(builder, fn_info, helper_fn_decl)
229225
add_close_to_generator_class(builder, fn_info)
230226
if is_coroutine:
231227
add_await_to_generator_class(builder, fn_info)
232228

233229

234230
def add_helper_to_generator_class(
235-
builder: IRBuilder,
236-
arg_regs: list[Register],
237-
blocks: list[BasicBlock],
238-
sig: FuncSignature,
239-
fn_info: FuncInfo,
231+
builder: IRBuilder, arg_regs: list[Register], blocks: list[BasicBlock], fn_info: FuncInfo
240232
) -> FuncDecl:
241233
"""Generates a helper method for a generator class, called by '__next__' and 'throw'."""
242-
sig = FuncSignature(
243-
(
244-
RuntimeArg(SELF_NAME, object_rprimitive),
245-
RuntimeArg("type", object_rprimitive),
246-
RuntimeArg("value", object_rprimitive),
247-
RuntimeArg("traceback", object_rprimitive),
248-
RuntimeArg("arg", object_rprimitive),
249-
),
250-
sig.ret_type,
251-
)
252-
helper_fn_decl = FuncDecl(
253-
"__mypyc_generator_helper__",
254-
fn_info.generator_class.ir.name,
255-
builder.module_name,
256-
sig,
257-
internal=True,
258-
)
234+
helper_fn_decl = fn_info.generator_class.ir.method_decls["__mypyc_generator_helper__"]
259235
helper_fn_ir = FuncIR(
260236
helper_fn_decl, arg_regs, blocks, fn_info.fitem.line, traceback_name=fn_info.fitem.name
261237
)
@@ -272,9 +248,7 @@ def add_iter_to_generator_class(builder: IRBuilder, fn_info: FuncInfo) -> None:
272248
builder.add(Return(builder.self()))
273249

274250

275-
def add_next_to_generator_class(
276-
builder: IRBuilder, fn_info: FuncInfo, fn_decl: FuncDecl, sig: FuncSignature
277-
) -> None:
251+
def add_next_to_generator_class(builder: IRBuilder, fn_info: FuncInfo, fn_decl: FuncDecl) -> None:
278252
"""Generates the '__next__' method for a generator class."""
279253
with builder.enter_method(fn_info.generator_class.ir, "__next__", object_rprimitive, fn_info):
280254
none_reg = builder.none_object()
@@ -289,9 +263,7 @@ def add_next_to_generator_class(
289263
builder.add(Return(result))
290264

291265

292-
def add_send_to_generator_class(
293-
builder: IRBuilder, fn_info: FuncInfo, fn_decl: FuncDecl, sig: FuncSignature
294-
) -> None:
266+
def add_send_to_generator_class(builder: IRBuilder, fn_info: FuncInfo, fn_decl: FuncDecl) -> None:
295267
"""Generates the 'send' method for a generator class."""
296268
with builder.enter_method(fn_info.generator_class.ir, "send", object_rprimitive, fn_info):
297269
arg = builder.add_argument("arg", object_rprimitive)
@@ -307,9 +279,7 @@ def add_send_to_generator_class(
307279
builder.add(Return(result))
308280

309281

310-
def add_throw_to_generator_class(
311-
builder: IRBuilder, fn_info: FuncInfo, fn_decl: FuncDecl, sig: FuncSignature
312-
) -> None:
282+
def add_throw_to_generator_class(builder: IRBuilder, fn_info: FuncInfo, fn_decl: FuncDecl) -> None:
313283
"""Generates the 'throw' method for a generator class."""
314284
with builder.enter_method(fn_info.generator_class.ir, "throw", object_rprimitive, fn_info):
315285
typ = builder.add_argument("type", object_rprimitive)

mypyc/irbuild/main.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def f(x: int) -> int:
2525
from typing import Any, Callable, TypeVar, cast
2626

2727
from mypy.build import Graph
28-
from mypy.nodes import ClassDef, Expression, MypyFile
28+
from mypy.nodes import ClassDef, Expression, FuncDef, MypyFile
2929
from mypy.state import state
3030
from mypy.types import Type
3131
from mypyc.analysis.attrdefined import analyze_always_defined_attrs
@@ -37,7 +37,11 @@ def f(x: int) -> int:
3737
from mypyc.irbuild.builder import IRBuilder
3838
from mypyc.irbuild.mapper import Mapper
3939
from mypyc.irbuild.prebuildvisitor import PreBuildVisitor
40-
from mypyc.irbuild.prepare import build_type_map, find_singledispatch_register_impls
40+
from mypyc.irbuild.prepare import (
41+
build_type_map,
42+
create_generator_class_if_needed,
43+
find_singledispatch_register_impls,
44+
)
4145
from mypyc.irbuild.visitor import IRBuilderVisitor
4246
from mypyc.irbuild.vtable import compute_vtable
4347
from mypyc.options import CompilerOptions
@@ -76,6 +80,15 @@ def build_ir(
7680
pbv = PreBuildVisitor(errors, module, singledispatch_info.decorators_to_remove, types)
7781
module.accept(pbv)
7882

83+
# Declare generator classes for nested async functions and generators.
84+
for fdef in pbv.nested_funcs:
85+
if isinstance(fdef, FuncDef):
86+
# Make generator class name sufficiently unique.
87+
suffix = f"___{fdef.line}"
88+
create_generator_class_if_needed(
89+
module.fullname, None, fdef, mapper, name_suffix=suffix
90+
)
91+
7992
# Construct and configure builder objects (cyclic runtime dependency).
8093
visitor = IRBuilderVisitor()
8194
builder = IRBuilder(

mypyc/irbuild/mapper.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def __init__(self, group_map: dict[str, str | None]) -> None:
6464
self.type_to_ir: dict[TypeInfo, ClassIR] = {}
6565
self.func_to_decl: dict[SymbolNode, FuncDecl] = {}
6666
self.symbol_fullnames: set[str] = set()
67+
# The corresponding generator class that implements a generator/async function
68+
self.fdef_to_generator: dict[FuncDef, ClassIR] = {}
6769

6870
def type_to_rtype(self, typ: Type | None) -> RType:
6971
if typ is None:
@@ -171,7 +173,14 @@ def fdef_to_sig(self, fdef: FuncDef, strict_dunders_typing: bool) -> FuncSignatu
171173
for typ, kind in zip(fdef.type.arg_types, fdef.type.arg_kinds)
172174
]
173175
arg_pos_onlys = [name is None for name in fdef.type.arg_names]
174-
ret = self.type_to_rtype(fdef.type.ret_type)
176+
# TODO: We could probably support decorators sometimes (static and class method?)
177+
if (fdef.is_coroutine or fdef.is_generator) and not fdef.is_decorated:
178+
# Give a more precise type for generators, so that we can optimize
179+
# code that uses them. They return a generator object, which has a
180+
# specific class. Without this, the type would have to be 'object'.
181+
ret: RType = RInstance(self.fdef_to_generator[fdef])
182+
else:
183+
ret = self.type_to_rtype(fdef.type.ret_type)
175184
else:
176185
# Handle unannotated functions
177186
arg_types = [object_rprimitive for _ in fdef.arguments]

mypyc/irbuild/prepare.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from mypy.semanal import refers_to_fullname
3939
from mypy.traverser import TraverserVisitor
4040
from mypy.types import Instance, Type, get_proper_type
41-
from mypyc.common import PROPSET_PREFIX, get_id_from_name
41+
from mypyc.common import PROPSET_PREFIX, SELF_NAME, get_id_from_name
4242
from mypyc.crash import catch_errors
4343
from mypyc.errors import Errors
4444
from mypyc.ir.class_ir import ClassIR
@@ -51,7 +51,14 @@
5151
RuntimeArg,
5252
)
5353
from mypyc.ir.ops import DeserMaps
54-
from mypyc.ir.rtypes import RInstance, RType, dict_rprimitive, none_rprimitive, tuple_rprimitive
54+
from mypyc.ir.rtypes import (
55+
RInstance,
56+
RType,
57+
dict_rprimitive,
58+
none_rprimitive,
59+
object_rprimitive,
60+
tuple_rprimitive,
61+
)
5562
from mypyc.irbuild.mapper import Mapper
5663
from mypyc.irbuild.util import (
5764
get_func_def,
@@ -115,7 +122,7 @@ def build_type_map(
115122

116123
# Collect all the functions also. We collect from the symbol table
117124
# so that we can easily pick out the right copy of a function that
118-
# is conditionally defined.
125+
# is conditionally defined. This doesn't include nested functions!
119126
for module in modules:
120127
for func in get_module_func_defs(module):
121128
prepare_func_def(module.fullname, None, func, mapper, options)
@@ -179,6 +186,8 @@ def prepare_func_def(
179186
mapper: Mapper,
180187
options: CompilerOptions,
181188
) -> FuncDecl:
189+
create_generator_class_if_needed(module_name, class_name, fdef, mapper)
190+
182191
kind = (
183192
FUNC_STATICMETHOD
184193
if fdef.is_static
@@ -190,6 +199,38 @@ def prepare_func_def(
190199
return decl
191200

192201

202+
def create_generator_class_if_needed(
203+
module_name: str, class_name: str | None, fdef: FuncDef, mapper: Mapper, name_suffix: str = ""
204+
) -> None:
205+
"""If function is a generator/async function, declare a generator class.
206+
207+
Each generator and async function gets a dedicated class that implements the
208+
generator protocol with generated methods.
209+
"""
210+
if fdef.is_coroutine or fdef.is_generator:
211+
name = "_".join(x for x in [fdef.name, class_name] if x) + "_gen" + name_suffix
212+
cir = ClassIR(name, module_name, is_generated=True, is_final_class=True)
213+
cir.reuse_freed_instance = True
214+
mapper.fdef_to_generator[fdef] = cir
215+
216+
helper_sig = FuncSignature(
217+
(
218+
RuntimeArg(SELF_NAME, object_rprimitive),
219+
RuntimeArg("type", object_rprimitive),
220+
RuntimeArg("value", object_rprimitive),
221+
RuntimeArg("traceback", object_rprimitive),
222+
RuntimeArg("arg", object_rprimitive),
223+
),
224+
object_rprimitive,
225+
)
226+
227+
# The implementation of most generator functionality is behind this magic method.
228+
helper_fn_decl = FuncDecl(
229+
"__mypyc_generator_helper__", name, module_name, helper_sig, internal=True
230+
)
231+
cir.method_decls[helper_fn_decl.name] = helper_fn_decl
232+
233+
193234
def prepare_method_def(
194235
ir: ClassIR,
195236
module_name: str,

mypyc/irbuild/statement.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@
4848
)
4949
from mypyc.common import TEMP_ATTR_NAME
5050
from mypyc.ir.ops import (
51+
ERR_NEVER,
5152
NAMESPACE_MODULE,
5253
NO_TRACEBACK_LINE_NO,
5354
Assign,
5455
BasicBlock,
5556
Branch,
57+
Call,
5658
InitStatic,
5759
Integer,
5860
LoadAddress,
@@ -930,16 +932,41 @@ def emit_yield_from_or_await(
930932
to_yield_reg = Register(object_rprimitive)
931933
received_reg = Register(object_rprimitive)
932934

933-
get_op = coro_op if is_await else iter_op
934-
if isinstance(get_op, PrimitiveDescription):
935-
iter_val = builder.primitive_op(get_op, [val], line)
935+
helper_method = "__mypyc_generator_helper__"
936+
if (
937+
isinstance(val, (Call, MethodCall))
938+
and isinstance(val.type, RInstance)
939+
and val.type.class_ir.has_method(helper_method)
940+
):
941+
# This is a generated native generator class, and we can use a fast path.
942+
# This allows two optimizations:
943+
# 1) No need to call CPy_GetCoro() or iter() since for native generators
944+
# it just returns the generator object (implemented here).
945+
# 2) Instead of calling next(), call generator helper method directly,
946+
# since next() just calls __next__ which calls the helper method.
947+
iter_val: Value = val
936948
else:
937-
iter_val = builder.call_c(get_op, [val], line)
949+
get_op = coro_op if is_await else iter_op
950+
if isinstance(get_op, PrimitiveDescription):
951+
iter_val = builder.primitive_op(get_op, [val], line)
952+
else:
953+
iter_val = builder.call_c(get_op, [val], line)
938954

939955
iter_reg = builder.maybe_spill_assignable(iter_val)
940956

941957
stop_block, main_block, done_block = BasicBlock(), BasicBlock(), BasicBlock()
942-
_y_init = builder.call_c(next_raw_op, [builder.read(iter_reg)], line)
958+
959+
if isinstance(iter_reg.type, RInstance) and iter_reg.type.class_ir.has_method(helper_method):
960+
# Second fast path optimization: call helper directly (see also comment above).
961+
obj = builder.read(iter_reg)
962+
nn = builder.none_object()
963+
m = MethodCall(obj, helper_method, [nn, nn, nn, nn], line)
964+
# Generators have custom error handling, so disable normal error handling.
965+
m.error_kind = ERR_NEVER
966+
_y_init = builder.add(m)
967+
else:
968+
_y_init = builder.call_c(next_raw_op, [builder.read(iter_reg)], line)
969+
943970
builder.add(Branch(_y_init, stop_block, main_block, Branch.IS_ERROR))
944971

945972
# Try extracting a return value from a StopIteration and return it.
@@ -948,7 +975,7 @@ def emit_yield_from_or_await(
948975
builder.assign(result, builder.call_c(check_stop_op, [], line), line)
949976
# Clear the spilled iterator/coroutine so that it will be freed.
950977
# Otherwise, the freeing of the spilled register would likely be delayed.
951-
err = builder.add(LoadErrorValue(object_rprimitive))
978+
err = builder.add(LoadErrorValue(iter_reg.type))
952979
builder.assign(iter_reg, err, line)
953980
builder.goto(done_block)
954981

0 commit comments

Comments
 (0)