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

Elide clone operation when calling iter::Cloned::advance_{back}_by() #90209

Closed
wants to merge 4 commits into from

Conversation

the8472
Copy link
Member

@the8472 the8472 commented Oct 23, 2021

This is a change in user-visible behavior. It will not only affect advance_by (which is still unstable) but also the stable nth, nth_back and Skip, Take and other adapters that make use of these methods.
It is also a bit of a test-balloon, if it gets approved there are a few more places where other side-effects could be skipped too.

I'm not sure if it is needed at this stage, but a note about side-effects could also be added to the std::iter module.

Part of #77404

@the8472 the8472 added the T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. label Oct 23, 2021
@rust-highfive
Copy link
Collaborator

r? @m-ou-se

(rust-highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 23, 2021
@cuviper
Copy link
Member

cuviper commented Oct 23, 2021

It is also a bit of a test-balloon, if it gets approved there are a few more places where other side-effects could be skipped too.

Skipping clone is possibly okay, but I would be surprised to see map closures skipped, for instance.

@the8472
Copy link
Member Author

the8472 commented Oct 23, 2021

I would be surprised to see map closures skipped, for instance.

Ideally it would be limited pure(ish) closures, but the trait system can't express that so it would have to be some approximation based on Fn and !&mut and I haven't even prototyped it yet so I think we can leave the discussion to the future. I just wanted to note the intent.

@JohnCSimon JohnCSimon added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 8, 2021
@JohnCSimon JohnCSimon added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 5, 2021
@the8472
Copy link
Member Author

the8472 commented Jan 5, 2022

After the discussion in #92543 I took another look at iter::Cloned and if we're already eliding side-effects then last() and count() could also be overridden either bei skipping cloning in all cases or conditionally on T: Copy, via specialization.

I could do that here or in a separate PR.

@m-ou-se m-ou-se added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Jan 5, 2022
@m-ou-se
Copy link
Member

m-ou-se commented Jan 5, 2022

Seems fine to me, but let's quickly get some more eyes from @rust-lang/libs-api on this. :)

@llogiq
Copy link
Contributor

llogiq commented Jan 6, 2022

rust-lang/rust-clippy#8203 attempts to tackle the perf footgun from the other side. I'm not on libs, but I would favor a two-pronged approach where we try to elide clones where possible and have the lint help people deal with the remaining cases.

With that said, I see that the current behavior is in place since 2015. However unlikely, somewhere someone could have implemented a side-effectful Clone impl and rely on all iterated elements being cloned. I would suggest at least a crater run before this is merged, so that if that someone has published on crates.io we can at least contact them before we break their code.

@the8472
Copy link
Member Author

the8472 commented Jan 6, 2022

I would suggest at least a crater run before this is merged, so that if that someone has published on crates.io we can at least contact them before we break their code.

Betas already get crater runs. Not sure if it's worth doing a separate one for this.

@joshtriplett
Copy link
Member

I'm personally hesitant to merge this, even with a crater run.

It seems to violate compositional reasoning: if people know what .cloned() does, and know what .last() does, they would then know what .cloned().last() does. (And likewise for nth and similar.)

I absolutely think it's reasonable to optimize these away for .copied(), because we know Copy can't have a side effect. But Clone absolutely can.

This is less a question of "will we break some existing code", and more that it seems like especially surprising behavior.

Are there real-world workloads in which people call .cloned() and then do operations on it that could skip the clones? What led to wanting this optimization?

(Also, whether we make this change or not, I don't think we should add the comments suggesting we'd skip other things that are more likely to have side effects, such as closures.)

@the8472
Copy link
Member Author

the8472 commented Jan 9, 2022

It seems to violate compositional reasoning: if people know what .cloned() does, and know what .last() does, they would then know what .cloned().last() does.

That depends on what the simplified mental model of cloned() is. If you think of it in terms of only wrapping next() and then running the default implementations of Iterator then yeah.
But if the idea is "it returns cloned items" then when there's no item returned no cloning needs to happen.

The problem with "know what X does" is kind of oversimplifying, because what it really does is return an adapter and adapters are more complicated than just next().

Are there real-world workloads in which people call .cloned() and then do operations on it that could skip the clones?

They're common mistakes. I mostly but not exclusively see them in advent-of-code style exercises or test code, but that suggests that a naive model that people have is only about the outcome of the iterator, not about the order in which adapters are applied. And it often is a very convenient model, a "sufficiently smart compiler std" kind of thing.

https://github.com/Simula-UiB/CRHS/blob/06ae94effdbad20e5ae0fa1fd97b8b10a7a7381d/cryptapath/src/targets/miniaes4x4.rs#L58-L63

And it can be more subtle than that. E.g. cloned().zip(...).next_back() does extra work in trimming iterators of unequal length until they're equal length before returning the tail item, and that trimming would lead to extra clones.

(Also, whether we make this change or not, I don't think we should add the comments suggesting we'd skip other things that are more likely to have side effects, such as closures.)

Well, that's what I'd try to do next, optimize away map and inspect side-effects when combined with zip, skip, take etc. Only for Fn closures and non-&mut items of course 😇 (assuming I can get specialization to do this).
And it's not only about the standard library, it would also allow 3rd-party adapters to implement advance_by in a way that skips unnecessary work.

@llogiq
Copy link
Contributor

llogiq commented Jan 9, 2022

@joshtriplett I hear you. While there is no guarantee of side effects in iterator operations in general, code might reasonably depend on them as the current situation has effectively been stable since 2015.

On the other hand, while we will soon have a clippy lint that can reduce the impact of this problem, it can only reason locally and thus will always miss some cases. And avoiding alloc churn is an attractive optimization.

So while I can certainly relate to your hesitation, we may want to at least think of a path to include these optimizations, perhaps as part of the next future edition. While not as good as getting the performance right now, it would at least give us a future to await (pun totally intended) and in the meantime we can test & benchmark with this PR.

@the8472
Copy link
Member Author

the8472 commented Jan 15, 2022

Closing this per discussion in this week's libs meeting. To summarize:

  • the change by itself might be ok, but on its own it's likely not worth it
  • a stepping stone towards more aggressive iterator optimizations (such as eliding non-essential map side-effects, even conditionally) it's too controversial as long as those optimizations would violate the as-if rule
  • if the typesystem allowed us to reliably identify side-effect-free functions (const? pure?) or we could better hint to llvm that something isn't a relevant side-effect that might be an alternative

@the8472 the8472 closed this Jan 15, 2022
@llogiq
Copy link
Contributor

llogiq commented Jan 15, 2022

Should we then document the current behavior or at least make clear that we don't offer any performance optimization at least for this edition? I might reopen #92543.

@the8472
Copy link
Member Author

the8472 commented Jan 15, 2022

Well, if we might want to make another attempt in the future then it might cause less confusion by not saying too much. On the other hand the guidance to avoid unnecessary clones can be useful. So maybe it would be better to phrase it in a way focusing on the latter part.

Dylan-DPC added a commit to Dylan-DPC/rust that referenced this pull request Mar 23, 2022
add perf side effect docs to `Iterator::cloned()`

Now that rust-lang#90209 has been closed, as the current state of affairs is neither here nor there, this at least adds a paragraph   example on what to expect performance-wise and how to deal with it to the .cloned() docs.

cc `@the8472`
@dtolnay dtolnay removed the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Sep 28, 2023
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-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants