News for hackers. Stuff that matters.
Re:Rust is the future, C is the new Assembly: Josh Triplett ... by pornel on Wednesday August 28, 2019 @01:08PM (#20821728)
> Rust is the first one I'm too stupid to understand.

That's a typical experience, but it's not because Rust is that complicated, but because it's different.

People approach Rust with their intuition from other languages, such as C's or Java's where you can have a web of objects referencing each other willy-nilly, and are stumped by Rust's insistence on clear, single ownership, and simpler tree/DAG-like structure of program's data.

Single ownership itself is simple, you just need to unlearn multiple-shared-ownership habits.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by wahern on Wednesday August 28, 2019 @01:27PM (#20821941)
Single ownership isn't a preference, it's a constraint and a limitation. A constraint (in a good way) because that's the entire point of the language--preventing you from losing track of memory ownership. It's a limitation because there are some extremely important and common relationships (e.g. putting an object in multiple containers simultaneously) that cannot be expressed in Rust's type system without using unsafe, RC boxes, etc, which create tremendous friction. Yet a low-level systems language is precisely the environment where you most want and need to express these relationships and data structures cleanly and efficiently.

A data structure isn't something you import from a module. A good program is composed of layers of bespoke data structures that culminate in a giant data structure tailored for a particular task. So the fact that you can just import a hash table or tree implementation (friction-free) is irrelevant; useful, but irrelevant because it's combining such data structures that is the primary task at hand when writing a non-trivial piece of software, and combining them necessarily results in complicated ownership dependencies.

And this is all especially true for applications that do more than transform some input into some output in one shot.

I'm not trying to criticize Rust. They took a good idea and ran with it, and expressing more complex ownership semantics is still an open problem (perhaps with no satisfying answer, ever). But minimizing or dismissing the sore points isn't constructive.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by Twisol on Wednesday August 28, 2019 @01:53PM (#20822230)
> It's a limitation because there are some extremely important and common relationships (e.g. putting an object in multiple containers simultaneously) that cannot be expressed in Rust's type system without using unsafe, RC boxes, etc, which create tremendous friction.

That is not a difficult relationship to capture, IMHO. The problem is that it's too easy to conflate "address" and "name" in most languages. An address is the physical location of a thing. A name is a location-independent way to look up a thing. Only one thing can live at an address, but you can use a name to look up multiple addresses depending on what question you want answered. (There is a parallel here with domain names and IP addresses.)

Rust associates ownership with every address. This causes serious problems when you've been using addresses as names. The solution is to use different names.

For instance, let's represent a graph as a vector of nodes. Every node has an index within that vector. Given this index, we can look up any node -- but critically, having the index does not confer ownership of the node. It's a completely orthogonal concern. So a node can quite happily possess a vector of the indices of its neighbors.

You might complain that this is a poor naming scheme for a graph structure, since it complicates deletion of nodes from the vector. Great! We have these exact same problems when using pointers (observe that the index plus the vector base address is the node address), but now that naming is a first-class concept, we can consider it on its own terms. There are alternative index spaces we can apply.

I've used this kind of design in a compiler hobby project. The intermediate representation is a set of tables representing different information about the program. With each pass, I can easily add new tables or transform existing ones, so long as I keep the names consistent. I can easily represent relationships between entities by using their indices. And I can easily attach more information to certain entities in compiler passes by adding a new table keyed on the same index.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by wahern on Wednesday August 28, 2019 @02:22PM (#20822506)
I've always found this a very odd rationalization. These games are unnecessary to get the same sort of type safety and double-free protection.[1] They're sufficient to subvert the Rust compiler, but that's it. The "benefit" only exists in the context of Rust because it's the only way to accomplish this.

And of course the extra indirection is costly, especially for a systems language. And you're also doing much more memory management. Hacks like these are why people say handling OOM is too difficult, because such hacks turn a simple logical operation into a series of multiple memory allocations, each of which could individually fail, which explodes the number of possible failure points, creating significantly more tension than necessary between memory management and functional clarity.

[1] The analog in the land of pointers is simply to NULL node member pointers and fail if a member is non-NULL at destruction or insertion, which is effectively the same as with the index tables hack. And for type safety you can of course use container-specific member types, which is exactly how the canonical linked-list and tree implementations work in BSD. (See https://man.openbsd.org/queue#LIST_EXAMPLE and https://man.openbsd.org/tree#EXAMPLES.) And like with many things in Rust, but-for the single ownership limitation Rust would otherwise make doing all of this much easier because it makes it easier to maintain the NULL invariants.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by Twisol on Wednesday August 28, 2019 @03:30PM (#20823180)
I think that we are subtly talking past each other, because what you're saying doesn't line up against what I thought I was saying. I'm not looking for additional type safety or security protections from this model. See my sibling comment to your earlier comment for a little more context [1].

As someone who works with Rust often, your assertions don't match my experience. I want to engage honestly and understand where Rust itself falters, versus where we need to invest in educating newcomers about what kinds of models are better represented in Rust than others. I have found the model I'm describing to be valuable in many ways -- more than a "hack" to get around Rust.

> And of course the extra indirection is costly, especially for a systems language.

To my knowledge, base-offset addressing is natively present on at least x86. With a vector-based index space like I was describing, a[b] == * (a + b) is no more costly than * b. If you would need a more complex index space, you were probably doing more than bare pointers already.

> And you're also doing much more memory management.

> because such hacks turn a simple logical operation into a series of multiple memory allocations

No, no more than you would have needed with pointer-based naming. The top-level arena (a vector in the graph example) owns everything and can be deallocated all at once when you're done with it. Arenas were already a well-known pattern before Rust, and they can reduce the amount of manual memory management over tracking a bunch of small, separate heap allocations.

Relatedly, Catherine West gave an excellent talk at RustConf 2018 on how the Entity-Component-System architecture maps nicely into Rust [2]. The address/name distinction comes from ECS, but it becomes even more useful in Rust.

> The analog in the land of pointers is simply to NULL node member pointers and fail if a member is non-NULL at destruction or insertion

I don't see how this is hindered by Rust. We use an Option type -- with Some(T) and None variants -- to model something that might not be present, so the logic can be exactly the same. Rust can even optimize away the "extra" space you would need for the Some/None variant flag in common situations [3].

[1] https://news.ycombinator.com/item?id=20822003

[2] https://kyren.github.io/2018/09/14/rustconf-talk.html

[3] https://github.com/rust-lang/rust/pull/45225

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by wahern on Wednesday August 28, 2019 @05:36PM (#20824239)
My original point is that real-world programs are complex data structures where any particular object may need to be referenced from multiple containers. At which point you're right back to square one--which container is the true owner responsible for maintaining all invariants and releasing the object when it's dead? Rust memory management does not and cannot automatically "free" resources with such references; you're still doing this manually. Indeed, this is irreducible complexity; it's why programming is hard, even in GC'd languages.

The indexing scheme is a hack to subvert Rust's typing system. It's even admitted in the talk you mentioned,

> One more criticism of this style is that you might say that using indexes instead of pointers is “safe” in the strict sense, but possibly only technically, you’re trading UB and potential crashes with pointers for “random but unspecified” behavior if you access the wrong or outdated index, and potentially panics. You’d be right, btw!

The generational indexing discussed therein is just a runtime invariant assertion, just as-if you asserted on internal pointer members being NULL at destruction (which implies but doesn't guarantee that the node has been properly unlinked).

This is all just more memory games.[1] In some respects they may be marginally safer, but they're also marginally more complicated. And that's friction. And even if on the whole it makes programs safer, the cost exists. ATS is an even safer language than Rust. If we're gonna pretend that the friction doesn't matter why wouldn't we just pretend we should all use ATS?

I'm not arguing that C or C++ is better than Rust. It just bugs me the way people rationalize some of the difficulty with arguments about why it makes things easier. Yeah, single-ownership does make things easier. Good C programs and good programmers generally learn to strive for single ownership semantics. Without an argument, Rust's enforcement of this invariant makes programs safer. But arguing that the enforcement of the invariant makes it easier to program? Come on! I know single-ownership is safer and simpler, which means when I choose to have a more complex ownership pattern it's for a darned good reason. Having to jump through hoops to accomplish it, even if it's "good" for me, is still jumping through hoops.

[1] And game makers love memory hacks; it's why their apps crash all the time, and why the thought of handling allocation failure would never cross their minds. To understand why people stopped using arenas and ad hoc invariant assertions for systems programming, see Heartbleed.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by wahern on Wednesday August 28, 2019 @05:56PM (#20824438)
> With a vector-based index space like I was describing, a[b] == * (a + b) is no more costly than * b. If you would need a more complex index space, you were probably doing more than bare pointers already.

Of course you're doing more than bare pointers. Which is why your example isn't applicable. (a + b) is fast when you're accessing record fields, because you already have a in a register and b is usually a constant. In the vector scheme you're doing (v + a + b), where v needs to be fetched (often through another level of indirection), and unless you're doing a long series of operation to the same structure, you're constantly reloading v and recalculating a.[1]

It doesn't follow that technique A is equivalent to B simply because when you reduce B to a microbenchmark the difference to A is de minimis. Microbenchmarks obscure and often elide the very reasons B would be slower than A. That said, I'm usually dubious of such performance arguments. This level of performance rarely matters, though when you do it ubiquitously it's more likely to matter, which is why it crossed my mind.

[1] It's often pointed out that loops like `for (size_t i = 0; i < n; i++) {}` are as fast as `for (; p < pe; p++) { ... }`, and that the former is preferable for various dubious reasons related to the ease of implementing boundary checks. But the performance is often the same because (and to the extent that) the compiler can reduce the former to the latter. Even today I regularly encounter situations where using pointers is cleaner and faster than indexing. Though that fact that it's faster is more a novelty; what matters most to me is clarity and correctness, which is highly context dependent.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by Twisol on Wednesday August 28, 2019 @06:06PM (#20824525)
> Though that fact that it's faster is more a novelty; what matters most to me is clarity and correctness, which is highly context dependent.

We agree.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by Twisol on Wednesday August 28, 2019 @01:31PM (#20822003)
This jibes very well with my own experience. When working in languages like Java, I feel myself overwhelmed with keeping a grip on the runtime topology of the object graph. Who can talk to whom? Who can _affect_ whom? My usual tools for covering my bases here -- immutability and/or value types -- are just not ergonomic in Java. If I have an "immutable" object with a getter that returns a reference to some mutable object, then it's not really immutable. Quoting Joe Armstrong:

> You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.

The clarity and simplicity of a single ownership model trumps just about everything else for me.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by jandrewrogers on Wednesday August 28, 2019 @01:49PM (#20822185)
There are very important and valid use cases for logical ownership both where N > 1 and N == 0. Ironically, the latter case doesn't have a use case outside of systems programming, so it is deficient for a systems programming language to not support it. (N == 0 does not imply literally "no owner", just that the owner is intrinsically non-visible to the compiler.)

Yes, it is possible to awkwardly and inefficiently shoehorn these cases into an N == 1 model but it comes at a substantial cost.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by pornel on Wednesday August 28, 2019 @06:45PM (#20824835)
The main point about Rust is that you're never stuck with a dead-end substantial cost. In the worst case you slap `unsafe` and you're no worse than in C (and better everywhere else where zero-cost abstractions worked and you didn't need to use `unsafe`).

There are awkward cases (self-referential structs, ugh), but let's not forget that in many many cases there are straightforward, zero-cost solutions. E.g. parent incorrectly stated you can't put one object in multiple containers. You can: one will own it, the rest will borrow it. And if you can't explain in your program which one is the owner, chances are you have the same problem in other languages:

• Many C programs with very complex and performance-sensitive ownership just use arenas. This pattern works in Rust, too (at zero cost).

• If you're certain you can get the custom ownership right with C pointers, Rust has C pointers, too (at zero runtime cost).

• When ownership is truly dynamic, C/C++ programs also have to use some kind of refcount or a flag to track it (e.g. setting freed pointer to null == Option.take() in Rust, bool free_me = borrow::Cow in Rust). Rust's refcounting is very efficient (intrusive, atomics not required), equal or better than `shared_ptr`.

With practice the frequency of "awkwardness" when writing code drops. You may still need to be more precise/restrained (especially around mutability and global state), but that pays off as soon as you want to make your code multi-threaded.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by jandrewrogers on Wednesday August 28, 2019 @09:41PM (#20825915)
For my use cases we can ignore the case of self-referential structs. Slapping "unsafe" on everything isn't equivalent to C++ because Rust is less expressive in some important ways.

I mostly work on high-end database engines, which appear to violate assumptions of Rust about what a sensible memory model can look like. In a modern database engine, almost all of your working memory is unavoidably paged, opaque, and directly DMA-ed. The concept of a lifetime cannot be attached to a memory address and virtually all of your data structures are "unsafe" in a Rust sense. You have to consider implications like destructors being effectively non-deterministic.

Fortunately, schedule-based safety models are transparent to developers, do not require "borrowing" so it works well with references not visible at compile-time, and all references can be mutable. To be clear, these models don't generalize to all software either, they are just well-suited to the requirements of high-throughput server engines. High-scale database engines are effectively single-threaded, so the only cost is a relatively trivial number of non-atomic ref counts. The underlying C++ to make this be handled automagically from a developer perspective is not trivial though.

Rust's memory safety model looks like it was designed for more traditional applications, like web browsers. Modern server software that is I/O intensive is going to look pretty similar to the above if performance matters, and that is a rough fit for Rust's memory model.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by steveklabnik on Wednesday August 28, 2019 @09:46PM (#20825939)
Have you looked at TiKV/TiDB? I’m curious what you think of them.
Re:Rust is the future, C is the new Assembly: Josh Triplett ... by jandrewrogers on Thursday August 29, 2019 @02:02PM (#20832024)
TiDB is built as a set of abstractions on top of RocksDB, inheriting weaknesses intrinsic to that style of architecture. Two things I would call out about the design:

First, loose coupling through the layers of the stack probably gives up an order of magnitude in throughput on the same hardware versus a state-of-the-art tightly coupled architecture for many workloads. Many optimizations are enabled when high level orchestration and operation semantics (like a SQL join) are understood at every level of the stack down to DMA scheduling. Most open source databases use loose coupling because (1) it is much simpler to design and (2) developers can reuse third-party code for parts they lack the expertise or resources to implement themselves.

Second, RocksDB is a modern implementation of an obsolete design. As a practical matter, this has a high cost in terms of storage scalability and throughput. Many open source projects use it because they don't have a clear idea of how to design something better. Applications where storage engine performance and scalability are critical, e.g. real-time sensor data models, is an area where open source is not remotely competitive at the moment.

Unfortunately, these criticisms could be leveled at most open source database engine designs, which tend to optimize for expediency and ease of implementation rather than performance, and with little awareness of how much performance is being sacrificed. The divergence between academic literature and state-of-the-art in databases is now almost unrecognizable, and the handful of people with expertise in high-end implementation are not working on open source systems.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by infinite8s on Friday August 30, 2019 @07:26AM (#20837552)
How are schedule-based safety models different than arena-based models?
Re:Rust is the future, C is the new Assembly: Josh Triplett ... by CJefferson on Wednesday August 28, 2019 @02:09PM (#20822382)
The borrow checker is only the first problem. Some objects gave an implicit member called await, except unlike every other member accessed with . this one does some crazy thing where the function returns early. Why does this look like member access? Any other weird members I need to know about?
Re:Rust is the future, C is the new Assembly: Josh Triplett ... by bbatha on Wednesday August 28, 2019 @02:24PM (#20822531)
`await` isn't a member its a post-fix operator. Its also a keyword so it will be highlighted in most environments and you won't be able to write actual members with the name unless you use and access it with the raw syntax `r#await`. Its also only available on types that impl the future trait (either manually or async functions).

The only other post-fix operator in rust is `?` which will return early and do error conversion like the old `try!` macro.

Both of these operators are post-fix so that you can easily intermix them with method calls and member accesses without temporaries. Going from a sync function to an async function is often little more than putting `async` in the function prototype and sprinkling some `.await`s in. If `.await` wasn't post-fix you would need to rewrite a large number of lines to introduce new temporaries.

Ya it looks a little weird at first but this is a pretty lazy criticism that doesn't hold water for anyone who's spent more than 5 minutes looking at async Rust code.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by steveklabnik on Wednesday August 28, 2019 @02:28PM (#20822565)
> Ya it looks a little weird at first but this is a pretty lazy criticism that doesn't hold water for anyone who's spent more than 5 minutes looking at async Rust code.

To be fair to your parent, this exact critique was brought up by many experienced Rustaceans during the development of async/await. In the end, it was decided that this drawback is worth it, but this is a drawback.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by bbatha on Wednesday August 28, 2019 @04:24PM (#20823645)
> this exact critique was brought up by many experienced Rustaceans

Just because they're experienced doesn't make it a great argument. Additionally they generally articulated more substantial concerns than "it looks weird."

To be exact my argument is that strange syntax does not increase the complexity of the feature. The complexity of async/await, especially when compared to features like the borrow checker as CJefferson complained, is in its semantics. You need to learn general async/await semantics like the fact that awaits always have yields regardless of which side of the expression you put it on among other things. And a number of rust-specific complexities that are very different from other languages (cold by default futures, executors vs reactors, pinning, etc.)

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by steveklabnik on Wednesday August 28, 2019 @04:30PM (#20823699)
> Just because they're experienced doesn't make it a great argument.

Neither does saying "this is a pretty lazy criticism that doesn't hold water for anyone who's spent more than 5 minutes looking at async Rust code." It clearly does and did.

Re:Rust is the future, C is the new Assembly: Josh Triplett ... by CJefferson on Wednesday August 28, 2019 @06:42PM (#20824814)
The problem is, someone has to tell you it's a keyword. I have written a bunch of rust. I started seeing await looking like a member, I assumed it was a member.

Now I wonder what other bits of rust examples I've been misreading. What other postfix operators are there where I might assume it's a member?