-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Unused arguments to async fn are dropped too early #54716
Comments
Marking this as blocking stabilization of async-await. Obviously, changing the semantics here would alter the semantics of stable code. Moreover, there's a good case to be made that the current semantics are incorrect. @cramertj and I (along with a few others) discussed this in a recent meeting. Our conclusion was that we effectively wanted a desugaring like the following: async fn foo(<pattern> @ x: Type) {
}
// becomes
fn foo(<pattern> @ x: Type) {
async move {
let <pattern> = x;
} // <-- as you "exit" the async block
} where Some of the key test cases we want to consider would be:
This is quite analogous then to the behavior for |
Tagging as E-needs-mentor as we need to write up some notes on how to alter the desugaring here. One bit I can see being complex is that the HIR presently has no way to "name" a parameter apart from entering a pattern. |
(nominating for discussion in T-lang to resolve the desired behavior) |
Summary of the current issues by @davidtwco can be found on Zulip. |
blocks #50547 |
At the T-lang meeting, @cramertj floated the idea that dropping these unused parameters early could be regarded as a useful feature for The main motivation given, as I understand it, is that each keeping such parameters alive (for their later dropping) adds to the space overhead of Futures, and may be impeding other optimizations downstream. My reaction to the idea of treating the current behavior as "the specification" rather than "a bug" is as follows: I am a bit nervous about trying to add a feature ( I do understand the idea of "there is expense associated with these droppable parameters, in terms of the Future having to carry them around. We can avoid that expense by dropping them early." My question is whether that optimization (which we should be able to apply to non-droppable parameters with no semantically observable effect) outweighs the conceptual cost for people trying to use the feature. I suggested in the meeting that I would be more amenable to adding some new feature that allows one to specify this intention explicitly for a parameter. E.g. an attribute like
|
We discussed this on this week's lang team meeting but I think we didn't reach any consensus. Here are some of my own thoughts:
|
I think there's great value in matching the expectation that developers have. However, I don't think that many developers have an expectation about the order in which unused arguments are dropped. In the end, I think that most Rust developers don't have an intuition about when unused parameters are dropped, and the cases where the effects would be observable are a much smaller set of circumstances. If and when developers do notice and learn about it (which I think will be rare) they have simple and obvious workarounds (manually calling |
I think a key here is that if this differs from normal functions the time between noticing an effect from it and learning about why and how to workaround it must be kept as short as possible. The only reason I even learnt about this issue was because of the linked thread on urlo from a user of the |
Hm, looking at that code that actually seems like the behavior I would expect-- trying to receive from a channel that will never have any messages sent to it again should result in a However, it is certainly useful feedback to know that people have noticed this in practice, and it's helpful to know that it was confusing "in the wild." I'm pretty split in my thoughts on this. |
In the code from the user forum, it seems to me there is a logic error. The user wants to select on 2 channels, detecting which one closes first, while expecting them to close both at the same time (when the fn having the senders ends). This doesn't make sense, so I don't think it's rusts fault if this breaks. In an ideal world there would be a compiler warning or lint detecting logic errors, but that might not land before another 15y I suppose. As a user I expect rust to generate the most performant code. If I'm not using something, it doesn't need to stay around. Channels seem like a special case here where dropping has side effects elsewhere. That seems to be a rather niche situation to me. More niche than having parameters forced onto you when implementing traits. It seems reasonable to demand that people using drop for side effects must be aware implementation details like early dropping of unused params. I wonder why unused parameters are not dropped early in sync fn. Would it not also allow optimization there? Could we test if it would break any existing code if drop early was consistent behaviour all over? Since it's easy to opt out (manual drop), and gives most performant result by default, this seems like the ideal solution? I see the following possibilities:
I think this list is ordered in terms of advantages, but the disadvantages differ:
Personally (4) would make me kind of sad. (1) seems interesting but probably is a lot of work and it might be to much of a breaking change, which we probably won't know before doing some serious testing? (2) and (3) are compromises. Question, do we pay unnecessary costs right now for sync fn? Do we allocate space on the stack or in registers for parameters that are never used? If so, option (2) also makes me kind of sad... |
Stack values that are bound to a variable are dropped at the end of scope. If function arguments were guaranteed the same drop order as local variables you could technically take in a The unintuitive thing is that not binding an argument ( Is argument drop order actually defined anywhere currently? From the issues noticed on the linked PR it seems like it's not the same as local variables.
As far as I'm aware a trait function's contract doesn't include the drop order of its arguments, so this seems like an implementation detail that can't be relied on anyway.
This appears to be based on a version of "async fn in traits" where you cannot implement the function via some form of |
I think the rules for formal parameters are the same as local variables. (There may be some discrepancies from ReScope implementation artifacts within In both formal parameters and local bindings, the interaction between
Update: Okay, I realized in a meeting today that the handling of temporary R-values leads to the very case under discussion now, where local Namely, if you do
|
Some code, especially unsafe code, can depend on objects not being dropped for correctness. Having a special rule for drop behavior in one very specific case, with no motivation for picking out that case other than an implementation quirk, seems like a really bad idea. If there's really a desire to optimize closure size at the cost of inconsistent behavior, then early drop should apply to all variables that aren't carried across suspend points, not just unused parameters. |
@comex there is motivation for
I don't particularly disagree with this, but it would be an incredibly complex and taxing addition to the language, especially this late in the process. |
(it would also be much more user-visible, and would likely impact a significant amount of code, unlike the current proposed change) |
FWIW, I expect parameters to behave as a "magic" Maybe the difference in behavior here is surprising, but we have it, and authors know to keep this in mind when holding an e.g. I definitely do not expect the addition of the use of a usable binding (that is, one that isn't |
This seems to solve all the performance questions for the price of users having to know that the two syntaxes don't behave the same. I must say I like it. There won't be any free cake whichever way we take this. |
We discussed this at the last lang team meeting and decided to change the drop order of However, in the case that we're not able to come up with a satisfactory implementation prior to resolving the other blockers for |
@cramertj Sorry if this is a noob question, but is the drop order for non-async functions: |
No, the performance gain being discussed here is not achievable by using |
Would that be a viable future change (for sync and async) to put through Crater? (As iirc parameter drop order isn't actually specified and it would make it more consistent with It doesn't need to be, but it seems desirable on the surface. |
@CAD97 That would be a breaking change to a pretty significant amount of existing code, a significant chunk of which is probably |
OTOH, if the behavior were changed in an edition, it would be a relatively straightforward automatic migration... |
Assigning myself again since I have an open PR for this. |
[wg-async-await] Drop `async fn` arguments in async block Fixes rust-lang#54716. This PR modifies the HIR lowering (and some other places to make this work) so that unused arguments to a async function are always dropped inside the async move block and not at the end of the function body. ``` async fn foo(<pattern>: <type>) { async move { } } // <-- dropped as you "exit" the fn // ...becomes... fn foo(__arg0: <ty>) { async move { let <pattern>: <ty> = __arg0; } // <-- dropped as you "exit" the async block } ``` However, the exact ordering of drops is not the same as a regular function, [as visible in this playground example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=be39af1a58e5d430be1eb3c722cb1ec3) - I believe this to be an unrelated issue. There is a [Zulip topic](https://rust-lang.zulipchat.com/#narrow/stream/187312-t-compiler.2Fwg-async-await/topic/.2354716.20drop.20order) for this. r? @cramertj cc @nikomatsakis
[wg-async-await] Drop `async fn` arguments in async block Fixes rust-lang#54716. This PR modifies the HIR lowering (and some other places to make this work) so that unused arguments to a async function are always dropped inside the async move block and not at the end of the function body. ``` async fn foo(<pattern>: <type>) { async move { } } // <-- dropped as you "exit" the fn // ...becomes... fn foo(__arg0: <ty>) { async move { let <pattern>: <ty> = __arg0; } // <-- dropped as you "exit" the async block } ``` However, the exact ordering of drops is not the same as a regular function, [as visible in this playground example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=be39af1a58e5d430be1eb3c722cb1ec3) - I believe this to be an unrelated issue. There is a [Zulip topic](https://rust-lang.zulipchat.com/#narrow/stream/187312-t-compiler.2Fwg-async-await/topic/.2354716.20drop.20order) for this. r? @cramertj cc @nikomatsakis
[wg-async-await] Drop `async fn` arguments in async block Fixes rust-lang#54716. This PR modifies the HIR lowering (and some other places to make this work) so that unused arguments to a async function are always dropped inside the async move block and not at the end of the function body. ``` async fn foo(<pattern>: <type>) { async move { } } // <-- dropped as you "exit" the fn // ...becomes... fn foo(__arg0: <ty>) { async move { let <pattern>: <ty> = __arg0; } // <-- dropped as you "exit" the async block } ``` However, the exact ordering of drops is not the same as a regular function, [as visible in this playground example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=be39af1a58e5d430be1eb3c722cb1ec3) - I believe this to be an unrelated issue. There is a [Zulip topic](https://rust-lang.zulipchat.com/#narrow/stream/187312-t-compiler.2Fwg-async-await/topic/.2354716.20drop.20order) for this. r? @cramertj cc @nikomatsakis
Unused arguments to
async fn
are not moved into the resulting generator so are dropped before the future runs, here's some example psuedo-code demonstrating this (full running playground example here):which gives the output:
I found this because of a related issue with
futures-await(0.1)
posted to urlo, it's at the very least surprising behaviour that needs documenting if not a bug.The text was updated successfully, but these errors were encountered: