Bonus: recent talk from Ralf Jung on his group's efforts to precisely specify Rust's operational semantics in executable form in a dialect of Rust: https://youtube.com/watch?v=yoeuW_dSe0o
> The problem with unsafe code is that it can do things like this:
fn main() {
let mut x = 42;
let ptr = &mut x as *mut i32;
let val = unsafe { write_both(&mut *ptr, &mut *ptr) };
println!("{val}");
}
No it can't? Using pointers to coexist multiple mutable references to the same variable is undefined behavior. Unless I'm just misunderstanding the point they're trying to make here.- Anything accepted by the borrow checker is legal
- Unsafe can express illegal / undefined behavior
- There's some set of rules, broader than what the borrow checker can check, that is still legal / defined behavior
The goal of this line of work is to precisely specify that set of rules. The outlines are clear (basically, no writable pointers should alias) but the details (interior pointers, invalidation of iterators, is it creating or using bad pointers that's bad, etc) are really hard. The previous paper in this series, on Stacked Borrows, was simpler but more restrictive, and real-world unsafe code often failed its rules (while still seeming correct). Tree Borrows is broader and allows more while still being provably safe.
> Given that aliasing optimizations are something that the Rust compiler developers clearly want to support, we need some way of “ruling out” counterexamples like the one above from consideration.
Yes, but which exact rule does it violate? What is the exact definition that says that it is UB? Tree Borrows is a proposal for exactly such a definition.
"code can do things like this" here means "you can write this code and compile it and run it and it will do something, and unless we have something like Tree Borrows we have no argument for why there would be anything wrong with this code".
You seem to have already accepted that we need something like Tree Borrows (i.e., we should say code like this is UB). This part of the paper is arguing why we need something like Tree Borrows. :)
Note that we have not yet proven this. :) I hope to one day prove that every program accepted by the borrow checker is compatible with TB, but right now, that is only a (very well-tested) conjecture.
You misunderstand the word "can". Yes, you can, in unsafe code, do that. And yes, that is undefined behaviour ;)
https://play.rust-lang.org/?version=stable&mode=debug&editio...
[1] https://github.com/Voultapher/sort-research-rs/blob/main/wri... Miri column
[2] https://github.com/rust-lang/rust/blob/6b3ae3f6e45a33c2d95fa...
All have different costs and capabilities across implementation, performance and developer experience.
Then we have what everyone else besides Rust is actually going for, the productivity of automatic resource management (regardless of how), coupled with one of the type systems above, only for performance critical code paths.
Doesn't matter if it's purely "syntaxical" because the language is garbage collected, just the fact of specifying what owns what and be explicit about multiple references is great imo.
Some sort of effects systems can already be simulated with Kotlin features too.
Programming language theory is so interesting!
And it seams to not be the case on the stable compiler version?
fn write(x: &mut i32) {*x = 10}
fn main() {
let x = &mut 0;
let y = x as *mut i32;
//write(x); // this should use the mention implicit twophase borrow
*x = 10; // this should not and therefore be rejected by the compiler
unsafe {*y = 15 };
}
rustc itself has no reason to reject either version, because y is a *mut and thus has no borrow/lifetime relation to the &mut that x is, from a compile-time/typesystem perspective.
Maybe a dumb question but couldn't you just run multiple implementations in parallel threads and whichever finishes first with a positive result wins?
How true is this really?
Torvalds has argued for a long time that strict aliasing rules in C are more trouble than they're worth, I find his arguments compelling. Here's one of many examples: https://lore.kernel.org/all/CAHk-=wgq1DvgNVoodk7JKc6BuU1m9Un... (the entire thread worth reading if you find this sort of thing interesting)
Is Rust somehow fundamentally different? Based on limited experience, it seems not (at least, when unsafe is involved...).
But in the end, it's a trade-off, like everything in language design. (In life, really. ;) We think that in Rust we may have found a new sweet spot for this kind of optimizations. Time will tell whether we are right.
C's aliasing is based on type alone, hence its other name "type based alias analysis" or TBAA.
In C you have a nuclear `restrict` that in my experience does anything only when applied to function arguments across clang & gcc, and type-based aliasing which is both not a generally-usable thing (don't have infinite different copies of the int64_t type (and probably wouldn't want such either)), and annoying (forces using memcpy if you want to reinterpret to a different type).
Whereas with Rust references you have finely-bounded lifetimes and spans and mutability, and it doesn't actually care about the "physical" types, so it is possible to reinterpret memory as both `&mut i32`/`&i32` and `&mut i64`/`&i64` and switch between the two for the same memory, writing/reading halves of/multiple values by the most bog standard Rust safe reads & writes, as long as the unsafe abstraction never gives you overlapping `&mut` references at the same time, or split a `&mut` into multiple non-overlapping `&mut`s.
It has migrated from a scope-based borrow checker to non-lexical borrow checker, and has next experimental Polonius implementation as an option. However, once the new implementation becomes production-ready, the old one gets discarded, because there's no reason to choose it. Borrow checking is fast, and the newer ones accept strictly more (correct) programs.
You also have Rc and RefCell types which give you greater flexibility at cost of some runtime checks.
I'd just like to interject for a moment. What you’re referring to as "affine types", is in fact, Uniqueness Types. The difference has to do with how they interact with unrestricted types. In Rust, these "unrestricted types" are references (which can be used multiple times due to implementing Copy).
Uniqueness types allow functions to place a constraint on the caller ("this argument cannot be aliased when you pass it to me"), but places no restriction on the callee. This is useful for Rust, because (among other reasons) if a value is not aliased you can free it and be sure that you're not leaving behind references to freed data.
Affine types are the opposite - they allow the caller to place a restriction on the callee ("I'm passing you this value, but you may use it at most once"), which is not something possible to express in Rust's type system, because the callee is always free to create a reference from its argument and pass that reference to multiple functions..
This is often explained via the "do not use more than once rule", but that's not the actual definition, and as your example shows, following that simplified explanation to the letter can cause confusion.
> because the callee is always free to create a reference from its argument and pass that reference to multiple functions..
Passing a reference is not the same thing as passing the actual value, so this does not contradict affinity.
But like Linus, I've noticed it doesn't seem to make much difference outside of obvious narrow cases.
"Rust", in this context, is "merely" "the usual invariants that people want" and "a suite of optimizations that assume those usual invariants, but not more or less".
I agree that passing a reference is not the same thing as passing the actual value. If it were, there would really be no point to references. However, it does contradict affinity. Specifically, the fact that multiple references can be created from the same value, combined with the properties of references, contradicts affinity.
> At its core, "affine" means that the type system has exchange and weakening but not contraction, and that exactly characterizes Rust's type system.
Well, the rust type system certainly does support contraction, as I can use a reference multiple times. So what is that if not contraction? It seems like rust at least does support contraction for references.
But in practice, having absolutely no contraction is not a very useful definition of affine, because no practical programming language would ever satisfy it. It prohibits too much and the language would not even be turing complete. Instead, there is usually an "affine world" and an "exponential world". (Exponential meaning "unrestricted" values that you can do whatever you want with). And the convention is that values can go from the exponential world to the affine world, but not back. So a function taking an affine value can be passed any value, but must use in in an affine way, and meanwhile but a function taking an exponential (unrestricted) value can only be passed exponential and not an affine value.
If you don't believe me, you can try using linear haskell, and notice that a function taking a linear argument can be passed a non-linear argument, but not the other way around.
If you interpret Rust's type system this way, it's natural to interpret references as exponentials. But references have the opposite convention. You can go from owned values to references, but not the other way around, which is precisely the opposite situation as the convention around linear/affine type systems. Because these systems feel very different to use and enforce very different properties, I do think it's important that we have separate names for them rather than referring to both as "affine". And the usual name for the rust-like system is "uniqueness types", see https://docs.idris-lang.org/en/latest/reference/uniqueness-t... or https://en.wikipedia.org/wiki/Uniqueness_type .
I’d be interested to see a more thorough analysis, but there is a simple way to gauge this - rip out all the parts of the compiler where aliasing information is propagated to LLVM, and see what happens to performance.
I found a claim that noalias contributes about 5% performance improvement in terms of runtimes[0], though the data is obviously very old.
https://github.com/rust-lang/rust/issues/54878#issuecomment-...
fn foo() {
let mut x = 42;
let mut mutable_references = Vec::new();
let test: bool = rand::random();
if test {
mutable_references.push(&mut x);
} else {
mutable_references.push(&mut x);
}
}
Because only one if/else branch is ever allowed to execute, the compiler can see "lexically" that only one mutable reference to `x` is created, and `foo` compiles. But this other function that's "obviously" equivalent doesn't compile: fn bar() {
let mut x = 42;
let mut mutable_references = Vec::new();
let test: bool = rand::random();
if test {
mutable_references.push(&mut x);
}
if !test {
mutable_references.push(&mut x); // error: cannot borrow `x` as mutable more than once at a time
}
}
The Rust compiler doesn't do the analysis necessary to see that only one of those branches can execute, so it conservatively assumes that both of them can, and it refuses to compile `bar`. To do things like `bar`, you have to either refactor them to look more like `foo`, or else you have to use `unsafe` code.Good question! For shared references, the answer is that they are `Copy`, so they indeed have contraction. Affinity just means that contraction is not a universal property, but some types/propositions may still have contraction. For mutable references, you can't actually use them multiple times. However, there is a desugaring phase going on before affinity gets checked, so uses of mutable references `r` get replaced by `&mut *r` everywhere. That's not using contraction, it's not literally passing `r` somewhere, it is calling a particular (and interesting) operating on `r` ("reborrowing").
Rust is not just an affine system, it is an affine system extended with borrowing. But I think it is still entirely fair to call it an affine system, for the simple fact that the language will prevent you from "using" a variable twice. "reborrowing" is just not a case of "using", it is its own special case with its own rules.
> But in practice, having absolutely no contraction is not a very useful definition of affine,
Obviously Rust has a class of "duplicable" types, called `Copy`. That's besides the point though.
> If you interpret Rust's type system this way, it's natural to interpret references as exponentials.
Why would that be natural? Mutable references are not even duplicable, so what you say makes little sense for references in general. Maybe you mean shared references -- those are just an example of a duplicable type.
Rust doesn't have a modality in its type system that would make every type duplicable, so there is no equivalent to exponentials. (In particular, `&T` isn't a modality around `T`. It's a different type, with a different representation. And as you noted, even if it were a modality, it wouldn't correspond to exponentials.)
But a type system can be affine/linear without having exponentials so I don't understand the point of this remark.
Uniqueness types seem to be all about how many references there are to a value. You can use linear/affine types to enforce such a uniqueness property (and that is indeed what Rust does), but that doesn't take away from the fact that you have a linear/affine type system.
> Because these systems feel very different to use and enforce very different properties,
I can't talk about the "feel" as I never programmed in an affine language (other than Rust ;), but in terms of the properties, what Rust does is extremely closely related to affine logics: the core property being enforced is that things do not get duplicated. My model of Rust, RustBelt, uses an affine separation logic to encode the properties of the Rust type system, and there's a lot of overlap between separation logic and linear logic. So we have further strong evidence here that it makes perfect sense to call Rust an affine language.
"I want my fuckin' money back."
"Hoom, hmm, let us not be hasty!"
"You got 48 hours to deliver or the sapling gets it, Treebeard."
When you're working with anything below the application level, C's confusing and underspecified rules about UB are almost impossible to keep track of, especially when it comes to aliasing and volatile/MMIO. The spec is so difficult to read and full of complicated cross-references that to actually get a practical answer you have to look for a random Stack Overflow post that may or may not have a correct interpretation of the spec, and may or may not address your specific problem.
Rust right now feels a lot harder to work with, because the spec isn't done. When you have a concrete question about a piece of code, like "is this conversion from an &mut to a *mut and back sound", and you try to look for documentation on it, you get either "Nobody knows, Rust aliasing model isn't defined"; a hand-wavy explanation that is not rigorous or specific; or a model like Stack Borrows or Tree Borrows that's defined a little too formally for easy digestion :)
But when I really started digging, I realized just how much cleaner Rust's semantics are. References aren't actually hard, Tree Borrows basically boils down to "while an &mut reference is live, you can only access the value through pointers or references derived from that reference". Pointer operations have straightforward semantics, there's no confusing notions of typed memory, and no UB "just because" for random things like integer overflow. It's just so much less complicated to understand than C's abstract machine.
I'm really looking forward to things like MiniRust, and to an aliasing model making it into the Reference / other documentation, because at that point I feel like unsafe Rust will be way easier to write confidently and correctly than C.
Congrats on the publication, and thanks again for the work you all have put into this.
Alias analysis is extremely important for getting good performance these days--but it should also be remembered that the biggest benefits accrue from the simplest heuristics (like two loads that use the same SSA value as the pointer must alias each other). In LLVM terms, that's BasicAA: a collection of very simple heuristics that primarily amounts to "if we can track down the allocation sites of objects, we can definitively resolve these alias queries; otherwise, we don't know."
The real question that you're trying to ask, though, is what is the value of alias analyses that go beyond the most basic, obvious tests. At the point where the alias queries are no longer trivial to solve, then it's generally the case that what you can do as a result of those queries also shrinks dramatically, pretty much to looking for code motion hazards, and the benefits you get from that are much reduced. One of the experiments I would like to do is measure the total speedup you'd get from a theoretically perfect alias analysis, and my guess is that it's somewhere in the 20% range even on non-HPC code like the Linux kernel [1].
[1] This doesn't account for the heroic optimizations, such as data-layout transformations, that you wouldn't attempt to write without a very high-quality alias analysis. But since we already know that alias analysis doesn't exist in practice, we're not going to attempt those optimizations anyways, so it's not worth including such stuff in prospective speed gains.
Also, the claims about Curry-Howard correspondence are wrong. It does not prove that rust is an affine language: https://liamoc.net/forest/loc-000S/index.xml
But Swift DOES have affine types with the "Non copyable" types that doesn't allow contraction.
And some people like to claim that the Curry-Howard correspondence proves something about their type system, but this is only true for dependently typed languages.
And the proofs aren't about program behavior.
Both clang/llvm and gcc can do alias checking at runtime if they can't at compile-time, which makes loops vectorizable without alias info, at the cost of a bit of constant overhead for checking aliasing. (there's the exception of gather loads though, where compile-time aliasing info is basically required)
And on the other hand there's good potential for benefit to normal code (esp. code with layers of abstractions) - if you have a `&i32`, or any other immutable reference, it's pretty useful for compiler to be able to deduplicate/CSE loads/computations from it from across the whole function regardless of what intermediate writes to potentially-other things there are.
> I spoke to Apple folks when their compiler team switched the default to strict aliasing. They reported that it made key workloads 5-10% faster and the fixes were much easier to do and upstream than I would have expected. My view of -fstrict-aliasing at the time was that it was a flag that let you generate incorrect code that ran slightly faster. They had actual data that convinced me otherwise.
Swift doesn't have references like Rust, and you can't even have unsafe raw pointers to variables without producing a dangling pointer, but this makes Swift more restrictive and less powerful than Rust.
Most worthwhile undefined behaviour is. If the compiler could tell whether it was happening or not, it would be defined as an error. Surely detecting whether a tree borrow violation happens is also equivalent to solving the halting problem?
Note that that comment is implying a range, from 0% improvement on some benchmarks to 5% improvement on others. It suggests that 5% is generally in the ballpark of the upper bound of what you should expect from putting noalias on mutable references, but that some specific cases could see better results.
I love how long Linus has been right about this.
There is a dual nature of linearity and uniqueness, and it only arises when there are expressions that are not linear/not unique. At the same time, they have a lot in common, so we do not have a situation that warrants separate names. It is even possible to combine both in the same type system, as the authors demonstrate.
Taken from the paper:
"Linearity and uniqueness behave dually with respect to composition, but identically with respect to structural rules, i.e., their internal plumbing."
The point of Affine logic is that it doesn't allow universal, unconstrained contraction, not that you can never do an operation that has the same properties that contraction would have in some circumstances. The same is true of Rust's type system.
The answer is basically that you were never going to, you're just externalizing the cost of making it look like your compiler generates faster code.
You can make some, though!
Basically, the idea is to define a class template NoAlias<T, Tag> that contains a single value of type T. Implement operators etc. to make the type useful in practice for working with the wrapped value. Type-based alias rules mean that an access to the value wrapped in NoAlias<int64_t, Tag1> can never alias a value wrapped in NoAlias<int64_t, Tag2>. (Tag1 and Tag2 can just be forward-declared structs that are never defined.)
One time, I even encountered a situation where this was mildly useful.
I think he's making an argument about CPU behavior here more than about compilers: if we call loads and stores which aliasing optimizations might remove "redundant", he's saying that modern machines with big caches and store buffers make those "redundant" operations so cheap they don't matter in practice for most workloads.
Of course, it's admittedly an oversimplification to reduce aliasing optimizations to simply eliminating loads and stores, you described things which go beyond that.
However, I just ran a silly little test where I added `-fno-strict-aliasing` to CFLAGS and rebuilt the world on one of my gentoo build machines, it only got 1.5% slower at compiling an allmodconfig linux kernel (gcc-14):
Before: 14m39.101s 14m42.462s 14m41.497s 14m44.540s
After: 14m54.354s 14m54.415s 14m55.580s 14m55.793s
That's on a shiny new znver5.https://github.com/rust-lang/rust/commit/71f5cfb21f3fd2f1740...
Without MIRI, a lot of Rust developers would be lost, as they do not even attempt to understand unsafe. And MIRI cannot and does not cover everything, no matter how good and beloved it is.
It should have been possible for senior Rust developers to write UB-free code without having to hope that MIRI saves them.
https://github.com/rust-lang/rust/pull/139553#issuecomment-2...
The above issue was part of diagnosing UB in Rust stdlib.
Though Linus and Linux turns off even strict aliasing/TBAA.
The aliasing rules of Rust for mutable references are different and more difficult than strict aliasing in C and C++.
Strict aliasing in C and C++ are also called TBAA, since they are based on compatible types. If types are compatible, pointers can alias. This is different in Rust, where mutable references absolutely may never alias, not even if the types are the same.
Rust aliasing is more similar to C _restrict_.
The Linux kernel goes in the other direction and has strict aliasing optimization disabled.
https://github.com/rust-lang/rust/pull/139553
This is why it may be a good idea to run MIRI on your Rust code, even when it has no unsafe, since a library like Rust stdlib might have UB.
https://users.rust-lang.org/t/polonius-is-more-ergonomic-tha...
>I recommend watching the video @nerditation linked. I believe Amanda mentioned somewhere that Polonius is 5000x slower than the existing borrow-checker; IIRC the plan isn't to use Polonius instead of NLL, but rather use NLL and kick off Polonius for certain failure cases.
fn split_at(&self, mid: usize) -> (&[T], &[T])
fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T])
What might their triples look like in separation logic?The borrow checker is supposed to be a sound static analysis, yes. I think Ralf Jung's comment at https://news.ycombinator.com/item?id=44511416 says soundness hasn't been proved relative to tree borrows yet.
> Maybe a dumb question but couldn't you just run multiple implementations in parallel threads and whichever finishes first with a positive result wins?
IIUC when you're compiling reasonably-sized programs you're already using all the cores, so parallelizing here doesn't seem like it's going to net you much gain, especially if it means you're doing a lot of extra work.
If you look into the code you linked, the problem was about accessing undefined bytes though an aliased, differently-typed pointer – something you would have hard time doing in C to begin with. MaybeUninit was a new thing back then. I think that nowadays, a senior Rust developer would clear the hurdles better.
I would like to see more effort dedicated to basic one liners that show up in real code like counting how many of a given character are in a string. E.g. `for (str) |e| count += e == '%'`. For this, LLVM spits out a loop that wants to do horizontal addition every iteration on x86-64 targets with vectors, at least. Let's focus on issues that can easily net a 2x performance gain before going after that 1-2% that people think pointer aliasing gets you.
Also, note that the paper discussed here is about the model / language specification that defines the envelope of what the compiler is allowed to optimize. Actually getting the compiler to optimize concrete real-world cases is an entirely separate line of work, and needs completely different expertise -- expertise me and my group do not have. We can only lay the framework for compiler wizards to have fun with. :)
And there is newer UB as well in Rust stdlib
"more difficult" is a subjective statement. Can you substantiate that claim?
I think there are indications that it is less difficult to write aliasing-correct Rust code than C code. Many major C codebeases entirely give up on even trying, and just set "-fno-strict-aliasing" instead. It is correct that some types are compatible, but in practice that doesn't actually help very much since very few types are compatible -- a lot of patterns people would like to write now need extra copies via memcpy to avoid strict aliasing violations, costing performance.
In contrast, Rust provides raw pointers that always let you opt-out of aliasing requirements (if you use them consistently); you will never have to add extra copies. Miri also provides evidence that getting aliasing right is not harder than dealing with other forms of UB such as data races and uninitialized memory (with Tree Borrows, those all occur about the same amount).
I would love to see someone write a strict aliasing sanitizers and run it on popular C codebases. I would expect a large fraction to be found to have UB.
Maybe this was a too convoluted way of saying this:
Loading something from main memory into a register creates a locally cached copy. This mini cache needs to be invalidated whenever a pointer can potentially write to the location in main memory that this copy is caching. In other words, you need cache synchronization down to the register level, including micro architectural registers that are implementation details of the processor in question. Rather than do that, if you can prove that you are the exclusive owner of a region in memory, you know that your copy is the most up to date version and you know when it gets updated or not. This means you are free to copy the main memory into your vector register and do anything you want, including scalar pointer writes to main memory, since you know they are unrelated and will not invalidate your vector registers.
No, they are not. You're not using a value more than once, you are borrowing it, which is an extension of affine logic but keeps true to the core principles of affinity. I have modeled multiple shared references in an affine logic (look up RustBelt), i.e. in a logic that doesn't have contraction, so we have very hard evidence for this claim.
(The other part you said about contraction and affine logics has already been successfully rebutted in some other replies so I won't repeat their points.)
In C, you do not use any special keywords to opt into or opt out of TBAA, instead it is the rule by default that one must follow. I do not consider that 'opting out'. One can disable that in some compilers by disabling 'strict aliasing', as the Linux kernel does, but that is usually on a whole-program basis and not standard.
In Rust, using raw pointers is using a different mechanism, and mutable references are always 'no aliasing'.
An example of opting in would be C's "restrict" keyword, where one opts into a similar constraint to that of Rust's 'no aliasing' for mutable references.
>use raw pointers (or interior mutable shared references) for all accesses, and you can stop worrying about aliasing altogether.
And dereferencing a raw pointer requires 'unsafe', right? And if one messee the rules up for it, theN UB.
Can you confirm that the interaction between raw pointers and mutable references still requires care? Is this comment accurate?
>It is safe to hold a raw pointer, const T or mut T, at the same time as a mutable reference, &mut T, to the same data. However, it is Undefined Behaviour if you deference that raw pointer while the mutable reference is still live.
That's just wrong. Affine logic totally can have contraction for some propositions.
Also, CH totally exists for non-dependently-typed languages -- for instance, there is a beautiful correspondence between the simply-typed lambda calculus and propositional logic. Please stop repeating claims that you apparently do not understand.
It would have been better to have prevented it in the first place, and it is inconsistent with "fearless concurrency" when the Rust stdlib has UB.
And there are categories of cases that Miri does not handle either, FFI being a clear example as far as I know.
Yes, that still requires care.
> In C, you do not use any special keywords to opt into or opt out of TBAA, instead it is the rule by default that one must follow
That's exactly the problem: sometimes you need to write code where there's more aliasing, and C just makes that impossible. It tells you to use memcpy instead, causing extra copies which cost performance.
In Rust, you can still write the code without the extra copies. Yes, it requires unsafe, but it's at least possible. (Remember that writing C is like having all code be unsafe, so in a comparison with C, if 10% of the Rust code needs to be unsafe that's still 90% less unsafe than C.)
I don't accept that statement. Rust's mutable references are statically checked, so they are impossible to get wrong (modulo compiler bugs) if you aren't using unsafe code. Using raw pointers has no no-aliasing requirements, so again, it's easier than in C.
The hard part is mixing mutable references and raw pointers. In 95% of Rust code, you are not required to do that, and you shouldn't do that. In the remaining 5%, you should understand the aliasing model. In that case, indeed, you need to know more than what TBAA requires you to know. But that's for the case where you can also _DO_ more than TBAA would allow you to do.
Yeah, even senior Rust devs make mistakes. Thanks to Miri, we can catch such mistakes. No reasonable person would expect even senior Rust devs to be magic superheroes that can write tricky unsafe code without making any mistake.
How confident are you that glibc has zero Undefined Behavior? I rather doubt it. The Rust standard library has its entire test suite (well, almost everything, except for some parts in std::fs and std::net) run through Miri. That's not a proof there's no UB in corner cases not covered by the tests, but it means we are much, much more likely to find such bugs and fix them than comparable C code.
Rust's rules are very simple and easy to get right - not least because breaking them is a compiler error.
I don’t think you were a visionary if you stated VLIW is not practical for general purpose computing after that time.
Also “VLIW will never become practical” is a stronger claim than “VLIW will never become practical for general purpose computing”, and may be up for debate. https://en.wikipedia.org/wiki/Very_long_instruction_word says
“Since the number of transistors on a chip has grown, the perceived disadvantages of the VLIW have diminished in importance. VLIW architectures are growing in popularity, especially in the embedded system market, where it is possible to customize a processor for an application in a system-on-a-chip.”
That may be untrue or no longer true, but clicking around looking a the Wikipedia pages for several of the mentioned VLIW designs, I couldn’t find conclusive evidence for that (but, as with that VLIW page, the pages I looked at may be out of date, weren’t clear as to whether chips still were in production, whether newer generations actually still are VLIW, etc.)
Unless casting or type punning through unions are used, the type system should help a lot in terms of avoiding using pointers to types that are not compatible in C. And then special care can be taken in any cases where casts are used. C++ is probably better at avoiding type casts, with all the abstractions it has.
This is different from no aliasing of Rust, where mutable references of even the same type may not alias.
Your own tool, Miri, reports that this fairly simple code snippet is UB, even though it is only the raw pointer that is dereferenced, and "a2" is not even read after assignment.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
And you know better than me that Miri cannot handle everything. And Miri is slow to run, which is normal for that kind of advanced tool, not a demerit against Miri but against the general kind of tool it is.
I am very surprised that you come with arguments this poor.
https://materialize.com/blog/rust-concurrency-bug-unbounded-...
https://zackoverflow.dev/writing/unsafe-rust-vs-zig
>If you use a crate in your Rust program, Miri will also panic if that crate has some UB. This sucks because there’s no way to configure it to skip over the crate, so you either have to fork and patch the UB yourself, or raise an issue with the authors of the crates and hopefully they fix it.
>This happened to me once on another project and I waited a day for it to get fixed, then when it was finally fixed I immediately ran into another source of UB from another crate and gave up.
Further, Miri is slow to run, discouraging people to use it even for the subset of cases that it can catch UB.
>The interpreter isn’t exactly fast, from what I’ve observed it’s more than 400x slower. Regular Rust can run the tests I wrote in less than a second, but Miri takes several minutes.
If Miri runs 50x slower than normal code, it can limit what code paths people will run it with.
So, while I can imagine that Miri could be best in class, that class itself has significant limitations.
This is simply wrong. For once, C's type system does not help you here at all. Consider the following code:
float* f = ...;
void* v = f;
long* i = v;
// Code using both *i and *f
This code has undefined behavior due to TBAA. Evidently, no unions are used in it. The type system also inserts implicit casts which can be hard to spot. This issue is not theoretical, the snippet above is taken from Quake 3's <https://en.wikipedia.org/wiki/Fast_inverse_square_root>.Further, you just can't seriously argue that C's type system helps you avoid UB in a thread about Rust. Rust's type system is the one that helps you avoid UB, and it's just so much better at that.
The code you mentions has UB, yes, but for reasons entirely unrelated to aliasing. You're reading from the address literal 0x0000000b, which is unsurprisingly not a live allocation. It's equivalent to the following C code (which similarly has UB).
printf("%d", *(int*)(0x0000000b));
The first rule of writing safe code in Rust is "don't use unsafe." This rule is iron-clad (up to compiler bugs). You broke that rule. The second rule of writing safe code in Rust is "if you use unsafe, know what you're doing." You also broke that rule since the Rust code you wrote is probably not you wanted it to be.But the implications of the second rule are also that you should know the aliasing model, or at least the over-approximation of "do not mix references and pointers." If you use raw pointers everywhere, you won't run into aliasing bugs.
> This is different from no aliasing of Rust, where mutable references of even the same type may not alias.
Aliasing in Rust is simpler when you follow the first rule, since everything is checked by the compiler. And if you use unsafe code with raw pointers, things are still simpler than in C since there is no TBAA. Only if you mix references and pointers do you get into territory where you need to know the aliasing model.
> when you're compiling reasonably-sized programs you're already using all the cores
Only on full rebuilds. I would assume most build jobs with a human in the loop only compile a handful of crates at once.
In fact as CPUs get more and more parallel we'll cross the threshold where thread count surpasses work items more often and then will come the time to get creative ;)
"let &mut a2 = &mut a;" is pattern-matching away the reference, so it's equivalent to "let a2 = a;". You're not actually casting a mutable reference to a pointer, you're casting the integer 13 to a pointer. Dereferencing that obviously produces UB.
If you fix the program ("let a2 = &mut a;"), then Miri accepts it just fine.
It is not. "fearless X" only applies to safe code. Rust was always very clear about that. It turns out that in practice, if >90% of your code are "fearless", that actually significantly reduces the amount of bugs you have to worry about, even if the remaining 10% can still have UB. (I don't have hard numbers on how much code is unsafe, AFAIK it is much less than 10%.)
But yeah, Miri cannot find all UB bugs. We are also very clear about that. But you don't have to find all UB bugs to make progress in this space compared to the status quo in C/C++.
In some cases you can use unions, but that, too, is very limited, and I am not sure it would let you do this particular case.
It is different, yes. I never said it was the same. Your claim was that the Rust model is more difficult, that needs more justification than just "it is different".
Rust allows pointers of arbitrary types to alias if you use raw pointers. That kind of code is impossible to write in C. So there is a very objective sense in which the Rust variant is more expressive, i.e., it lets you do more things. That's not necessarily a measure of difficulty, but it is at least an objective way to compare the models. (But arguably, if something is impossible to do in C, that makes it more difficult than doing the same thing in Rust... ;)
Compare that to Rust, where if you hit aliasing limitations, there's always a way to change your code to still do what you need to do: use raw pointers. In other words, every UB-free C program can be translated to a UB-free Rust program that does substantially the same thing (same in-memory representation), but the other direction does not hold: translating a UB-free Rust program to UB-free C is not always possible, you might have to do extra copies.
Objectively comparing difficulty is going to be hard, so I gave some anecdotal evidence based on what we know about making real-world Rust code compatible with our aliasing models. Do you have any evidence for your claim of the C model being simpler?
(Other people already replied very well to the other points, I will not repeat them.)
Sure -- but it's still better than writing similar code in C/C++/Zig where no comparable tool exists. (Well, for C there are some commercial tools that claim similar capabilities. I have not been able to evaluate them.)
Though wrapping in different structs doesn't seem to even make gcc & clang properly optimize utilizing the types as non-aliasing: https://godbolt.org/z/r1MT9W9db. Clang just completely fails to do anything, but gcc somehow manages to reorder the load but not do the trivial part of constant-propagating afterwards..
> The rules we are proposing for Rust are very different. They are both more useful for compilers and, in my opinion, less onerous for programmers.
My question was too vague, what I meant to ask was: what aliasing optimizations will be possible in Rust that aren't possible in C?
Example 18 in the paper is one, if I'm understanding it. But that specific example with a pointer passed to a function seems analogous to what is possible with 'restrict' in C, I'm struggling to come up with more general examples that don't involve gratuitous globals.
It seems to me like having to allow for the possibility of unsafe constrains the ability to do "heroic" optimizations such as what jcranmer described elsewhere in the thread. If that is true to some extent, is there a future where Rust might optimize more aggressively if the programmer promises not to use unsafe anywhere in the program? That's something I've always been curious about.
Yes, but is it a performance improvement significant enough? L1 latency is single cycle. Is the performance improvement from eliminating that worth the trouble it brings to the application programmer?
These functions do not memory accesses, they are both from an operational perspective essentially:
p, n -> (p, p + n)
The separation logics I've seen have all have what we might call a strong extensional calculi / shallow DSL embedding flavor. What that means is roughly that there is strong distinction between the "internal" program under scrutiny, and fairly arbitrary external reasoning about it.
I bring this up in order to say that we're very far from "what is the principal type of this expression?" type questions. There are many, many, ways one might type split_at/_mut depending on what the context requires. The interesting thing about these in Rust is really not the functions themselves, but & and &mut. Those types are stand-ins for some family of those myriad potential contexts, in the way that interfaces are always reifications of the possibilities of what the component on the other side of the interface might be.
In the "menagerie of separation logics" I was originally thinking of, there may not be singular & and &mut that work for all purposes. The most reusable form split_at indeed may be tantamount to the simple arithmetic pure function I wrote above, leaving to the caller the proof of showing whatever properties it needs are carried over from the original pointer to the new ones. Given the arithmetic relations, and the knowledge that nothing else is happening (related to the famous frame rule), the caller should be able to do this.
You're suggesting that someone who works on compilers shouldn't be used as an expert in compilers because their field is in compilers.
There is a difference between those two statements, and that difference is what makes one rude where the other isn't.