-
-
Notifications
You must be signed in to change notification settings - Fork 3k
[mypyc] Call generator helper method directly in await expression #19376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Also use generator class as the return type of generators.
# Give a more precise type for generators, so that we can optimize | ||
# code that uses them. They return a generator object, which has a | ||
# specific class. Without this, the type would have to be 'object'. | ||
ret: RType = RInstance(self.fdef_to_generator[fdef]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the first part of the optimization.
and val.type.class_ir.has_method(helper_method) | ||
): | ||
# This is a generated generator class, and we can use a fast path. | ||
iter_val: Value = val |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes in this function implemented the bulk of the optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a bit more detailed comment (essentially summarizing the PR description) so we will not forget the motivation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LG, just one suggestion.
and val.type.class_ir.has_method(helper_method) | ||
): | ||
# This is a generated generator class, and we can use a fast path. | ||
iter_val: Value = val |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a bit more detailed comment (essentially summarizing the PR description) so we will not forget the motivation.
When calling a native async function using `await`, e.g. `await foo()`, avoid raising `StopIteration` to pass the return value, since this is expensive. Instead, pass an extra `PyObject **` argument to the generator helper method and use that to return the return value. This is mostly helpful when there are many calls using await that don't block (e.g. there is a fast path that is usually taken that doesn't block). When awaiting from non-compiled code, the slow path is still taken. This builds on top of #19376. This PR makes this microbenchmark about 3x faster, which is about the ideal scenario for this optimization: ``` import asyncio from time import time async def inc(x: int) -> int: return x + 1 async def bench(n: int) -> int: x = 0 for i in range(n): x = await inc(x) return x asyncio.run(bench(1000)) t0 = time() asyncio.run(bench(1000 * 1000 * 200)) print(time() - t0) ```
Results in:
Ping #19764 |
Error message and highlevel reproduction looks awfully similar to #19558, I expect they might both be stemming from the same issue. |
They do not. Your issue exists prior to this merge - the code from your opening comment errors out if I go further back in history than this commit. |
I created an issue about the regression: mypyc/mypyc#1141 Let's move further discussion there. |
Previously calls like
await foo()
were compiled to code that included code like this (in Python-like pseudocode):In the above code,
get_coro(a)
just returnsa
iffoo
is a native async function, so we now optimize this call away. Alsonext(b)
callsb.__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 aStopIteration
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.