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

LLVM addition optimizations default to wrapping_add instead of saturating_add #114888

Closed
SUPERCILEX opened this issue Aug 16, 2023 · 11 comments
Closed
Labels
C-bug Category: This is a bug.

Comments

@SUPERCILEX
Copy link
Contributor

I tried this code:

pub fn bar(count: &mut usize, n: usize) {
    for i in 0..n {
        if *count == 0 {
            assert!(i == 0);
            unsafe {see();}
        }

        *count += 1;
    }
}

extern "Rust" {
    fn see();
}
...

.L__unnamed_1:
        .ascii  "assertion failed: i == 0"

Using *count = count.saturating_add(1); instead removes the panic.

cc @RalfJung to confirm that assuming overflow cannot happen is a valid optimization.

@SUPERCILEX SUPERCILEX added the C-bug Category: This is a bug. label Aug 16, 2023
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Aug 16, 2023
@saethlin
Copy link
Member

You should be able to use alive2 to verify if a desired transformation is correct.

@RalfJung
Copy link
Member

Not sure what the bug is here. You are pointing to a part of the assembly output. What's up with that? In a bug report, please always clearly state the behavior you see and the behavior you would have expected instead.

Are you saying LLVM should have optimized this code more?

@veber-alex
Copy link
Contributor

veber-alex commented Aug 16, 2023

Why would the compiler assume overflow can't happen?
for example If count is usize::MAX you will hit the assert on the second iteration of the loop where i=1

@RalfJung
Copy link
Member

Right, good point.

If you add *count = 0; or *count = 1; at the beginning, LLVM does optimize away the assert.

@SUPERCILEX
Copy link
Contributor Author

Why would the compiler assume overflow can't happen?

Because a language can make anything UB and by definition something being UB means the compiler can assume it won't happen.

@RalfJung my question is if overflow is UB. Seems implicitly answered by the fact that it's on this page: https://doc.rust-lang.org/reference/behavior-not-considered-unsafe.html#integer-overflow. But there's no explicit discussion of UB. If it's not, it would be nice to have an explanation for why that was chosen: seems weird to crash on debug mode on overflow but then say the compiler can't optimize on that assumption?

@erikdesjardins
Copy link
Contributor

That page lists two possible behaviors that the implementation can choose, and neither of them is UB:

Other kinds of builds may result in panics or silently wrapped values on overflow, at the implementation's discretion.

I think that's explicit enough. We don't want to imply that things are allowed to be UB unless there's a clause specifically saying they aren't.

It's not discussed further because addition is safe, so allowing it to cause UB is not on the table.

@SUPERCILEX
Copy link
Contributor Author

Rational for that decision (or a link to such documentation) would be helpful. Either way, this answers my question.

@RalfJung
Copy link
Member

RalfJung commented Aug 17, 2023 via email

@SUPERCILEX
Copy link
Contributor Author

Sure, that makes sense. I guess I'm a little confused about why these are in a gray zone: the compiler can't assume ops won't overflow, but it also won't check that for you. So if you overflow an allocation capacity for example, you'll almost certainly end up with UB in release mode which means you need to use checked_add and panic on overflow.

So neither the programmer nor the compiler may assume arithmetic won't overflow. I assume the rationale is that inserting checks into release mode is just too slow?

@RalfJung
Copy link
Member

almost certainly end up with UB in release mode

Rust guarantees no UB even in release mode. Every access to the allocation is bounds-checked after all.

I assume the rationale is that inserting checks into release mode is just too slow?

Yes. It's not so much the actual checks (is my understanding) but the fact that they break up control flow, which makes it much harder for the compiler to move code around.

rust-lang/rfcs#2635 contains some interesting discussion regarding a proposal to enable overflow checking in release mode at least for some operations.

So neither the programmer nor the compiler may assume arithmetic won't overflow.

It is definitely a bug to have overflowing addition. So any Rust verification or sanitization tool can flag overflows. However, given how easy it is to accidentally overflow, UB is just too harsh of a penalty for getting this wrong, and hence we don't have the compiler assume no-overflow.

@SUPERCILEX
Copy link
Contributor Author

Thank you! These are the details that were missing.

@Noratrieb Noratrieb removed the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Aug 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug.
Projects
None yet
Development

No branches or pull requests

7 participants