Skip to content
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

optimizing Use expressions inside if condition #111644

Closed
wants to merge 6 commits into from

Conversation

azizghuloum
Copy link
Contributor

First time contributor here.

While debugging the issue, I noticed that the HIR tree for the "slow" part contained Use expressions that were not handled in any special way during lowering to MIR, and thus introduced a temporary variable and lead to the performance degradation reported.

The change of course affects how MIR is generated, and thus affects a number of tests. In particular, it makes one (ui) test pass when it used to compile with error. The other tests affected, according to my (shallow) analysis, look correct.

Feedback appreciated.

Fixes #111583

@rustbot
Copy link
Collaborator

rustbot commented May 16, 2023

Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @petrochenkov (or someone else) soon.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 16, 2023
@rust-log-analyzer

This comment has been minimized.

@petrochenkov
Copy link
Contributor

r? rust-lang/compiler

@rustbot rustbot assigned davidtwco and unassigned petrochenkov May 16, 2023
@cjgillot
Copy link
Contributor

Could you add a reduced version of the code in #111583 as a mir-opt test? In the tests/mir-opt/pre-codegen directory there are several examples.

I'm a bit worried by this change. ExprKind::Use is responsible for dropping temporaries. The ui test changing is probably a symptom. Could you elaborate why this change is sound?

cc @dingxiangfei2009 as have better knowledge of MIR scoping than I

@azizghuloum
Copy link
Contributor Author

Thanks for the review @cjgillot.

I think the change is sound based on how ExprKind::Use was handled in the general case in https://github.com/rust-lang/rust/blob/master/compiler/rustc_mir_build/src/build/expr/into.rs#L287 where it's basically dropped and the source is used.

As for the UI change, the code is basically:

pub fn foo(){
    let x;
    if true && {x = 4; true} && x == 4 {}
}

which currently does not compile (x is possibly uninitialized). Now consider this equivalent function:

pub fn bar(){
    let x;
    if true {
       if {x = 4; true} && x == 4 {}
    }
}

This one does compile today but if you transform it to the equivalent function foo (which is a correct transformation), it doesn't compile. I believe that either both foo and bar should compile, or both should err, but not one and not the other.

Please correct me if I'm wrong in any of that. I could be missing something.

As for the test cases, I would add them if I'm on the right track.

@dingxiangfei2009
Copy link
Contributor

@azizghuloum Thank you for your PR! I would like to supply additional information for the review.

The purpose of ExprKind::Use is indeed for scoping in MIR. It is used for inserting drops while building MIRs, but it is an indirect effect because its sole purpose is a "marker" to insert instructions to drop temporaries while evaluating the source.

By bypassing the as_temp step, the temporaries are then dropped at a later program point because the effective terminating scope of them are higher than we would like it to be. Maybe our current test suite has not tested this yet. That could be a source of problem.

There are two interesting questions remained. One is how shall we also admin the program in #111644 (comment) and another is why the performance degradation happens. I would need some extra time on the investigation. I will be right back.

@azizghuloum
Copy link
Contributor Author

azizghuloum commented May 17, 2023

Note to self. The thir::ExprKind::Use came from a hir::ExprKind::DropTemps which was introduced here: https://github.com/rust-lang/rust/blob/master/compiler/rustc_ast_lowering/src/expr.rs#L421

From reading the code, it seemed like having a "chained" let anywhere would make it NOT drop temporaries.

So, strangely, this compiles:

#![feature(let_chains)]
pub fn foo() {
    let x;
    if let _t = 0 && true && {x = 4; true} && x == 4 {}
}

while this doesn't:

pub fn foo() {
    let x;
    if true && {x = 4; true} && x == 4 {}
}

which is leading me to think that there is more that is broken than it seemed at first.

@azizghuloum
Copy link
Contributor Author

Working on another solution which does not affect lifetimes... testing now.

@dingxiangfei2009
Copy link
Contributor

@azizghuloum I digged deeper into the code today and I would like to say that I am glad that you have kept this PR open. I believe it is still safe to unwrap the ExprKind::Use here. The reason is that the scope information has been computed in the lowering to THIR, which I overlooked. From now on, as_temp and eventually as_place will consult with the temp_lifetime scoping information to insert the drops.

cc @cjgillot

Also, the reason that employing let_chains makes the example compile is because let-chains are handled completely differently. Chaining lets can only be written and connected by &&. Exploiting this feature, the match failure cases are always routed to breaking into the else block. For example, the version with the let-chain gets compiled into this:
g2
Without let-chain and without your patch, it would look like this:
g
Now you would see that, the control flow would always take on a path on which _1 is initialized. However, this fact can only be exploited after running the ConstProp and SimplifyConstCondition MIR optimization passes which are applied only after the MIR borrow checking. Your patch, however, would successfully mitigate this issue in my humble opinion. 👍

@cjgillot
Copy link
Contributor

@azizghuloum like I asked in #111752, could you add a test for this new behaviour in the mir-opt/built directory? There are examples in that directory.

That test should involve each term in the logical expression to have stuff to drop (struct Droppy; with an empty Drop impl should be enough), so we can see that this remains correct.

This PR makes new code compile, so we'll need a signoff from t-lang.

@azizghuloum
Copy link
Contributor Author

@cjgillot is this the test you were looking for to ensure drops are handled properly? I'll add another test from issue #111583 .

@azizghuloum
Copy link
Contributor Author

@cjgillot what's a "signoff from t-lang"? Sorry, new here.

@azizghuloum
Copy link
Contributor Author

Did add the test, and as I expected, there are now two drop calls for each Droppy (one drop at top of the consequent side and one at the top of the alternate side). Dunno if this is a problem but that's why I was going for the alternative solution at #111725.

@azizghuloum
Copy link
Contributor Author

Greetings again,

I have fixed PR #111725 to

  1. perform the desired optimization but at AST->HIR lowering level (instead of HIR->MIR lowering which this one does)
  2. not skip over the Use expressions (which this one does)
  3. not emit duplicate drop calls (which this one does)
  4. have the two new tests that I've added here.

Can we review that one and close this PR please? @cjgillot @dingxiangfei2009

@davidtwco davidtwco removed their assignment Jun 23, 2023
@cjgillot
Copy link
Contributor

Replaced by #111725

@cjgillot cjgillot closed this Jun 24, 2023
bors added a commit to rust-lang-ci/rust that referenced this pull request Sep 1, 2023
…=cjgillot

Lower `Or` pattern without allocating place

cc `@azizghuloum` `@cjgillot`

Related to rust-lang#111583 and rust-lang#111644

While reviewing rust-lang#111644, it occurs to me that while we directly lower conjunctive predicates, which are connected with `&&`, into the desirable control flow, today we don't directly lower the disjunctive predicates, which are connected with `||`, in the similar fashion. Instead, we allocate a place for the boolean temporary to hold the result of evaluating the `||` expression.

Usually I would expect optimization at later stages to "inline" the evaluation of boolean predicates into simple CFG, but rust-lang#111583 is an example where `&&` is failing to be optimized away and the assembly shows that both the expensive operands are evaluated. Therefore, I would like to make a small change to make the CFG a bit more straight-forward without invoking the `as_temp` machinery, and plus avoid allocating the place to hold the boolean result as well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Collapsing two if-statements to a single if statement can result in a large performance decrease
7 participants