17 ms·
What memory model should Rust use?
- DeathArrow 5y ago>Rust's growing connection to the deep embedded world rules out the memory models of dynamic languages such as Java and Javascript. Since when is Java a dynamic language?
- deleted 5y ago[deleted]
- pharmakom 5y agoJava is actually pretty dynamic under the hood. You can create classes on the fly, write byte code, reflect, cast at runtime… in comparison all Rust abstractions are static, hence compiled away.
- deleted 5y ago[deleted]
- Banana699 5y agoMaybe mentioning it in the same breath as Javascript rubbed you the wrong way, but Java _is_ pretty dynamic. Object being a valid write destination for any reference means the VM has to maintain type information in all objects (small 'o'), which is a distinctively dynamic thing to do. I mean, simply declare all variables to be Object and you can see a faint shadow of a python-like class system lurking underneath the static types. The fact that nobody does this (I hope!) is of no use to the VM, the semantics say it must do this. All object allocations are on the heap (and only ever moved to stack if escape analysis proves they could), all methods are dynamically dispatched. Hell, even class loading (i.e. linking libraries) is done at startup, which means you can recompile libraries without re-linking the client code which uses them.
- pjmlp 5y agoI can do similar stuff with C++, so it is a dynamic language now?
- Banana699 5y agoThe reference Python interpreter is implemented in C, does that invalidate the common sense observation that python is dynamic while C is static ? What counts is the default zero-effort mode. Sliding into "All languages are turing complete so all distinctions are meaningless" territory is very rarely helpful for conversations. Sure C++ offers you both stack and heap allocations, virtual and ordinary methods, static types and std::any for values of unknown/irrelevant runtime type. But the default zero-effort thing to do is to use statically-typed stack-allocated objects with non-virtual methods. Java only offers two of these things and one of them needs to be explicitly stated upfront. And, furthermore, Java's runtime still maintains the full machinery needed to do dynamic typing and dynamic method dispatch (the final modifier on methods is purely a promise to javac, the JVM has no concept of a method call that doesn't involve looking up the class it refers to and potentially all of its superclasses), it doesn't matter that you don't use it, your calling code or called code might, and the specification says that it can. C++ objects compile to dumb bit buckets that contain exactly what's asked of them and nothing more. No matter what you do, you can never have all class types in C++ share a common ancestor, and the ones you can will know nothing about their parents unless specifically asked to with 'virtual', they can only remember this when allocated on the heap.
- pjmlp 5y agoYeah, because there isn't such thing as C++ language runtime, RTTI, dynamic_cast<>() and loading C++ classes via shared objects. /s
- Banana699 5y ago>RTTI, dynamic_cast<>() I don't get the snark, these things are only allowed for virtual-method-containing classes, something I explicitly mentioned and said it doesn't count because it's not on by default, not idiomatic to be on by default, doesn't exist in binaries or at runtime when not in the source, and is extremly limited compared to the rich metadata every java object hold no matter what you do or say (so even objects of final classes have it). >loading C++ classes via shared objects A capability of the underlying operating system, which _is_ a "runtime", just not a standard or required part of the language, and shared objects or dynamic libraries know a tiny drop of water about their source compared to the ocean of metadata in every .class file and the runtime representation of them. >C++ language runtime What does that even mean in this context ? I have heard it said to refer to the standard library, which is ridiculous in our context because it's neither required (let alone mandatory) part of the language nor carries information or controls execution of the calling code like the java runtime does. I have heard it being said to refer the variant of C++ that Microsoft runs on the CLR, which might as well be C# in this context. The closest thing would be the Exception Handling Mechanism and the runtime info it requires, but that's again extremely limited compared to what java does and a qualitatively different thing. Dumb tables of pointers or return addresses can never be comparable to the very rich representation of types and other metadata about code that the JVM or the CLR does.
- johnnycerberus 5y agoThe runtime is highly dynamic. You can do trickery with class loading or wizardry with bytecode manipulation.
- pjmlp 5y agoGiven the use of COM and ability to create code pages and turn them into executable code on the fly, C and C++ are also dynamic then.
- Banana699 5y ago>C and C++ are also dynamic then More correctely : C/C++ + Windows, because Windows is the runtime implementing the dynamicity, the language standards alone say nothing about generating executables or operating on them, for all they care you can generate .class files from them and obtain the same benfits of java classes, it still would be the java runtime doing all the work. Java, in contrast, defines the executable generation and runtime loading to be part of the language semantics, anything you write that can't do what they say is by defintion not a "java runtime". It's like the difference between the process abstraction in Erlang and the process abstraction in linux-hosted C.
- steveklabnik 5y agoI am woefully under-informed about the LKMM, but one useful comment I can contribute to this discussion is that the recommended strategy here would be extremely difficult to implement, because you can use both the relaxed and consume orderings in safe Rust today, and so making them unsafe would be a breaking change. Furthermore, because they're standard library constructs, they are not straightforward to change in an edition either.
- zozbot234 5y agoWhat the article suggests seems to be more along the lines of a clippy lint warning agaunst use of these orderings, which wouldn't be a breaking change.
- steveklabnik 5y agoI am vaguely uncomfortable with this idea for reasons I am struggling to put into words, but that is a more viable path, yeah.
- saghm 5y agoSoft-deprecating something in the standard library with a clippy lint feels a bit off for me too; I think that if something in the standard library is going to be discouraged, it should be done in the standard library itself (e.g. through documentation and/or using the deprecation warning). Clippy is absolutely phenomenal, but I tend not to think of it as something that's intended to be required by all users. Using it as a way to keep track of discouraged std types feels like it could easily lead to fragmentation where users stop looking to the standard library as the source of truth for its own contents.
- dochtman 5y agoAnd after some period of that it could be made unsafe in a future edition. So it’s not a totally crazy idea, but the payoff would have to be quite something.
- 5y ago
- Hercuros 5y agoRelegating parts of the memory model with unclear/hard-to-specify semantics (i.e. relaxed or consume) to the "unsafe" part of the language is an interesting idea, but it kind of goes against the spirit of "unsafe" (this point is debatable, of course). The purpose of the unsafe keyword is to mark code that could cause safety issues (undefined behavior) if its expectations about the surrounding code are not met. (e.g., an unchecked array access expects its index to be in bounds). With unlimited time and resources, it should be possible to give a mathematical proof about those unsafe blocks, showing that their expectations are met and that undefined behavior will not actually occur (by for example proving that the array index supplied will always be in bounds). But since relaxed and consume have unclear semantics, it hard to formally reason about the correctness of any code that uses them (and it is also hard to build tools to do that checking, as mentioned). So in an ideal world, we would have a 100% clear semantics for all parts of the language, _including_ the unsafe fragment. Unsafe should not be a place to put poorly specified languages features. Of course, we don't live in this ideal world, but I would rather hope for fixing the memory model so that it be made completely formal and precise (if necessary, giving up some currently allowed compiler/hardware optimizations if they're more trouble than they are worth). As an extreme example, sequential consistency is easy to understand as long as you don't mix it with other types of atomic accesses and avoid non-atomic data races.
- nybble41 5y ago> The purpose of the unsafe keyword is to mark code that could cause safety issues (undefined behavior) if its expectations about the surrounding code are not met. That is one use of the `unsafe` keyword. You see this version in the qualifiers for unsafe functions, where safety depends on the inputs and other context. It means that the marked function cannot be called from safe code. The `unsafe` keyword is also used to mark code blocks where the content of the block must be manually proved to be safe, even for valid inputs—this includes calls to unsafe functions, dereferencing raw pointers, etc. (Unsafe functions use this mode by default.) The same keyword is also used to mark unsafe traits and their implementations, where an incorrect implementation of the trait—even the members which are not themselves marked as `unsafe` and thus can be called from safe code—can impact the safety of other parts of the program (via unsafe code blocks or unsafe functions). An example of this would be a trait with a member which returns a raw pointer (itself a safe operation) which is then called from within an `unsafe` block and dereferenced.
- j-pb 5y agoThe shallowness of rusts correctness has pretty much killed my interest in it. Much of its purported virtue seems to be mostly lore and myth, and doesn't keep its promises under close inspection and scrutiny. For example the "Rustonomicon" is only half finished and doesn't have that much real insight that couldn't be gained from a lot of basic-ish rust tutorials. The memory model is not propery defined and only kinda sorta borrowed from C++ as the article mentions. The borrow checker has only been formally specified and veryfied in a much simplified form and only very recently [2] ,with the actual model "just being what the borrow checker implementation does". Having a fancy datalog based borrow checker is cool, but I'd much rather have a formal specifiation of the semantics. Rust took the big bang approach of building everything at once, implementing stuff first without any formal semantics and just floating on an iceberg of implementation definedness, with endless discussions on how things COULD be formalized. That's pretty much the antithesis to rusts safety philosophy; it's teaching water but drinking barrels of wine. It wants your code to be well defined and follow its rules, but its rules are completely arbitrary, not properly defined nor written down. I wish instead they had just started with a small borrow checked core language, and grown it from there, like described in Guy Steeles talk on "growing a language" [1]. I feel that Zig is doing a much better job in this regard, they have a tiny well thoughought language with a rigorous semantic description, and a good reference document. I'm sure it would be much easier to add a borrow checker to the well-defined-ness and correctness of Zig, than to make Rust well defined and correct. Super friendly community though. Who knows, maybe someday the language that people think rust is, will crystalize from that community through their collective effort, like a self fulfilling prophecy. 1. https://m.youtube.com/watch?v=lw6TaiXzHAE https://m.youtube.com/watch?v=lw6TaiXzHAE 2. https://arxiv.org/pdf/1903.00982.pdf https://arxiv.org/pdf/1903.00982.pdf
- kristoff_it 5y ago> I feel that Zig is doing a much better job in this regard Let's wait until Zig gets a proper specification before trying to argue that we're doing a good job. Rust is much more mature and I'm sure people have found, fixed and standardized things that we probably have never even experienced once, ever. Right now the compiler doesn't even implement the full language.
- 5y ago
- legerdemain 5y agoI'd kill for a "volatile" with guaranteed semantics in Rust. Possibly more than one person.
- steveklabnik 5y agoIs there a specific worry that this would solve, or is it just the general uncomfortableness with it not being a 100% guarantee?
- legerdemain 5y agoThe latter: I translate slightly off-the-beaten-path concurrency stuff to Rust, and a lot of papers and reference implementations lean on the semantics of "volatile" in C++ or Java.
- steveklabnik 5y agoCool, makes sense. Thanks :)
- NobodyNada 5y agoNote that “slightly off-the-beaten-path concurrency stuff” may not be correct in today’s world, especially if it uses ‘volatile’. This is primarily because memory accesses can now be reordered throughout the compiler/processor stack, from the optimizer down to out-of-order execution down to the memory hierarchy. In particular, C(++) guarantees that volatile reads/writes will never be reordered with one another, but they may be reordered with reads/writes to other variables [0] (and this ordering may not be consistent across CPU cores). Importantly, the x86 ISA provides much stronger guarantees than C/C++. AFAIK the only memory reordering allowed is that a write followed by a read can be swapped around. This is extremely strict (we can’t even reorder adjacent reads) and literally any other processor architecture will provide much weaker guarantees. And because C and C++ formally defined a weak memory model in 2011, modern compilers are free (even when targeting x86 chips) to perform optimizations that reorder memory accesses in ways programmers familiar with x86’s semantics might not expect. So an algorithm that worked on x86 processors 10-15 years ago may no longer be correct when compiled with a modern optimizing compiler, or when running on ARM. (If in doubt, you should be OK replacing anything volatile with an atomic and specifying sequentially-consistent ordering semantics for all operations — which you can do in Rust!) [0]: https://systemtbe.blogspot.com/2014/05/volatile-memory-barriers-and-loadstore.html?m=1 https://systemtbe.blogspot.com/2014/05/volatile-memory-barri...
- mlazos 5y agoI’m but a mere mortal but as an outside observer of rust and from my playing around with it, doesn’t the borrow checker currently handle this for you? Ie part of fearless concurrency?
- pcwalton 5y agoThe lifetimes and borrow check make sure that, no matter what order your threads execute in, nothing bad (memory-unsafe) will ever happen. But it doesn't say anything about what that order is. That's what a memory model is for. (This is handwavy and imprecise, but it's the easiest way I can explain it.)
- masklinn 5y agoI agree, the “fearlessness” is that you can’t share things which must not be shared (e.g. unprotected writables). And there are a fair amount of quite ergonomic constructs to manage concurrency. However if you start manipulating atomics directly, while the atomics themselves are safe the behaviour can get quite complicated below seqcst. The latest Crust of Rust is rather illuminating to the subject for people like me who had not had to really dive into the logic and implications.
- gpderetta 5y agoThat's very interesting. I would have intuitively expected that memory reordering could have been used to subvert memory safety. Can you point me to more discussion on the topic?
- exDM69 5y agoAtomics and mutexes constrain memory reordering. Stores and loads while holding a mutex can't be reordered so that they happen outside the mutex lock (because there are compiler/cpu memory barriers in mutex lock/unlock). Given that Rust's "shared xor mutable" is guarded with mutexes, atomics or other synchronization primitives, memory reordering can't violate Rust's memory safety guarantees (assuming the sync primitives are not buggy, of course). But this does not guarantee that your program logic is correct if you are sloppy with memory ordering. If you do two atomic stores, they are not guaranteed to occur in that order if they use "relaxed" ordering. This gets more complex if you're writing device drivers and communicating with peripheral devices using memory mapped i/o.
- bullen 5y agoI'm confused: "Inspired by the apparent success of Java's new memory model, many of the same people set out to define a similar memory model for C++, eventually adopted in C++11." - https://research.swtch.com/plmm https://research.swtch.com/plmm "Rust's growing connection to the deep embedded world rules out the memory models of dynamic languages such as Java and Javascript." So what is Rust using now, C++11 or C++20? "memory model for atomics from C++20"?!
- NobodyNada 5y agoC++’s memory model has not changed much (if at all) since C++11. The Rust documentation specifically refers you to the C++20 standard, because they had to pick some version, but the C++20 memory model is (as far as I know) the same memory model introduced with the C/C++11 standards.
- pjmlp 5y agoKind of, volatile meaning changed, but they are looking into it for C++23 as the embedded community is not happy with the outcome.
- tialaramex 5y agoBut, volatile isn't actually part of the memory model (except arguably in MSVC). And all that was changed was to deprecate things that are a bad idea. They don't mean anything different, they aren't hard errors, they're just deprecated. The embedded community in this context is people who were able to: #include <embedded_vendor/mmio_stuff.h> ... a file full of dodgy C pre-processor macros into their C++ project and are unhappy that J. F. Bastien's volatile deprecation means now their compiler points out what a terrible idea some of those macros actually are. Having the compiler shut up and silently allow this might make them feel better, but the situation they're in isn't actually improved by such a reversion at all. Removing the batteries and disabling the smoke alarm does mean you get a peaceful night's sleep, but sometimes you don't wake up after that peaceful night's sleep because meanwhile your home burned down and there was no alarm. The claim made in the proposal paper is that every C++ programmer who is including such macros definitely ensures by construction that their use of the macros is actually safe. You've probably read a bunch of real world C++ code written by people who are very confident in their ability to write correct programs, how do you feel about the likelihood that all these programs are in fact correct by construction despite using the unsafe macros? It seems that in later revisions of the proposal, they considerably reduced the scope of the un-deprecation, realising that even if you earnestly believe (interrupt_register |= ENABLE_IRQC) isn't a footgun, it's much harder to rationalise (clock_rate /= 3) and either way nobody wanted half the random places C++ allowed you to write "volatile" anyway, so deprecating those isn't actually a problem and "reverting" the deprecation just highlights yet another mysterious black box in C++ with no clear purpose.
- sufehmi 5y agoOTOH - TIL : LiveJournal.com still exists, even when giants such as Friendster, MySpace, Google Plus #cough have perished. Awesome.
- da39a3ee 5y agoWhat is a memory model?
- tialaramex 5y agoIn this context the Memory Model tells us about how memory seems to work in our concurrent program. It only matters if your program has concurrency (more than one apparently simultaneous execution context, today often because there is physically more than one CPU core) and the concurrent elements of the program are able to read or write the same memory. Notice that at the speeds CPUs run, the actual main "memory" of the computer may take a relatively long time to read from or write to and so we use multiple layers of caches further complicating the problem. We want: A model that's fast yet also a model that's understandable for merely human programmers. This turns out to be a difficult problem.
- pornel 5y agoA definition of what exactly happens when you write to RAM. It turns out to be much more complicated than "the data is just written", because there are other threads that could be reading and writing at the same time, multiple layers of inconsistent caches, and clever compiler optimizations or hardware that can change execution order of the code.
- coding123 5y agoWhat history model is that site using?
- volta83 5y agoSo many thoughts. * Rust threads run on CPU cores, but also GPU cores, FPGA cores, and a wider range of nastier hardware than Linux kernel threads run on. * The author implies throughout the text that safe Rust operations might not be allowed in unsafe Rust. This hints at a deep misunderstanding of how Rust works. * C, C++, and similar languages, do not have as a goal to be "sound". Soundness is, however, core to Rust's value proposition. * C, C++, and similar languages, do not have the goal that undefined behavior must always trap in their abstract machines. But Rust does. * C++ and similar languages do not have the goal of forever-backwards-compatibility (e.g. C++ breaks backwardward compatibility on almost every new standard release). Rust, however, guarantees that all programs written for Rust 1.0 in 2015 that have no undefined behavior will build and run correctly _forever_. * Rust has many compiler backends, e.g., LLVM-IR, GCC, Cranelift, SPIR-V, etc. Rust code is translated to the IR of those toolchains. That is, those IRs would need to be extended with the new semantics as well to be useful. > But again, this decision rests not with me, but with the Rust communty. The Rust community needs people working on formalizing the memory model further. So if they are interested, they should jump right in. They should probably start by understanding Rust as is today, learning about unsafe code, and well the blog post cites https://plv.mpi-sws.org/rustbelt/rbrlx/ https://plv.mpi-sws.org/rustbelt/rbrlx/ , but it is clear from the exposition that the author has not deeply understood that paper, so maybe they should also take a deeper look at that. All the proofs are open-source. So if they think they can extend the soundness proofs with new features that are also sound, that could be a place to start.
- littlestymaar 5y ago> * C, C++, and similar languages, do not have the goal that undefined behavior must always trap in their abstract machines. But Rust does. What do you mean ?
- volta83 5y agoThe implementation of the Rust abstract machine - miri - stops execution of Rust programs when they exhibit undefined behavior. C, C++, etc. don't even have implementations of their abstract machines. They don't have one existing as a goal. And they are happy to make certain operations exhibit undefined behavior even if that implies that it would make an implementation of the abstract machine that traps impossible. This is why even if you were to combine valgrind with address sanitizer, memory sanitizer, thread sanitizer, undefined-behavior-sanitizer, and other existing C and C++ tools, there is still a lot of classes of undefined behavior that these tools can't detect. That's fine in C and C++, but not fine in Rust. In Rust, if we add a new type of undefined behavior, the constraint is that it should be (demonstrably) possible to extend miri to detect it, such that if a user doesn't know whether some program exhibits undefined behavior for some inputs, they can just run it under miri, and miri will precisely pinpoint which part of their code exhibited undefined behavior and why, and how their program execution got there.
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]