Repository navigation
slice::Iter::fold optimizes poorly for some niche optimized types. #106288
Description
Activity
- addedI-slowIssue: Problems and improvements with respect to performance of generated code.Issue: Problems and improvements with respect to performance of generated code.A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
on Dec 30, 2022 Looks like slice::Iter uses Iterator's default impl. Writing a custom one with a counted loop instead optimizes better. I'll make a PR and see what perf says.
I'm assuming this somehow has to do with NonNull and &T having the null niche value, as I don't see any other reason for the differences between *const T and NonNull.
Not on its own at least. LLVM is sensitive to details here.
https://rust.godbolt.org/z/xx6MfKnK7fold_nonnull_ptr_stdis the current impl with the bad assemblyfold_nonnull_ptr_neis that but with the niche-handling removed (by not going through intermediateOptions fromnext()). Same bad results.fold_nonnull_ptr_leis the same except!=was replaced with<in the loop condition. It results in less assembly but it still loops unnecessarily, instead of optimizing all but the last iteration away. This is weird because afaik!=was chosen intentionally in the past for being easier on the optimizer. Is that no longer true?fold_nonnull_idxfinally changes from direct pointer increments to index increments + taking index-based offsets. This finally eliminates the loop. Also weird because it means llvm can't derive the last pointer value somehow?Sounds like LLVM is at least partially to blame here right? Might be worth to try and find some minimized IR that should optimize out the loop but doesn't and open an issue on the LLVM repo.
I'm assuming this somehow has to do with NonNull and &T having the null niche value, as I don't see any other reason for the differences between *const T and NonNull.
Not on its own at least. LLVM is sensitive to details here. https://rust.godbolt.org/z/xx6MfKnK7
IR: https://rust.godbolt.org/z/59dPqob8E Without runtime unrolling: https://rust.godbolt.org/z/hss67focY
fold_val() has a reassociation failure, with something like this:
%0 = getelementptr inbounds i32, ptr %s.0, i64 %s.1 %1 = ptrtoint ptr %0 to i64 %2 = ptrtoint ptr %s.0 to i64 %3 = sub nuw nsw i64 -4, %2 %4 = add i64 %3, %1%1 - %2is4 * %s.1, but this does not fold due to missing or undesirable reassociation.fold_ptr() has a minor optimization failure, which should be fixed by #106294:
%accum.sroa.4.0.lcssa.i = select i1 %_10.i.peel.i, ptr undef, ptr %uglygepfold_nonnull_ptr_stdis the current impl with the bad assemblyfold_nonnull_ptr_neis that but with the niche-handling removed (by not going through intermediateOptions fromnext()). Same bad results.fold_nonnull_ptr_leis the same except!=was replaced with<in the loop condition. It results in less assembly but it still loops unnecessarily, instead of optimizing all but the last iteration away. This is weird because afaik!=was chosen intentionally in the past for being easier on the optimizer. Is that no longer true?!=is better. What you're seeing here is the loop being unrolled because the trip count is known and sufficiently simple. Compare: https://llvm.godbolt.org/z/YTcznesv9fold_nonnull_idxfinally changes from direct pointer increments to index increments + taking index-based offsets. This finally eliminates the loop. Also weird because it means llvm can't derive the last pointer value somehow?LLVM can derive the final value of the primary IV, but not of the result, which is
phi ptr [ null, %start ], [ %p.0.i, %bb1.i ], i.e. either null or the IV from the next-to-last iteration. This would need dedicated support.Edited: Cleaned up base IR for future reference: https://llvm.godbolt.org/z/nG8aMEnq3
LLVM can derive the final value of the primary IV, but not of the result, which is phi ptr [ null, %start ], [ %p.0.i, %bb1.i ], i.e. either null or the IV from the next-to-last iteration. This would need dedicated support.
So the issue is specific to the case where one writes a pointless fold where all previous iterations are disregarded?
(nevermind the fact that these could obviously just use slice::back)
@Sp00ph was this reduced from real code where using slice::back() wasn't obvious? If not then maybe it's too uncommon to be worth optimizing for. #106343 doesn't show much of an impact.
It's just a little synthetic test I wrote, nothing from real code. I was just trying around how much LLVM can optimize and mainly opened the issue because of the unintuitive discrepancy between the different cases.
LLVM can derive the final value of the primary IV, but not of the result, which is phi ptr [ null, %start ], [ %p.0.i, %bb1.i ], i.e. either null or the IV from the next-to-last iteration. This would need dedicated support.
🤔 peeling the first loop iteration should solve this.... but it turns out it's even simpler than that. Making the len == 0 case explicit and then turning the while into a do-while loop fixes it too.
I think this is the same root issue as #76746 (comment)
Basically, the point of
foldis to get ownership access to the accumulator. If you don't need that, then you're better off usingfor_eachwith a mutable closure instead.For example, if you write https://rust.godbolt.org/z/jrbzPExWG
pub fn fold_ref(s: &[i32]) -> Option<&i32> { let mut r = None; s.iter().for_each(|i| r = Some(i)); r }
then it optimizes down well already
example::fold_ref: test rsi, rsi lea rax, [rdi + 4*rsi - 4] cmove rax, rsi ret
- addedT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.
on Apr 5, 2023 Fixed on nightly by #106343
I tried this code:
(nevermind the fact that these could obviously just use
slice::back)(godbolt link: https://rust.godbolt.org/z/6fjzo4faW )
I expected that all of these functions produce more or less similar assembly, as all of them just need to peel the last loop iteration to be able to optimize away the whole loop body. Indeed, the first two functions optimize just fine:
The
fold_{nonnull,ref}functions however don't optimize away the loop:I'm assuming this somehow has to do with
NonNulland&Thaving the null niche value, as I don't see any other reason for the differences between*const TandNonNull<T>. It doesn't seem to be happening with all niche optimized types though, as functions like these do optimize away the loop:This is using nightly rustc on godbolt, which currently is: