Add rustc flag to disable mutable no-aliasing optimizations?

The issue is that a Rust version of -fno-strict-aliasing would do much more than the C/C++ -fno-strict-aliasing, because Rust provides much stronger guarnatees (in the form of &mut references) than C/C++ do.

If I understand correctly, you believe that a Rust version of -fno-strict-aliasing would simply be a more permissive version of the language, and that anyone who doesn't want to use it can simply ignore it.

Unfortunately, this is not the case. Consider this (contrived) program:

fn modify_vec(val: &mut Vec<u8>, f: impl FnOnce()) {
    val.push(25);
    f();
    assert_eq!(val.pop(), Some(25));
}

As a user of Rust, I can guarantee that the assert_eq! will always pass - I have a mutable reference to a vector, which means that I have exclusive access to it. This is despite the fact that I'm calling an arbitary user-provided function - if that function were to try to modify val through a raw pointer, it would be undefined behavior, and therefore unambiguously be at fault.

Note that this is completely independent of whatever optimizations that compiler chooses to perform. What's important is the fact that &mut T implies exclusive access to a value of type T. Even if a user isn't aware that raw pointers even exist in the language, they can still correctly reason about the behavior of modify_vec.

While this example might seem contrived, there are many types in the standard library that rely on &mut T being exclusive (e.g. Cell::get_mut). This is an important property of the language, regardless of whether or not the compiler chooses to exploit it for optimization purposes.

If I understand your proposal correctly, the Rust version of -fno-strict-aliasing would break the assumptions made by modify_vec. While it might seem clear that this is the 'fault' of whatever code is creating the aliasing mutable references, this will become far less clear in a complex dependency graph. Two crates may make assumptions that seem reasonable in isolation (this functiion will never be called with an aliasing mutable reference, or using aliasing mutable references will only affect my code), but that interact badly when brought together.

However, I definitely sympathsize with your concern about peace of mind. Personally, I would support a way to disable any mutable-aliasing relating optimizations, provided that having aliasing mutable references is still always U.B.. Effectively, this would be a more fine-grained version of the optimization level - unsafe code that breaks on a higher optimization level is still wrong at a lower optimization level, but you as a user are free to choose a less agressive set of optimizations.

With such an option, aliasing mutable references would still unambiguously be a bug in your code, and users would still be responsible for fixing them (with the help of tools like Miri). However, this would allow authors of binary crates to mitigate (but not eliminate!) the effects of U.B, especially for code that they don't necessarily trust to be perfectly well-written. This would be similar in spirit to how std::mem::uninitialized now panics for uninhabited types - your code still has undefined behavior, but the compiler is choosing to make it not 'as bad' as the standard allows it to be.

4 Likes

I support this completely. If Rust wants to condemn the use of the flag in the strongest possible terms, I am totally fine with that. Put big red angry sirens in front of it. Put the word "unsafe" literally in part of the flag name. As long as I can actually use the flag, I'm happy. That's literally all I want.

So you could say they get UB, but it might look different (e.g. that assertion failing), or whatever you like. I mean, -fno-strict-aliasing is UB according to the C and C++ standards, after all.

It was never about changing the language or anything for me. I just wanted the functionality to disable those optimizations so the code will behave like C or C++ would with -fno-strict-aliasing. It doesn't have to be condoned or called anything other than UB. :^)

If all you want it to disable the optimizations, not in any way change whether you're allowed to do it: you do that by talking to the optimizing backend, not the frontend. You use -C llvm-args or whatever the flags are to customize what optimization passes are run.

At this point you aren't speaking Rust. Rust is perfectly allowed to insert a spurious read and write of &mut _ as often as it wants, which will still break if it's not an exclusive reference (because threading exists, and can be used in a scoped manner even if it looks like you're calling a blocking API).

If it's still just as disallowed, and just as UB, and just as allowed to miscompile, just it won't be these specific optimization passes, customize your optimization passes, I'm sure it will be fine.

Offering a big shiny "solve your aliasing UB" button is a really bad idea, because people will use it, no matter the disclaimers. This is the same reason that we don't allow the use of unstable features on the stable compiler, even behind a compiler flag[1]. Because if something is available, people will use it and expect it to keep working. Rustc is Rust right now.

It'll be interesting to see if gccrs offers a -fno-strict-aliasing for Rust. It's much more likely they do than rustc, but I still think it's quite unlikely, actually.

[1]: yeah yeah there's one open secret backdoor

5 Likes

Rust lets you do unsafe things when you intend to do them. You can do that by writing unsafe, and by using raw pointers, and by using UnsafeCell, and similar mechanisms.

Even unsafe doesn't let you invoke undefined behavior. What it lets you do is disable the compiler's mechanisms that prevent you from invoking undefined behavior, and instead promise that you're going to avoid undefined behavior yourself.

We don't just throw around "undefined behavior" lightly. We're not making developer's lives more difficult just so that we can optimize code more aggressively. You may find it comforting that the people in Rust who work on language semantics and correctness, like @RalfJung in this thread, are very careful about trying to take people's real-world code into account, and about balancing the many needs of developers in real-world programs. We want to make it easier to write correct code, and part of that is defining certain fundamental assumptions of the language, such as &mut being exclusive. Violating those assumptions is undefined behavior. By design, it's not possible to violate those assumptions in safe code. Don't think of unsafe as letting you violate those assumptions; think of unsafe as taking personal responsibility for not violating those assumptions.

What you're asking for would break fundamental assumptions of other people's code. And the problem with a "switch" like what you're describing is that ecosystems fall to the lowest common denominator. In an ecosystem where some code has non-exclusive &mut, nobody can count on exclusive &mut.

You haven't talked about why raw pointers and UnsafeCell aren't the escape hatch you're looking for. They give you exactly the behavior it seems like you want, with zero overhead. (If they ever have non-zero overhead, that's a bug.)

That's leaving aside new abstractions like GhostCell, which will let you safely pass access to shared objects between threads, making it even easier to build shared data structures. The abstractions that let you write entirely safe code will continue to get better. But meanwhile, you will always have the ability to write unsafe code, and tell the compiler "I know what I'm doing".

You're allowed to ask the compiler to let you tightrope-walk over the cliff. You're not allowed to remove the fence and the "danger: cliff" sign for other people.

12 Likes

Actually, it would be very easy for Rust to support safe goto. Most of the interesting semantics happens at the level of MIR, which is based on a CFG representation and so can have arbitrary jumps in it. You can already get almost arbitrary goto-like behavior using break-with-value. The semantics of drop placement and so on is all very straightforward: anything that goes out of scope gets dropped when you do a non-local jump. This is already implemented in rustc, it just doesn't have a goto construct in the surface language.

2 Likes

This is rapidly getting off-topic. If anyone wants to make any next steps in this area, I suggest starting a new thread or an RFC, with a concrete proposal that takes into account all the feedback from the discussion so far.

4 Likes