Most active commenters
  • ralfj(14)
  • gronpi(5)
  • hamcocar(5)
  • jcalvinowens(4)
  • dzaima(4)
  • jcranmer(3)
  • saagarjha(3)
  • GolDDranks(3)
  • simonask(3)

←back to thread

Tree Borrows

(plf.inf.ethz.ch)
575 points zdw | 81 comments | | HN request time: 2.117s | source | bottom
1. jcalvinowens ◴[] No.44513250[source]
> On the one hand, compilers would like to exploit the strong guarantees of the type system—particularly those pertaining to aliasing of pointers—in order to unlock powerful intraprocedural optimizations.

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...).

replies(11): >>44513333 #>>44513357 #>>44513452 #>>44513468 #>>44513936 #>>44514234 #>>44514867 #>>44514904 #>>44516742 #>>44516860 #>>44517860 #
2. ralfj ◴[] No.44513333[source]
I would agree that C's strict aliasing rules are terrible. 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. We also have an actual in-language opt-out: use raw pointers. And finally, we have a tool you can use to check your code.

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.

replies(3): >>44513873 #>>44514788 #>>44521382 #
3. Asooka ◴[] No.44513357[source]
While I can't name the product I work on, we also use -fno-strict-aliasing. The problem with these optimisations is that they can only be done safely if you can prove aliasing never happens, which is equivalent to solving the halting problem in C++. In Rust I suspect the stronger type system can actually prove that aliasing doesn't happen in select cases. In any case, I can always manually do the optimisations enabled by strict aliasing in hot code, but I can never undo a customer losing data due to miscompilation.
replies(2): >>44513415 #>>44515614 #
4. pornel ◴[] No.44513415[source]
> actually prove that aliasing doesn't happen in select cases

In the safe subset of Rust it's guaranteed in all cases. Even across libraries. Even in multi-threaded code.

replies(2): >>44514341 #>>44517425 #
5. steveklabnik ◴[] No.44513452[source]
While both involve aliasing, C's strict aliasing and Rust's aliasing are two different things. Rust pretty explicitly did not adopt the C style.

C's aliasing is based on type alone, hence its other name "type based alias analysis" or TBAA.

replies(1): >>44517331 #
6. dzaima ◴[] No.44513468[source]
Rust's aliasing rules are very different from C's.

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.

replies(1): >>44517043 #
7. kookamamie ◴[] No.44513873[source]
Agreed about C's aliasing rules. Fortran had a better set of defaults.
8. jandrewrogers ◴[] No.44513936[source]
Strict aliasing rules are useful conditional on them being sufficiently expressive and sensible, otherwise they just create pointless headaches that require kludgy workarounds or they are just disabled altogether. I don't think there is much disagreement that C strict aliasing rules are pretty broken. There is no reason a language like Rust can't be designed with much more sensible strict aliasing rules. Even C++ has invested in providing paths to more flexibility around strict aliasing than C provides.

But like Linus, I've noticed it doesn't seem to make much difference outside of obvious narrow cases.

replies(1): >>44517397 #
9. ivanbakel ◴[] No.44514234[source]
>How true is this really?

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-...

replies(1): >>44516399 #
10. oconnor663 ◴[] No.44514341{3}[source]
To elaborate on that some more, safe Rust can guarantee that mutable aliasing never happens, without solving the halting program, because it forbids some programs that could've been considered legal. Here's an example of a function that's allowed:

    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.
replies(1): >>44516578 #
11. NobodyNada ◴[] No.44514788[source]
As someone who has been writing a lot of unsafe Rust (mostly in an embedded context), I'm thrilled about and thankful for the work that you, your students, and the opsem team are doing.

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.

replies(3): >>44517236 #>>44517279 #>>44518115 #
12. jcranmer ◴[] No.44514867[source]
Take anything Linus says about compilers with a grain of salt--he writes OS kernels, not compilers, and those are pretty different domains.

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.

replies(3): >>44515164 #>>44517072 #>>44517943 #
13. tliltocatl ◴[] No.44514904[source]
It is mostly useful on arrays/numeric code, probably next to useless otherwise. Numerics people was the ones who sponsored much of compiler/optimization work in the first place, that's how strict aliasing came to be.
replies(1): >>44515020 #
14. dzaima ◴[] No.44515020[source]
I don't think the usefulness is that skewed towards numerics?

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.

replies(1): >>44521705 #
15. oconnor663 ◴[] No.44515164[source]
Here's another data point: https://lobste.rs/s/yubalv/pointers_are_complicated_ii_we_ne...

> 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.

replies(4): >>44515813 #>>44516893 #>>44518168 #>>44522183 #
16. immibis ◴[] No.44515614[source]
> which is equivalent to solving the halting problem

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?

17. jcalvinowens ◴[] No.44515813{3}[source]
That's really interesting, thanks.
18. kibwen ◴[] No.44516399[source]
> I found a claim that noalias contributes about 5% performance improvement

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.

19. ◴[] No.44516578{4}[source]
20. rayiner ◴[] No.44516742[source]
> Compiler people think aliasing matters. It very seldom does. And VLIW will never become practical for entirely unrelated reasons (read: OoO is fundamentally superior to VLIW in general purpose computing).

I love how long Linus has been right about this.

replies(2): >>44517062 #>>44518433 #
21. saghm ◴[] No.44516860[source]
When unsafe is involved, sure, but that's a pretty small fraction of Rust code as a whole. As far as I can tell, a lot of his argument seems to be more against needing language changes in C to take advantage of strict aliasing that come at the cost of expanding the circumstances where UB can occur, but I don't really see how those would apply to safe Rust when it already has a perfectly safe way to express a reference guaranteed to not have any alias, i.e. `&mut T`. If the compiler authors came up with better ways to generate optimized code with unaliased pointers, I don't see why Rust would need to make any language changes in order to take advantage of them. That doesn't necessarily mean that there there is any significant untapped potential for these sorts of optmizations of course, or that the amount of effort to identify and implement them would be worthwhile for a toolchain like LLVM that is used for far more than just Rust, but it's not clear to me why the arguments he gives would be relevant to Rust.
22. naniwaduni ◴[] No.44516893{3}[source]
The counterpoint to this is that if you're willing to accept patching upstream as an option to make "key workloads" (read: partial benchmarks) perform better, what's stopping you from adding the necessary annotations instead?

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.

replies(1): >>44517065 #
23. nhaehnle ◴[] No.44517043[source]
> don't have infinite different copies of the int64_t type

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.

replies(2): >>44517059 #>>44517421 #
24. saagarjha ◴[] No.44517059{3}[source]
Of course this is substantially less doable in C :(
replies(1): >>44517187 #
25. saagarjha ◴[] No.44517062[source]
He's right about OoO but not about aliasing.
26. saagarjha ◴[] No.44517065{4}[source]
Because they require changes in thousands or millions of lines of code.
27. jcalvinowens ◴[] No.44517072[source]
> Take anything Linus says about compilers with a grain of salt

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.
replies(1): >>44519944 #
28. nhaehnle ◴[] No.44517187{4}[source]
Fair point, yeah. The general concept should still apply, you could maybe wrap it in some macros, but it is going to be more awkward mostly because of the lack of operator overloading.
replies(1): >>44519258 #
29. gronpi ◴[] No.44517236{3}[source]
In C, you can alias pointers if they have compatible types. Not the case in Rust for mutable references. And the rules of Rust have tripped up even senior Rust developers.

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.

replies(3): >>44517796 #>>44518119 #>>44518365 #
30. gronpi ◴[] No.44517279{3}[source]
One example of MIRI not being guaranteed to handle all cases.

https://github.com/rust-lang/rust/pull/139553#issuecomment-2...

The above issue was part of diagnosing UB in Rust stdlib.

replies(1): >>44518126 #
31. gronpi ◴[] No.44517331[source]
And TBAA is much easier for programmers to wrangle than the aliasing of Rust, right? The corresponding aliasing feature for C would be _restrict_, which is rarely used.

Though Linus and Linux turns off even strict aliasing/TBAA.

replies(1): >>44518406 #
32. gronpi ◴[] No.44517397[source]
>There is no reason a language like Rust can't be designed with much more sensible strict aliasing rules.

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.

replies(1): >>44518204 #
33. smallnamespace ◴[] No.44517421{3}[source]
Typescript ecosystem calls these "branded types" (branded like cattle, presumably) which I found to be a good evocative name.
34. gronpi ◴[] No.44517425{3}[source]
It requires that the libraries you use do not have UB. If you have no unsafe, but your library does, you can get UB.

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.

replies(1): >>44518454 #
35. GolDDranks ◴[] No.44517796{4}[source]
The situation is not that bad. The rules of unsafe code were pretty badly defined back then, but they are in process of becoming a lot clearer, and like the grandparent argues, with a well-defined aliasing model like Tree Borrows, they are easier to understand than C's.

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.

replies(1): >>44518178 #
36. Validark ◴[] No.44517860[source]
Personally, I would like compilers to better exploit vectorization, which can get you 2x to 10x faster on random things within typical workloads, rather than worry about dubious optimizations that have performance improvements that may or may not be caused by changing the alignment of code and data blocks.

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.

replies(2): >>44518139 #>>44518211 #
37. kunley ◴[] No.44517943[source]
Maybe Linus by writing kernels, not compilers, has even more to say, because his use-case is much more practical than anything that compiler designers could imagine.
replies(2): >>44520924 #>>44534425 #
38. ralfj ◴[] No.44518115{3}[source]
Thank you for your kind words. :)
39. ralfj ◴[] No.44518119{4}[source]
C has an opt-out that works sometimes, if a compatible type exists. Rust has an opt-out that works always: use raw pointers (or interior mutable shared references) for all accesses, and you can stop worrying about aliasing altogether.
replies(2): >>44518264 #>>44518705 #
40. ralfj ◴[] No.44518126{4}[source]
Yeah, concurrency bugs that only occur in very specific situations are hard to track down with a pure testing tool. However, we have some ongoing work that should make Miri a lot better at this... we are just not sure yet whether we can get it to have usable performance. ;)
replies(1): >>44518298 #
41. ralfj ◴[] No.44518139[source]
Pointer aliasing information is often crucial for vectorization. So in that sense TB is a big boost for vectorization.

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. :)

42. ralfj ◴[] No.44518168{3}[source]
OTOH we have https://web.ist.utl.pt/nuno.lopes/pubs/ub-pldi25.pdf which shows that on a range of benchmarks, disabling all provenance-based reasoning (no strict aliasing, no "derived from" reasoning, only straight-forward "I know the addresses must be actually different") has a much smaller perf impact than I would ever have expected.
43. hamcocar ◴[] No.44518178{5}[source]
I am very sorry, but you do not address that TBAA, like C has by default, generally is easier than just no aliasing, like what Rust has for mutable references. This is a major difference. C code can opt into a similar kind of aliasing, namely by using _restrict_, but that is opt-in, while it is always on for Rust.

And there is newer UB as well in Rust stdlib

https://github.com/rust-lang/rust/pull/139553

replies(2): >>44518319 #>>44518355 #
44. ralfj ◴[] No.44518204{3}[source]
> The aliasing rules of Rust for mutable references are different and more difficult than strict aliasing in C and C++.

"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.

replies(1): >>44518522 #
45. imtringued ◴[] No.44518211[source]
Pointer aliasing is necessary for auto vectorization, because you can't perform SIMD if the data you're modifying overlaps with the data you're reading and since the compiler is only allowed to modify the code in a way that is legal for all inputs, it will be conservative and refuse to vectorize your code rather than break it in situations with pointer aliasing.

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.

46. hamcocar ◴[] No.44518264{5}[source]
I think that way of describing it is really weird.

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.

replies(1): >>44518327 #
47. hamcocar ◴[] No.44518298{5}[source]
>Yeah, concurrency bugs that only occur in very specific situations are hard to track down with a pure testing tool.

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.

replies(1): >>44519031 #
48. simonask ◴[] No.44518319{6}[source]
How is that bug in the stdlib related to any of this?
49. ralfj ◴[] No.44518327{6}[source]
> Can you confirm that the interaction between raw pointers and mutable references still requires care?

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.)

50. GolDDranks ◴[] No.44518355{6}[source]
> TBAA, like C has by default, generally is easier than just no aliasing [note: I assume no aliasing here to refer Rust's mutable references, not C's restrict]

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.

replies(2): >>44518661 #>>44519044 #
51. ralfj ◴[] No.44518365{4}[source]
> And the rules of Rust have tripped up even senior Rust developers.

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.

52. simonask ◴[] No.44518406{3}[source]
I don't think TBAA is easier to wrangle at all, or rather, it's almost never relevant except for allowing the compiler to make obviously, trivially true assumptions. Without it (all pointers could alias all the time), any C function taking more than one pointer argument would compile to a very surprising series of instructions, reloading from memory every single time any pointer is dereferenced.

Rust's rules are very simple and easy to get right - not least because breaking them is a compiler error.

replies(1): >>44518543 #
53. Someone ◴[] No.44518433[source]
Linus worked for Transmeta in 2003. That company built a VLIW CPU.

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.)

54. simonask ◴[] No.44518454{4}[source]
Isn't this a pretty trivial observation, though? All code everywhere relies on the absence of UB. The strength of Rust comes from the astronomically better tools to avoid UB, including Miri.
replies(1): >>44518645 #
55. hamcocar ◴[] No.44518522{4}[source]
I am very sorry, but your arguments here are terrible.

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.

replies(3): >>44518942 #>>44518975 #>>44519103 #
56. hamcocar ◴[] No.44518543{4}[source]
Hmmm.

How is your comment consistent with some C compilers enabling users to disable TBAA/strict aliasing? Like the Linux kernel does. Do those codebases fit "compile to a very surprising series of instructions,"?

57. gryhili ◴[] No.44518645{5}[source]
Miri is good, but it still has very significant large limitations. And the recommendation of using Miri is unlikely to apply to using similar tools for many other programming languages, given the state of UB in the Rust ecosystem, as recommended by

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.

replies(1): >>44519131 #
58. gryhili ◴[] No.44518661{7}[source]
Ycombinator/Hacker News is giving me trouble regarding replying, maybe the debate is not going the way that some people want it to go. Sad.
replies(2): >>44519119 #>>44528256 #
59. mgaunard ◴[] No.44518705{5}[source]
the opt-out in C is to use char*, which is specifically allowed to alias other types.

There are also GCC extensions to build other types that may alias.

replies(1): >>44519048 #
60. jojomodding ◴[] No.44518942{5}[source]
> 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

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.

61. ynik ◴[] No.44518975{5}[source]
You are not testing what you think you are testing.

"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.

62. ralfj ◴[] No.44519031{6}[source]
> it is inconsistent with "fearless concurrency" when the Rust stdlib has UB.

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++.

63. ralfj ◴[] No.44519044{7}[source]
Yes, exactly. I'd be surprised if it's 5% -- that's about the ratio of all unsafe code, and most unsafe code does not mix references and raw pointers. I think it's less than 1% of the overall code that has to worry about this, but unfortunately I don't have hard data.
replies(1): >>44519199 #
64. ralfj ◴[] No.44519048{6}[source]
That doesn't let you treat data in-place at two different types, unless one of them happens to be char. So you still need to make a copy in many cases, e.g. to convert between an array of uint32_t and an array of uint16_t.

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.

replies(1): >>44521399 #
65. ralfj ◴[] No.44519103{5}[source]
> This is different from no aliasing of Rust, where mutable references of even the same type may not alias.

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.)

66. eru ◴[] No.44519119{8}[source]
I think HN is (intentionally) delaying the ability to reply to deeply nested comments.
67. ralfj ◴[] No.44519131{6}[source]
> So, while I can imagine that Miri could be best in class, that class itself has significant limitations.

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.)

68. GolDDranks ◴[] No.44519199{8}[source]
Agreed, I said 95% just to be generous, should've mentioned that.
69. dzaima ◴[] No.44519258{5}[source]
Yeah, I meant specifically C in an ergonomic way with operators working on them and being passable to existing functions.

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..

70. sapiogram ◴[] No.44519944{3}[source]
Very cool benchmark, thanks!
71. jcranmer ◴[] No.44520924{3}[source]
That's kind of a rude thing to say, suggesting that compilers aren't real programmers. Nevertheless, it is the case that most optimizations in the compiler are motivated by real issues with somebody's code, as opposed to compiler engineers thinking up optimizations in their heads.
replies(1): >>44523330 #
72. jcalvinowens ◴[] No.44521382[source]
Thanks for replying Ralf. I'm barely qualified to have an opinion about these things, if I am at all :)

> 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.

73. mgaunard ◴[] No.44521399{7}[source]
Not if you use may alias.

Also yes, that's the point of the C++ object model, only one type may exist in one location at a time.

Since usually different register banks are used for different types, having to copy makes sense.

74. tliltocatl ◴[] No.44521705{3}[source]
> pretty useful for compiler to be able to deduplicate/CSE loads/computations

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?

replies(1): >>44527480 #
75. SleepyMyroslav ◴[] No.44522183{3}[source]
I don't understand how they convinced him that code with limited set of aliasing patches remained 'correct'. Did they do 'crash ratio' monitoring? Disclaimer I had to do that in gamedev on 'postlaunch' for couple of projects. Pretty nasty work.
76. kunley ◴[] No.44523330{4}[source]
So, if my sentence is rude, and "take anything Linus says about compilers with a grain of salt" isn't, then I definitely quit this discussion.
replies(2): >>44524193 #>>44527040 #
77. jcranmer ◴[] No.44524193{5}[source]
I'm suggesting that Linus isn't the best person to be used as an expert in compilers because his field isn't in compilers.

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.

78. dwattttt ◴[] No.44527040{5}[source]
Your statement

> by writing kernels... his use-case is much more practical than anything that compiler designers could imagine.

States that compiler designers cannot even imagine the practicalities needed to write kernels. Which is quite a blanket statement to try to make and defend.

79. dzaima ◴[] No.44527480{4}[source]
L1 latency is 4 cycles typically (1 nanosecond would be closer). And of course it gets longer if you're chasing through multiple pointers.

It of course depends on the specific program, but, looking at any optimization at the level of separate impacted assembly intructions, everything other than mispredictions, division, and vectorization is "just a couple cycles" so that's not really a meaningful way to look at them.

80. tomhow ◴[] No.44528256{8}[source]
We have rate-limiters for new accounts to limit the damage that can be done by spammers, trolls and flamewar participants.

For users who have valuable contributions to make to threads and are earnest about participating positively to HN, we're happy (really, very happy) to turn off the rate limiters.

You seem to have registered multiple different accounts in order to keep participating in the thread, then hitting the rate limiters each time. Please don't do this. It's confusing for other readers and is against the guidelines [1]:

Throwaway accounts are ok for sensitive information, but please don't create accounts routinely. HN is a community—users should have an identity that others can relate to.

If you would like us to merge these accounts and turn off the rate-limiter, please email us at hn@ycombinator.com.

[1] https://news.ycombinator.com/newsguidelines.html

81. tomjakubowski ◴[] No.44534425{3}[source]
Compilers are practical too, software tools used all the time. They're just as fundamental to building software as an operating system kernel is to running software. I don't know what point you're trying to make.