But I bet they would criticize programs for being written in assembly, if they didn't need to be.
If you could have a language with all the performance of C without the footguns, why wouldn't you want that?
But I bet they would criticize programs for being written in assembly, if they didn't need to be.
If you could have a language with all the performance of C without the footguns, why wouldn't you want that?
I've yet to see a language that actually delivered on this claim.
Rust delivers on all these claims. And there have been others before it. Rust hits all the sweet spots for me.
Example for the first case: Writing a garbage collector runtime in Rust has most of the same problems in Rust as in C, because you have to write most of it in unsafe code, where Rust inherits much of C's undefined behavior w.r.t. pointers via LLVM. In short, you have largely the same problems and have added a hard dependency on Rust.
For high-level work, almost all [1] of what Rust gives you is memory safety and that comes at the price of dealing with a LOT of extra language complexity. But aside from dynamic memory management, memory safety isn't hard (we did that back in the 1970s and 1980s), and for dynamic memory management, we can get memory safety with a garbage collector and much less complexity. So Rust is primarily of interest for those use cases where garbage collection is not an option.
While that still gives you plenty of interesting use cases for Rust, there are also plenty of programming niches that it serves poorly.
[1] People will also mention "fearless concurrency", but guaranteeing the absence of data races is not hard. That more languages don't do it is partly because they simply neglected that aspect [2], but also because any mechanism – including Rust's – for doing so inherently constrains your options w.r.t. concurrency [3]. Plus, avoiding data races is the easy part of getting concurrency right.
[3] Concurrent Pascal had guaranteed absence of data races in the absence of pointers in the 1970s, Eiffel had done it with pointers in the 1980s, and there was a plethora of research in the 1990s to do it in various other ways.
[3] For example, there are plenty of use cases, such as certain idempotent operations, where data races are not only perfectly safe, but also desired for performance. There are also use cases where you can prove that no data races occur, but a type system cannot easily capture that.
It's too high-level for a lot of low-level work, and too low-level for a lot of high-level work.
This is true in the very specific cases that you gave, but I believe that is the minority of use cases, not the majority.Even the example of writing a GC that requires tons of unsafe code, that is not a good argument for making all the code unsafe. All the unsafe GC code would be abstracted away into a module and would be more obvious to those looking at it that they will need to be watchful for undefined behavior. Now you can proceed writing the rest of the project in safe, simple Rust.
People will also mention "fearless concurrency", but guaranteeing the absence of data races is not hard
Maybe for developers that are very familiar with the race conditions of parallel code, but definitely not for most people. Even seasoned developers will make mistakes with simple multithreaded code.Also, the reasoning behind "x is easy so why do I need my language to check it for me" is questionable. The whole point is that you have a guarantee. Have you never had a compiler catch a stupid mistake before it happened and felt relieved? I doubt it. Now imagine if instead of debugging stupid data races in your parallel code you can spend that time optimizing and improving it. I fail to see how this can be viewed as negative.
Sure Rust doesn't cover 100% of use cases, but it definitely covers more than you're implying. It's low-level enough that Redox OS can be written in Rust, but high-level enough that Firefox is now outpacing other browsers and parallelizing everything with Rust.
That code that could be "abstracted away" would be "virtually all the code" in my example.
> Maybe for developers that are very familiar with the race conditions of parallel code, but definitely not for most people. Even seasoned developers will make mistakes with simple multithreaded code.
I'm not talking about manually guaranteeing absence of data races. I mean absence of data races as a language feature.
> Also, the reasoning behind "x is easy so why do I need my language to check it for me" is questionable.
This is not at all what I was talking about. You completely misunderstood me.
> A quick grep gives us some stats: the kernel has about 70 invocations of unsafe in about 4500 lines of code overall.
My example was a GC runtime, not an OS kernel. If I have only very little unsafe code, then I could just do that in C and the rest in whatever other high-level language suits my project and not see any difference.
The bigger problem – where Rust failed to pick some low-hanging fruit, IMO – is that "unsafe" is too much like the bad parts of C. There is no medium position between "everything is defined and memory-safe" and "everything may explode at a moment's notice".
My most practical need for a low-level language is a language that is in that in-between position: semantics that remain easy to comprehend and predictable even if there are no static guarantees, and where I have to use a different strategy for software assurance. The point here is that for such a language I can resort to alternate validation tools (think Ada and SPARK for an example). Rust's unsafe mode does not handle that situation well because (like C) it does not provide a foundation for alternate validation strategies.
It's perhaps also worth pointing out that I have a formal methods background. In short, I've done formal specifications/proofs for software before. In this context, safe Rust has a fairly high cost for only providing memory safety (and few other guarantees), and unsafe Rust is not a good foundation (or at least, not much better than C) for bringing advanced tools to bear.
We find that Rust’s safety features do not create significant barriers to implementing a high performance collector. Though memory managers are usually considered low-level, our high performance implementation relies on very little unsafe code, with the vast majority of the implementation benefiting from Rust’s safety. We see our experience as a compelling proof-of-concept of Rust as an implementation language for high performance garbage collection.
[0] http://users.cecs.anu.edu.au/%7Esteveb/downloads/pdf/rust-is...
> We found that the Rust programming model is quite restrictive, but not needlessly so. In practice we were able to use Rust to implement Immix. We found that the vast majority of the collector could be implemented naturally, without difficulty, and without violating Rust’s restrictive static safety guarantees. In this paper we have discussed each of the cases where we ran into difficulties and how we overcame those challenges. Our experience was very positive: we enjoyed programming in Rust, we found its restrictive programming model helpful in the context of a garbage collector implementation, we appreciated access to its standard libraries (something missing when using a restricted language such as restricted Java), and we found that it was not difficult to achieve excellent performance. Our experience leads us to the view that Rust is very well suited to garbage collection implementation.
The underlying problem is that Rust's borrow checker cannot possibly capture at compile time the arbitrary relations between objects managed by a garbage collector and arbitrary object layouts. Whatever guarantees it provides, memory safety isn't one of them (and cannot be), even if nominally only part of it is unsafe code. Pointer arithmetic and memory safety do not mix.
What you get out of that is basically an alternative approach to information hiding, not memory safety.
> Writing a garbage collector runtime in Rust has most of the same problems in Rust as in C, because you have to write most of it in unsafe code, where Rust inherits much of C's undefined behavior w.r.t. pointers via LLVM. In short, you have largely the same problems and have added a hard dependency on Rust.
to the conclusions drawn in the cited paper. They sit in pretty stark contrast from where I'm standing.
At a severe cost in performance. Static object lifetimes cover 99.9% of a garbage collector's use cases, without the performance cost of GC, nor the nondeterministic runtimes. We first saw static object lifetimes come into their own with C++'s value semantics; Rust refines and clarifies the idea and makes memory safety an inherent part of the language itself.
Static object lifetime is to GC what static types are to dynamic types. Lisp is 1960s tech. It has failed, and been replaced with something much better.
For starters, we have to assume that we don't deal with value types (which will end up on the stack, one way or the other), but with local variables that reference heap objects. Second, we have to distinguish between tracing and reference-counting GCs.
A modern tracing garbage collector will have cost for such temporary allocations comparable to `alloca()` and those allocations will typically be inlined. The cost of deallocating a short-lived stack object is zero (yes, zero). This is possible because GCs (unlike manual memory management schemes) are compacting. Whether one approach or the other comes out on top is very situational.
More importantly, I dispute the 99.9% as a vast exaggeration. There are plenty of important use cases (such as persistent data structures, shared caches, etc.) where unique ownership is insufficient; Rust requires you to use either copying or reference counting when you run into shared ownership scenarios, both of which are more expensive than tracing GC (naive reference counting is already one of the more expensive memory management methods known, and atomic reference counting is especially expensive).
If you use reference-counting GC, then for any program that satisfies Rust's borrow checker, the optimizer can eliminate reference counts that satisfy the same conditions (assuming that the optimizer knows about them because they're part of the language semantics). This is largely what Swift does, for example.
Finally, there is deferred reference counting, which incurs only trivial overhead for objects with automatic lifetime (on the order of a fraction of a percent). This is because this algorithm incurs real cost only when pointers are written to global or heap locations; this is also why it's seen limited use in practice: it's excellent for objects with automatic lifetimes and does not rely on the generational hypothesis, but those do not constitute 99.9% of all use cases. If they were, deferred reference counting would have a far more prominent role.
This does not even account for the fact that when there is overhead, that overhead is generally trivial in an imperative language with value types.
There are use cases, of course, where a tracing GC is an inappropriate choice, but that is not because of throughput. Tracing GCs make interoperability with other GCs different, for example, and have implicit memory overhead that may be prohibitive in large applications such as a web browser (that can easily consume gigabytes of memory on a laptop or desktop machine). That said, there are alternative approaches to garbage collection that do not have those problems.
Rust is not a replacement for C in the sense of portability. People love simplifying the world into Windows/macOS/Linux, but that is not all you may want to target.
Nowhere in this sentence do I see "for every platform C targets".
Given no other requirement other than C without the footguns (memory unsafe) is there a good reason not to use the safe version? I'd say there are some, but they aren't crazy compelling (ie: developers have to learn rust, maybe harder to hire for, etc).
Tests can't establish the absence of bugs the way Rust can. You only think your C code is reliable, you don't actually know that it's reliable. Rust only appears harder because of the latent bugs in your C program that you're not aware of.
For simple cases, this may be true, for complex cases it but shifts the cognitive load up front. Which may be more frustrating, but also may prevent a large class of hard to identify, intermittent in manifestation, bugs from getting into production. Which also saves programmer frustration.
That's like saying a SawStop (http://www.sawstop.com/) lacks a simplicity of a sawblade. I mean - Yeah, sure, but sawblade also won't stop you from turning yourself into amputee.
I understand Rust can be overly verbose, but main complexity comes from the borrow checker and the effect adding another kind of type, to track lifetimes. The lifetime system is the main selling point.
There are other sources of complexity in Rust, but I am glad to say both Rust/Scala seem to be looking for a way to simplify things.
No it doesn’t.
One reason is rust doesn’t have SSE/AVX/Neon intrinsics.
Without them, you can’t get anywhere near all the performance of C, nor anywhere near advertised performance of any modern CPU.
IMO, this isn't even the right goal. The problem is that in many cases using C is itself a premature optimization. Consider two languages:
* C, which gives you performance by default at the cost of an entire arsenal of footguns with esoteric nonlocal trigger conditions
* Hypothetical language X, which has all the same footguns but engineered for safer triggering and locked up in a safe that you have to choose to open when you want C-like performance
I'd rather have hypothetical language X (which is an accurate portrayal of many real existing languages), because it's got better failure modes. Performance issues are less impactful, in general, and more importantly they are more obvious. It is usually easy to tell when code is not fast enough. The endless parade of CVEs ultimately deriving from memory safety issues, often decades old, is living proof that misuse of the footguns is frequently far from obvious.
A language that avoided it would have to be very close to C while making it only mildly more difficult to foot shoot. Everyone who is trying is overshooting and therefore not really writing an adequate replacement low level systems language. Even if they did, it would be so close to C that adoption pickup would be low, awkward, and the language would fail outright. (Think Python 3 but worse)
>But I bet they would criticize programs for being written in assembly, if they didn't need to be.
Certainly. If you're writing your web app in assembly you are very likely a crazy person unless your goal is to do something ridiculous. If you're writing some assembly in a critical path in a web server to precisely control the network stack, you might not be a crazy person (see Netflix tech blogs).
Says who? A language where every single memory access did NOT have the potential to be a buffer overflow would be much more difficult to foot-shoot with, even if it allowed you to sometimes access memory with raw pointers. Just having the ability, as in Rust, to mark code as either safe or unsafe goes a long way towards preventing footguns.
>Everyone who is trying is overshooting and therefore not really writing an adequate replacement low level systems language.
Using Rust as an example again, I don't think there's anything about being able to say "this code right here cannot cause memory unsafety" that precludes "low-level systems" programming.