You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bad codegen for comparing struct of two 16bit ints #140167
Reacted by Szymon Marczak, René Hickersberger, Juhan Oskar Hennoste, Michael Schmidt, Hantong Chen, Jonathan Spira, Jakub Beránek, Cathal, Magnus Groß and Elichai Turkel
Using transmute::<_, u32>(read_unaligned(self)) and transmute::<_, u32>(read_unaligned(other)) inside of the PartialEq impl produces the expected codegen.
eq:moveax, dword ptr [rdi]cmpeax, dword ptr [rsi] sete alretpartial_eq:movzxeax, word ptr [rdi]cmpax, word ptr [rsi]jne .LBB1_1movzxeax, word ptr [rdi+2]cmpax, word ptr [rsi+2] sete alret.LBB1_1:xoreax,eaxret
That suggests that codegen for Eq could be improved by using memcmp in some cases. (fields must be copy, no padding , probably other conditions that I'm missing).
At the time derives are expanded, field types aren’t known at all. The derive just gets tokens and it might not even see the same tokens as later stages of the compiler. So the derive can’t do the necessary checks (notably including: is bitwise equality even correct for all field types). In theory the built-in derives could expand to some magic intrinsic that’s lowered much later when types are known, but that’s a big hammer. I don’t see any fundamental reason why LLVM shouldn’t be able to do this optimization (at least for Rust) and that would help non-derived code as well. But I haven’t checked Alive2.
Note that a manual (a.x == b.x) && (a.y == b.y) compiles to the same suboptimal codegen (which makes sense, since that's what the derive expands to), but (a.x == b.x) & (a.y == b.y) (non-short-circuiting &) compiles to 32-bit load and compare.
Looks like LLVM in all three cases does 2 vectorized loads of 2 x i16, and then a compare of the vectors, but how it ANDs the resulting 2 x i1 is different: OP and && seems to do like if cmp[0] { cmp[1] } else { false }, while & does something else.
(Note the OP version's LLVM IR is different from the && version, but IIUC only in var names and debug info; they get deduplicated anyway if both are present)
// common prefix
%0 = load <2 x i16>, ptr %a, align 2
%1 = load <2 x i16>, ptr %b, align 2
%2 = icmp eq <2 x i16> %0, %1// OP and && verison
%3 = extractelement <2 x i1> %2,i640
%4 = extractelement <2 x i1> %2,i641
%_0.sroa.0.0 = select i1 %3, i1 %4, i1 false
ret i1 %_0.sroa.0.0// & verison
%shift = shufflevector <2 x i1> %2, <2 x i1> poison, <2 x i32> <i321,i32poison>
%3 = and <2 x i1> %2, %shift
%_0 = extractelement <2 x i1> %3,i640
ret i1 %_0
I assume they meant !noundef. As noted in a later comment rustc already adds that metadata when it emits LLVM IR, but SimplifyCFG drops them when turning the two branches on icmps into a select.
That's not simply a missed optimization opportunity. While the load of the second field can't load poison/undef, that property is control-dependent. For example, when matching on Option<u32> behind a reference, loading the u32 field in the Some case is also !noundef, but that part of memory will generally be undef/poison on the None branch. Thus, hoisting !noundef loads generally has to drop the !noundef metadata.
Solving this seems hard: I don't think LLVM has a way to express "loading through this pointer always reads initialized bytes" (like how dereferenceable says "loading through this pointer is always okay but may yield poison/undef"). I'm also no sure if we could apply such an attribute to Rust references -- it may break common unsafe patterns that form references to uninitialized memory. The reference conservatively calls that UB for now, but even there it's noted that this is controversial and may end up being non-UB. Miri doesn't enforce any such rule, I believe. So Rust may be unable to justify the desired optimization for the same reason why C compilers can't do it.
This might be a tangent, but it feels related and gives us some very useful building blocks:
If we had something like Plain(previously discussed in the forum) named AllBitPatternsValid<Repr=...> that was derived automatically for structs, and a const stdlib utility function std::mem::all_bit_patterns_valid<T> and std::mem::bitwise_compare<T>, could PartialEq generate code like so?
This approach wouldn’t directly address the !noundef metadata issue discussed earlier, but it could enable the compiler to perform safe, low-level comparisons for types where all bit patterns are valid, potentially improving performance.
Comparing the assembly when all_bit_patterns_valid returns false vs when all_bit_patterns_valid returns true, we can see that the compiler happily generates the wider comparisons:
It's not a full solution. Obviously it would certainly require some nuance around types like Box that are technically AllBitPatternsValid but require pointer chasing to compute equality -- perhaps something like Plain or Pod might be a better approach, but it does seem like these building blocks could be very useful.
After reviewing the LLVM issues I linked in my last comment more carefully:
The right combination of existing LLVM passes can optimize this kind of pattern at least in principle. I haven't manage to trigger MergeIcmps on the IR that rustc generates, but I believe that's just due to the very specific pattern matching that pass currently does, not a fundamental issue.
we can't actually fix this right now, because this needs the notion of a frozen load. Unless we want to expand into vector loads and comparisons (which is known to have very bad codegen)
So yeah, I was wrong in my earlier comments in this issue: LLVM could totally do this without any change to rustc's output, or even to Rust's UB rules. But current LLVM can't do it soundly and fixing that would probably require a new LLVM IR construct that does not exist today, the existing "freeze on SSA values" is not enough. (Or I guess someone could "just" fix all the cases of vector loads and comparisons having bad codegen for this use case 🙃)
Part of the problem here is that de facto memory cannot contain poison (making these transforms legal), but de jure it can (because people would like poison in memory in the future). Which gives us the awkward situation where you can't actually put poison in memory without miscompiles, while at the same time we also can't exploit the absence of poison for optimization purposes :)
Part of the problem here is that de facto memory cannot contain poison (making these transforms legal), but de jure it can (because people would like poison in memory in the future)
I'm curious as to how that would work: would each byte have 257 possible bit patterns? Presumably you would store this separately as a validity bitmask (similar to sanitizers, valgrind etc work).
But this would also break common optimised assembly implementations of strlen, memcmp, etc where you do full aligned loads into SIMD registers and deal with "read past the end" in clever ways. So I'm sceptical of people wanting to break backwards compatibility like that, at least on x86-641. And LLVM should strive for generating the best possible code for each ISA.
But following the specifics of the architecture manual trumps "there is no implementation that does that", fair enough. (I assume that when you said "de jure" you have such a specific thing to point to? Because otherwise it is not really "de jure".)
But thinking about this from another angle: the Rust type system should be rich enough to allow optimising this anyway: an T& where T contains no MaybeUninit, and is valid for all bit patterns should allow for this. Are there some missing annotations for telling LLVM about this?
Footnotes
Apple or Google on ARM? Anything could happen, they care much less about breaking backward compatibility. ↩
In that case, could a MIR optimization be an interim solution after all? The MIR for derived Eq for simple structs is very regular, so it should be possible (assuming this is valid according to UCG) to collapse several consecutive comparisons to a u32/u64 comparison, or transform to &[u8; N] and have LLVM figure out how to convert it back to an int compare.
EDIT: HN comment mentions that this is not always guaranteed to be a beneficial transformation.
Derive is generated as a macro without type knowledge, but match code generation is up to the compiler. I hoped it would already have better codegen, but as of 1.86 that's not the case:
In that case, could a MIR optimization be an interim solution after all? The MIR for derived Eq for simple structs is very regular, so it should be possible (assuming this is valid according to UCG) to collapse several consecutive comparisons to a u32/u64 comparison, or transform to &[u8; N] and have LLVM figure out how to convert it back to an int compare.
Rewriting it to loads and comparisons of wider integers needs something like LLVM's (not yet existing) "frozen load" which MIR doesn't have and which rustc can't just introduce without there being something in LLVM it can lower to. Rewriting it as a &[u8; N] comparison seems sound to me, it's essentially the same trick as LLVM's MergeICmps -- punting the "frozen load" problem to the later pass that hopefully specializes the generic memcmp call to something efficient. However, this rewrite should only be done if you know for sure that this specialization will happen, otherwise it's a pessimization. LLVM has hooks that allow MergeICmps to figure this out. In theory rustc could also access those hooks, but this may be tricky because MIR passes may run before a codegen backend is even loaded, and the same optimized MIR can be lowered to LLVM IR several times under different compiler flags that may affect whether any given byte comparison will be efficient or not. (Not to mention that the same optimized MIR can be passed to multiple codegen backends, so it really ought to be LLVM-independent.)
Rewriting it to loads and comparisons of wider integers needs something like LLVM's (not yet existing) "frozen load" which MIR doesn't have and which rustc can't just introduce without there being something in LLVM it can lower to.
Why would that be necessary if we restrict this to derived Eq impls? I would expect it to be library UB to pass an uninitialized struct to a derived Eq impl, so it would be fine for the derive impl to be changed in a way that causes compiler UB in that case. This wouldn't apply to manually implemented Eq however as those may have chosen to not make this library UB.
Reacted by Arvid Norlander, Hantong Chen, Nadrieril and scottmcm
I agree, if it's restricted to derived impls it's fine, I just hadn't considered the possibility of restricting a MIR pass to code from derived impls. It could be tricky w.r.t. pass ordering though: if you do it before inlining you may not see that the field comparisons are trivial, if you do it after inlining you have to keep track of which code came from derived impls. As discussed up-thread, an alternative is changing the derive's expansion to include something like
if core::intrinsics::can_compare_bitwise::<Self>(){
core::intrinsics::compare_bitwise(self, other)}else{// as before}
I'm going to try implementing the intrinsics idea. It's possible it will be unmergeable due to the opsem issues, but if it has compelling perf implications, that could motivate the discussion.
Trying to do this as an optimization in MIR faces essentially the same problem as asking LLVM to do it. We'd need find a way to make programs like the following UB, otherwise the "optimization" would introduce UB1 in such programs:
the derived PartialEq tries to compare scalar fields first
Do we have any evidence that that improves things? It's a bunch of complexity in the derive for a very weak heuristic -- can't even do NonZero<u32> -- that doesn't even seem consistently obviously better (if there's an [u8; 16] in there it's probably better to compare before a bool) and landed with no codegen tests...
Activity
[-]Bad codegen for comparing struct of 16bit ints[/-][+]Bad codegen for comparing struct of two 16bit ints[/+]bjorn3 commentedon Apr 22, 2025
Using
transmute::<_, u32>(read_unaligned(self))andtransmute::<_, u32>(read_unaligned(other))inside of thePartialEqimpl produces the expected codegen.folkertdev commentedon Apr 22, 2025
Trying this in C shows that it is also unable to optimize (the equivalent of)
Foo::eq, but it is able to generate good code whenmemcmpis used:https://godbolt.org/z/cqhM9hThK
That suggests that codegen for
Eqcould be improved by usingmemcmpin some cases. (fields must be copy, no padding , probably other conditions that I'm missing).hanna-kruppe commentedon Apr 22, 2025
At the time derives are expanded, field types aren’t known at all. The derive just gets tokens and it might not even see the same tokens as later stages of the compiler. So the derive can’t do the necessary checks (notably including: is bitwise equality even correct for all field types). In theory the built-in derives could expand to some magic intrinsic that’s lowered much later when types are known, but that’s a big hammer. I don’t see any fundamental reason why LLVM shouldn’t be able to do this optimization (at least for Rust) and that would help non-derived code as well. But I haven’t checked Alive2.
zachs18 commentedon Apr 24, 2025
Note that a manual
(a.x == b.x) && (a.y == b.y)compiles to the same suboptimal codegen (which makes sense, since that's what the derive expands to), but(a.x == b.x) & (a.y == b.y)(non-short-circuiting&) compiles to 32-bit load and compare.Looks like LLVM in all three cases does 2 vectorized loads of
2 x i16, and then a compare of the vectors, but how it ANDs the resulting2 x i1is different: OP and&&seems to do likeif cmp[0] { cmp[1] } else { false }, while&does something else.(Note the OP version's LLVM IR is different from the
&&version, but IIUC only in var names and debug info; they get deduplicated anyway if both are present)godbolt link with asm and llvm ir
(Maybe LLVM is assuming that the second fields could be
poisonin the&&case, so avoiding using their comparison if the first comparison is false?)bjorn3 commentedon Apr 24, 2025
Unfortunately not in the code I minimized this from that does roughly
That still results in two separate loads even with non-short-circuiting
&:https://rust.godbolt.org/z/1e47McvW1
jrmuizel commentedon May 20, 2025
memorysafety/rav1d#1400 (comment) suggests that adding
!undefto the load would fix this.hanna-kruppe commentedon May 20, 2025
I assume they meant
!noundef. As noted in a later comment rustc already adds that metadata when it emits LLVM IR, but SimplifyCFG drops them when turning the two branches onicmps into aselect.That's not simply a missed optimization opportunity. While the load of the second field can't load poison/undef, that property is control-dependent. For example, when matching on
Option<u32>behind a reference, loading theu32field in theSomecase is also!noundef, but that part of memory will generally be undef/poison on theNonebranch. Thus, hoisting!noundefloads generally has to drop the!noundefmetadata.Solving this seems hard: I don't think LLVM has a way to express "loading through this pointer always reads initialized bytes" (like how
dereferenceablesays "loading through this pointer is always okay but may yield poison/undef"). I'm also no sure if we could apply such an attribute to Rust references -- it may break common unsafe patterns that form references to uninitialized memory. The reference conservatively calls that UB for now, but even there it's noted that this is controversial and may end up being non-UB. Miri doesn't enforce any such rule, I believe. So Rust may be unable to justify the desired optimization for the same reason why C compilers can't do it.mmastrac commentedon May 22, 2025
This might be a tangent, but it feels related and gives us some very useful building blocks:
If we had something like
Plain(previously discussed in the forum) namedAllBitPatternsValid<Repr=...>that was derived automatically for structs, and aconststdlib utility functionstd::mem::all_bit_patterns_valid<T>andstd::mem::bitwise_compare<T>, couldPartialEqgenerate code like so?Fuller example at: https://play.rust-lang.org/?version=stable&mode=release&edition=2024&gist=bc3313d6c653a1c9844c49eaf735528a
This approach wouldn’t directly address the
!noundefmetadata issue discussed earlier, but it could enable the compiler to perform safe, low-level comparisons for types where all bit patterns are valid, potentially improving performance.Comparing the assembly when
all_bit_patterns_validreturns false vs whenall_bit_patterns_validreturns true, we can see that the compiler happily generates the wider comparisons:It's not a full solution. Obviously it would certainly require some nuance around types like
Boxthat are technicallyAllBitPatternsValidbut require pointer chasing to compute equality -- perhaps something likePlainorPodmight be a better approach, but it does seem like these building blocks could be very useful.16 remaining items
hanna-kruppe commentedon May 22, 2025
After reviewing the LLVM issues I linked in my last comment more carefully:
So yeah, I was wrong in my earlier comments in this issue: LLVM could totally do this without any change to rustc's output, or even to Rust's UB rules. But current LLVM can't do it soundly and fixing that would probably require a new LLVM IR construct that does not exist today, the existing "
freezeon SSA values" is not enough. (Or I guess someone could "just" fix all the cases of vector loads and comparisons having bad codegen for this use case 🙃)nikic commentedon May 22, 2025
Part of the problem here is that de facto memory cannot contain poison (making these transforms legal), but de jure it can (because people would like poison in memory in the future). Which gives us the awkward situation where you can't actually put poison in memory without miscompiles, while at the same time we also can't exploit the absence of poison for optimization purposes :)
VorpalBlade commentedon May 22, 2025
I'm curious as to how that would work: would each byte have 257 possible bit patterns? Presumably you would store this separately as a validity bitmask (similar to sanitizers, valgrind etc work).
But this would also break common optimised assembly implementations of strlen, memcmp, etc where you do full aligned loads into SIMD registers and deal with "read past the end" in clever ways. So I'm sceptical of people wanting to break backwards compatibility like that, at least on x86-641. And LLVM should strive for generating the best possible code for each ISA.
But following the specifics of the architecture manual trumps "there is no implementation that does that", fair enough. (I assume that when you said "de jure" you have such a specific thing to point to? Because otherwise it is not really "de jure".)
But thinking about this from another angle: the Rust type system should be rich enough to allow optimising this anyway: an
T&where T contains no MaybeUninit, and is valid for all bit patterns should allow for this. Are there some missing annotations for telling LLVM about this?Footnotes
Apple or Google on ARM? Anything could happen, they care much less about breaking backward compatibility. ↩
Diggsey commentedon May 22, 2025
This LLVM issue also seems very relevant: llvm/llvm-project#52930
adrian17 commentedon May 23, 2025
In that case, could a MIR optimization be an interim solution after all? The MIR for derived
Eqfor simple structs is very regular, so it should be possible (assuming this is valid according to UCG) to collapse several consecutive comparisons to au32/u64comparison, or transform to&[u8; N]and have LLVM figure out how to convert it back to an int compare.EDIT: HN comment mentions that this is not always guaranteed to be a beneficial transformation.
kornelski commentedon May 23, 2025
Maybe
rustccould take advantage ofStructuralPartialEqto special-case code generation ofPartialEq?Derive is generated as a macro without type knowledge, but
matchcode generation is up to the compiler. I hoped it would already have better codegen, but as of 1.86 that's not the case:https://rust.godbolt.org/z/7eMf9sKbs
hanna-kruppe commentedon May 24, 2025
Rewriting it to loads and comparisons of wider integers needs something like LLVM's (not yet existing) "frozen load" which MIR doesn't have and which rustc can't just introduce without there being something in LLVM it can lower to. Rewriting it as a
&[u8; N]comparison seems sound to me, it's essentially the same trick as LLVM's MergeICmps -- punting the "frozen load" problem to the later pass that hopefully specializes the genericmemcmpcall to something efficient. However, this rewrite should only be done if you know for sure that this specialization will happen, otherwise it's a pessimization. LLVM has hooks that allow MergeICmps to figure this out. In theory rustc could also access those hooks, but this may be tricky becauseMIR passes may run before a codegen backend is even loaded, andthe same optimized MIR can be lowered to LLVM IR several times under different compiler flags that may affect whether any given byte comparison will be efficient or not. (Not to mention that the same optimized MIR can be passed to multiple codegen backends, so it really ought to be LLVM-independent.)bjorn3 commentedon May 24, 2025
Why would that be necessary if we restrict this to derived Eq impls? I would expect it to be library UB to pass an uninitialized struct to a derived Eq impl, so it would be fine for the derive impl to be changed in a way that causes compiler UB in that case. This wouldn't apply to manually implemented Eq however as those may have chosen to not make this library UB.
hanna-kruppe commentedon May 24, 2025
I agree, if it's restricted to derived impls it's fine, I just hadn't considered the possibility of restricting a MIR pass to code from derived impls. It could be tricky w.r.t. pass ordering though: if you do it before inlining you may not see that the field comparisons are trivial, if you do it after inlining you have to keep track of which code came from derived impls. As discussed up-thread, an alternative is changing the derive's expansion to include something like
saethlin commentedon Nov 23, 2025
I'm going to try implementing the intrinsics idea. It's possible it will be unmergeable due to the opsem issues, but if it has compelling perf implications, that could motivate the discussion.
saethlin commentedon Nov 23, 2025
FYI starting in 1.89, the derived PartialEq tries to compare scalar fields first: https://godbolt.org/z/bT6hn9d8c
And it was pointed out in the precipitating issue #141141 that the comparison order is not documented anywhere.
scottmcm commentedon Mar 23, 2026
Do we have any evidence that that improves things? It's a bunch of complexity in the derive for a very weak heuristic -- can't even do
NonZero<u32>-- that doesn't even seem consistently obviously better (if there's an[u8; 16]in there it's probably better to compare before abool) and landed with no codegen tests...