Skip to content

slice::Iter + max on array: Regression since Rust 1.65 (LLVM 15) #129583

Closed

Description

@seritools

Code

I tried this code:

pub fn fna(x: [u8; 1024]) -> u8 {
    x.into_iter().max().unwrap()
}

pub fn fnb(x: [u8; 1024]) -> u8 {
    *x.iter().max().unwrap()
}

Godbolt

I expected to see this happen:

  1. Both functions should optimize away the unwrap
  2. (Bonus) Both functions should inline the respective iterator functions and auto-vectorize

Instead, this happened:

  1. fna has no unwrap in the assembly output, but fnb has.
  2. (Bonus) Both inline their respective iterators (according to the MIR output on the playground). fna auto-vectorizes, but fnb doesn't.

fnb generates a byte-wise max implementation. Interestingly, it keeps some information about the array length around, influencing the codegen. A byte-wise max implementation for [u8; 1024] will need to do 1023 comparisons (since the first value of the array will be the initial max result value), and 1023 is divisible by 3, so the compiler emits a loop that emits a loop that runs 341 times, doing 3 max computations each. If I change the length of the array to 1025, it will do 4 max computations each.

Even though the generated code relies on the number of array entries, it still doesn't elide the Option::unwrap check for the max result. In Rust 1.45-1.64 fnb also generates the 3-byte-at-a-time loop, but does remove the Option::unwrap.

Version it worked on

It most recently worked on: Rust 1.64. (For context: Rust 1.65 added LLVM 15.)

It (initially?) started working in 1.45 (that version added LLVM 10).

Version with regression

Any version since Rust 1.65.


Since I thought this behavior was unexpected, I polled it on mastodon before looking further into it. ATM about half of the respondents thought just like me that both should be optimized (at least the unwrap part):

image

Activity

  1. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 25, 2024
  2. changed the title [-]`slice::Iter` regression since Rust 1.65 (LLVM 15)[/-] [+]`slice::Iter` + `max` on array: Regression since Rust 1.65 (LLVM 15)[/+] on Aug 25, 2024
  3. the8472 commented on Aug 25, 2024

    @the8472
    Member

    fnb is a by-reference iterator. If you use .copied().max() it vectorizes. See #106539

  4. seritools commented on Aug 25, 2024

    @seritools
    ContributorAuthor

    @the8472 ah, that explains the vectorization difference (bonus). just to clarify - that one is not the regression, just another thing I've noticed ^^

  5. saethlin commented on Aug 25, 2024

    @saethlin
    Member

    The slice by-reference iterator fully unrolls and optimizes out the unwrap for len <= 10: https://rust.godbolt.org/z/1cT4z91eP (at length 11 LLVM still unrolls but can't figure out the unwrap)

    That's not a power of 2 so I feel like the distinction between 10 and 11 is a magic number in some LLVM optimization.

  6. saethlin commented on Aug 25, 2024

    @saethlin
    Member

    It looks like the 10 -> 11 difference is in InstCombine: https://rust.godbolt.org/z/hKof6449W
    Looks like we're getting some kind of special treatment until the loop exceeds 10 iterations.

  7. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    I-slowIssue: Problems and improvements with respect to performance of generated code.
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Aug 25, 2024
  8. nikic commented on Aug 25, 2024

    @nikic
    Contributor

    @saethlin Yeah, the problem there is that we need to look through a very deep sequence of selects to determine that the final pointer is actually guaranteed to be non-null. This is going to hit a cutoff sooner or later.

    I think we could support this without cutoffs inside SCCP, by starting from a NotConstant(Null) value lattice root for the nonnull argument and then propagating that across GEP inbounds. I'll try that when I have a chance.

  9. apiraino commented on Aug 26, 2024

    @apiraino
    Contributor

    WG-prioritization assigning priority (Zulip discussion).

    @rustbot label -I-prioritize +P-low

  10. added
    P-lowLow priority
    and removed
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Aug 26, 2024
  11. nikic commented on Aug 26, 2024

    @nikic
    Contributor

    llvm/llvm-project#106090 should fix both the unrolled and not unrolled cases.

  12. added
    llvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes
    on Aug 28, 2024
  13. added
    E-needs-testCall for participation: An issue has been fixed and does not reproduce, but no test has been added.
    on Mar 28, 2025
  14. scottmcm commented on Mar 28, 2025

    @scottmcm
    Member

    Looks like with the LLVM bump this is fixed in nightly 🎉

    Seems like a plausible thing to have a test for, since we should be able to write a fairly focused one:

    • Confirm that doing this on &[u8] does have the call{{.+}}unwrap_failed.
    • Confirm that doing this on &[u8; 1024] does not have it.

    (Notably: don't check for "no calls" or similar. There are loads of calls in the LLVM IR, like unsurprisingly to llvm.umax.)

  15. added
    regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.
    and removed
    regression-untriagedUntriaged performance or correctness regression.
    on Mar 31, 2025
  16. removed
    llvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes
    on Aug 10, 2025
  17. added 2 commits that reference this issue on Aug 22, 2025
  18. added a commit that references this issue on Aug 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.E-needs-testCall for participation: An issue has been fixed and does not reproduce, but no test has been added.I-slowIssue: Problems and improvements with respect to performance of generated code.P-lowLow priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions