Repository navigation
Suboptimal eq compilation on structs compared to equivalent C++ code #106269
Copy link
Copy link
Closed
Labels
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.C-bugCategory: This is a bug.Category: This is a bug.E-needs-testCall for participation: An issue has been fixed and does not reproduce, but no test has been added.Call 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.Issue: 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.Relevant to the compiler team, which will review and decide on the PR/issue.
Description
The eq implementation produces worse code than the equivalent C++ code (Rust, C++)
I tried this code:
pub struct S {
a: u8,
b: u8,
c: u8,
d: u8,
}
pub fn eq(s1: &S, s2: &S) -> bool {
s1.a == s2.a && s1.b == s2.b && s1.c == s2.c && s1.d == s2.d
}I expected to see this happen:
The resulting assembly should load and compare a single u64:
example::eq:
ldr w8, [x0]
ldr w9, [x1]
cmp w8, w9
cset w0, eq
retInstead, this happened:
The assembly loads and compares each u8 individually:
example::eq:
ldrb w9, [x0]
mov w8, wzr
ldrb w10, [x1]
cmp w9, w10
b.ne .LBB0_4
ldrb w9, [x0, #1]
ldrb w10, [x1, #1]
cmp w9, w10
b.ne .LBB0_4
ldrb w8, [x0, #2]
ldrb w9, [x1, #2]
cmp w8, w9
b.ne .LBB0_5
ldrb w8, [x0, #3]
ldrb w9, [x1, #3]
cmp w8, w9
cset w8, eq
.LBB0_4:
mov w0, w8
ret
.LBB0_5:
mov w0, wzr
retMeta
rustc --version --verbose:
rustc 1.68.0-nightly (88c58e3c2 2022-12-26)
binary: rustc
commit-hash: 88c58e3c2c097ebffac425d9e080dcb1aadf790e
commit-date: 2022-12-26
host: x86_64-unknown-linux-gnu
release: 1.68.0-nightly
LLVM version: 15.0.6
Compiler returned: 0
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
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.C-bugCategory: This is a bug.Category: This is a bug.E-needs-testCall for participation: An issue has been fixed and does not reproduce, but no test has been added.Call 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.Issue: 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.Relevant to the compiler team, which will review and decide on the PR/issue.
Activity
leonardo-m commentedon Dec 29, 2022
With more aggressive compilation flags I am seeing (latest Nightly):
Kmeakin commentedon Dec 29, 2022
What flags did you use?
leonardo-m commentedon Dec 29, 2022
-O -Z mir-opt-level=4
nikic commentedon Dec 29, 2022
Looks like the first two comparisons gets flattened into one block, and that prevents detection of the whole pattern.
clubby789 commentedon Jan 2, 2023
SeparateConstSwitch(enabled atmir-opt-level >= 4) is the MIR opt which merges the short-circuiting failure paths into 1, and allowsSimplifyCFGto reduce this to a pattern which can be turned into a memcmp.scottmcm commentedon Jan 6, 2023
#106294 might improve this.
I noticed in https://rust.godbolt.org/z/q768EYdqq that it's doing
<2 x i8>dances:which might be no longer needed once it's
!noundefand it can thus just compare them both directly.There's also something going on here related to the short-circuiting that I don't understand.
If I change it to
Then it optimizes as expected
https://rust.godbolt.org/z/ThfqaW3cr
It's definitely marked
dereferenceable(4)either way, so LLVM ought to be able to move the loads early either way.nikic commentedon Jan 6, 2023
@scottmcm Short-circuiting is relevant in that moving everything into one block is generally non-profitable. There is a special pass that detects this pattern (MergeICmps) and converts it into memcmp/bcmp, which is then later expanded into a target-specific efficient comparison sequence. This is a backend IR pass, so you don't see it in
--emit=llvm-iroutput.However, it requires a pretty specific pattern, and does not handle the case where the control flow has been partially flattened and one pair of comparisons uses a select rather than a branch.
Your X86 example adds insult to injury by actually vectorizing those two i8 comparisons: https://llvm.godbolt.org/z/qPe6Kfq6e That looks pretty clearly non-profitable: https://llvm.godbolt.org/z/dT85WE8xn
Edit: Filed llvm/llvm-project#59867 for the SLPVectorizer issue.
workingjubilee commentedon Feb 20, 2023
I suppose this opt would be specifically enabled by our optimization semantics allowing spurious reads. No change since the
noundefadd, though. TheSeparateConstSwitchpass may be affected by #107009 being accepted.clubby789 commentedon Apr 13, 2024
Looks like this is fixed now, we should have a test for this if we don't already
Auto merge of rust-lang#124299 - clubby789:106269-test, r=nikic
Rollup merge of rust-lang#124299 - clubby789:106269-test, r=nikic
Unrolled build for rust-lang#124299