Hacker News new | ask | show | jobs
by pron 12 hours ago
> I'd argue that the value of Rust is that it makes low-level viable for a lot of stuff that would otherwise require a lack of memory safety; a lot of it is stuff that might be written in a higher level language, but that's just because relatively few programs are impossible to write in higher level languages.

Maybe, but I don't see making a low language viable for something it's not needed as offering much value. Low-level languages are primarily designed to give you direct, low-level control over interaction with the hardware, they sacrifice other things for that goal (including performance [1]), and so if I don't need that control I don't use a low-level language. When I do need that control, I find that Rust requires reaching for unsafe too frequently while still paying the full price for the safety of things I don't use (even Rust's memory management of strings doesn't give me the control I want; I have to work pretty hard for it).

> The cost for memory safety in Java is performance overhead though,

It's not performance (you often gain performance, especially in large programs). It's warmup and footprint.

> But that doesn't change the fact that some languages objectively require you to opt into which parts are memory unsafe, and others don't.

Like I said, C also fits in the category, so it's not a meaningful distinction. The difference is in what you can do in the safe subset. Zig lets you do more things in a safe way than C (where the safe subset is effectively empty), Rust lets you do more safe things than Zig, and Java lets you do more safe things than Rust.

> You mentioned finding the fact that they actually produce real world software that in practice do not suffer from the class of bugs that C/C++ suffers from uninteresting

I didn't say that that's uninteresting; in fact Zig also eliminates spatial unsafety as well as Rust, and I think that's good. I said that merely looking at broad statistics is uninteresting if you don't consider the kinds of programs being written. I.e. Rust gives me safety mostly when I write code with the same level of low-level control as I have in Java, then that's the part I find interesting.

> You seem to be arguing that unless you can eliminate literally the most bugs of any language in existence, then eliminating any bugs by picking a language that eliminates some of them is a useless endeavor.

That's the very thing I'm arguing against. I'm saying that different languages eliminate different bugs at a cost (again, Zig eliminates many memory safety bugs you'd find in C or even C++, arguably the most dangerous ones). What I'm saying is that what you get and whether the price is worth it depends both on the program you're writing and on your personal preferences. Just to be clear, "preferences" doesn't mean I care more or less about correctness, but which approaches to correctness I find more or less effective, something on which there is no consensus.

> To me, the reasonable thing would be to choose a place to draw the line and say "anything beyond this is too risky, but I'll tolerate anything that's at least this safe", and memory safety is in practice the place I think it makes sense to do.

I think it also depends on the kinds of programs you write, because for many programs I write (and for which I pick Java) Rust's level of memory safety is too low, and for the programs I pick a low-level language I wish I could have some cheap memory safety, but it's not offered to me. So in those cases I would prefer Zig's spatial memory safety, as it's no worse than Rust, and not pay the high price for Rust's while getting little in return. Anyway, I'm saying that it's both a matter of which approach you believe leads to better correctness and the kinds of programs you write in the language.

[1]: For example, the fact that in Java, references are not required to be stable machine pointers opens the door to some powerful optimisations that are not available to languages where pointers are required to be machine pointers (or something close enough to them). Or the fact that low-level languages require that the machine instructions executed are those present in the compiled image (or close enough), or, if you want, caring about worst-case performance at the expense of average case performance (although both C++ and Rust specifically don't always make that easy) precludes some other very powerful optimisations. People like me who've spent years on huge C++ programs know that the low-level control offered by low-level languages (regardless of the question of safety) sometimes helps performance and sometimes harms it.

3 comments

> C (where the safe subset is effectively empty),

Well, Fil-C and also Cheri show that C is a language that can be implemented with perfect memory safety for 99.9% of the language. This is not true for every language but is also not an accident in C. But also with the typical implementations of C such as clang and gcc you can essentially get spatial memory easily by using safe abstractions.

Very explicitly making a silly point; a rock is 100% memory safe.

The serious point: I care about whether the program I write aborts regularly, whether due to a Rust panic or a capability violation.

Regarding the silly point: Fil-C is less of a rock than Safe Rust as everything in C just works.

I am not sure about what you mean by "aborts regularly". After a memory safety issue, I think one usually wants to abort quickly and not do anything else in the program as the program state is confused, so running specific sanitizers in trapping mode together with coding abstractions that avoid unchecked raw pointer access you can write spatially memory safe code in C without a problem. But yes, sometimes you may want to continue running, but this is much harder and needs a careful design of the system anyway, also in other languages.

> It's not performance (you often gain performance, especially in large programs). It's warmup and footprint.

To me, those are also performance characteristics. Maybe my view on what constitutes "performance" is broader than average here.

> When I do need that control, I find that Rust requires reaching for unsafe too frequently while still paying the full price for the safety of things I don't use (even Rust's memory management of strings doesn't give me the control I want; I have to work pretty hard for it).

Fair enough, I can't tell you that you don't have that experience when writing Rust. It's pretty different from mine though, and the experience of the large number of former C/C++ devs I've worked with after they learned Rust; the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them, which informs my perception here, but I recognize that individual experiences won't always fit into larger trends.

> Like I said, C also fits in the category, so it's not a meaningful distinction. The difference is in what you can do in the safe subset. Zig lets you do more things in a safe way than C (where the safe subset is effectively empty), Rust lets you do more safe things than Zig, and Java lets you do more safe things than Rust.

I don't think I understand what you're saying here. I don't know of a way to turn off undefined behavior by default in C and only opt into it in discrete segements of the code, but maybe I'm missing something.

> That's the very thing I'm arguing against. I'm saying that different languages eliminate different bugs at a cost (again, Zig eliminates many memory safety bugs you'd find in C or even C++, arguably the most dangerous ones). What I'm saying is that what you get and whether the price is worth it depends both on the program you're writing and on your personal preferences. Just to be clear, "preferences" doesn't mean I care more or less about correctness, but which approaches to correctness I find more or less effective, something on which there is no consensus.

It seems like you're arguing against the idea of memory safety as a category at all then. To me, "I can't write code that's memory unsafe without explicitly opting into it" seems like an objective statement, and it's objectively different than "I can't write certain types of memory safety bugs in a given language". I don't really understand what's useful about being able to write memory unsafe code without having to opt in when in practice the number of bugs from mistaken memory safety are overwhelmingly more common than the cases when you're forced to opt into unsafe because Rust forced you to work around the constraints, and even in low-level programs, the actual number of truly unsafe operations you need to do tend to be fairly low in my experience. I guess I can't say for certain that you don't truly need to do things that you're forced to write unsafe for too often, but to me, it seems like you're refusing to pay a pretty small price for mostly ideological purity rather than pragmatism.

> I think it also depends on the kinds of programs you write, because for many programs I write (and for which I pick Java) Rust's level of memory safety is too low

> [1]: For example, the fact that in Java, references are not required to be stable machine pointers opens the door to some powerful optimisations that are not available to languages where pointers are required to be machine pointers (or something close enough to them). Or the fact that low-level languages require that the machine instructions executed are those present in the compiled image (or close enough), or, if you want, caring about worst-case performance at the expense of average case performance (although both C++ and Rust specifically don't always make that easy) precludes some other very powerful optimisations.

I'm struggling to imagine what the circumstances are where these are genuine concerns rather than theoretical or premature optimizations. What are some examples of programs where you'd get better characteristics running them if they were written in Java rather than Rust due to the lack of enough "memory safety" in Rust?

> To me, those are also performance characteristics. Maybe my view on what constitutes "performance" is broader than average here.

Yes, but they come with speed gains, so you can't say that you pay "performance overheads" when Java removes some of the performance overheads that programs in low-level languages and replaces them with others. You could similarly say that you pay performance overheads when going in the other direction.

> and the experience of the large number of former C/C++ devs I've worked with after they learned Rust

And it's not my experience or a large number of C/C++ devs I work with.

> the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them

Then your exposure isn't wide enough.

> I don't think I understand what you're saying here.

What I'm saying is that we can't say that the value is merely in the existence of a clear syntactic distinction between safe and unsafe code, because that distinction exists in C, only in C, the clearly delineated line between safe and unsafe code is that between `int main(void) {}` and anything that isn't that; i.e. any program other than that explicitly opts into unsafety. So any meaningful discussion about memory safe languages must include what you can do in the safe subset. In C's "safe subset" (the empty program), you can do nothing, and that's what makes it not valuable. But for my needs, what you can do in Rust's safe subset (compared to both Java and Zig) is also far too little (to justify the cost).

> To me, "I can't write code that's memory unsafe without explicitly opting into it" seems like an objective statement

It is, but what I'm trying to say is that it alone doesn't have much value. In C you also "can't write code that's memory unsafe without explicitly opting into it" by writing anything other than the empty program, but obviously you wouldn't consider C's memory-safe subset suitable because you can't use it to do what you want to do in C. Rust's value is not, therefore, in that it has a memory-safe subset, but that it has a useful memory-safe subset. It's just that the utility of that subset depends on the kinds of programs you'd want to use a low-level language in the first place.

> it seems like you're refusing to pay a pretty small price for mostly ideological purity rather than pragmatism.

Quite the opposite. The price of Rust's complexity, implicitness, and compilation time is too high for what little safety I get in return, that I don't want to pay it for pragmatic reasons.

> I'm struggling to imagine what the circumstances are where these are genuine concerns rather than theoretical or premature optimizations. What are some examples of programs where you'd get better characteristics running them if they were written in Java rather than Rust due to the lack of enough "memory safety" in Rust?

It's nothing to do with memory safety. Low-level programs sacrifice optimisation opportunities available to Java because above all else they need to offer low-level control. That low-level control can translate to good performance sometimes (especially in smaller programs), and sometimes it translates to worse performance (especially in large programs). The huge C++ programs I worked on migrated to Java not (just) for safety but also for better performance than C++ (again, it's easy to get excellent performance in low-level languages when the programs are small or specialised; it gets harder and harder as they grow). So we got better performance than C++ while also getting better safety than Rust, a much simpler language than Rust (or C++), and faster build cycles than Rust (or C++). But the topic of how Java reduces the overheads that C/C++/Rust/Zig programs often have when they grow large (although Zig makes it easier than the other them to reduce them) is a whole complicated topic. I might give a talk about it at the upcoming Devoxx.

> > and the experience of the large number of former C/C++ devs I've worked with after they learned Rust

> And it's not my experience or a large number of C/C++ devs I work with.

>> the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them

> Then your exposure isn't wide enough.

Or maybe your exposure is only to people who didn't give it a fair chance? I don't know how either of us can be confident that we know 100% for sure that our sample is more definitive.

> What I'm saying is that we can't say that the value is merely in the existence of a clear syntactic distinction between safe and unsafe code, because that distinction exists in C, only in C, the clearly delineated line between safe and unsafe code is that between `int main(void) {}` and anything that isn't that; i.e. any program other than that explicitly opts into unsafety. So any meaningful discussion about memory safe languages must include what you can do in the safe subset. In C's "safe subset" (the empty program), you can do nothing, and that's what makes it not valuable. But for my needs, what you can do in Rust's safe subset (compared to both Java and Zig) is also far too little (to justify the cost).

> It is, but what I'm trying to say is that it alone doesn't have much value. In C you also "can't write code that's memory unsafe without explicitly opting into it" by writing anything other than the empty program, but obviously you wouldn't consider C's memory-safe subset suitable because you can't use it to do what you want to do in C. Rust's value is not, therefore, in that it has a memory-safe subset, but that it has a useful memory-safe subset. It's just that the utility of that subset depends on the kinds of programs you'd want to use a low-level language in the first place.

That seems like an absurd false dichotomy in the form I was talking about before. I don't seriously believe that you can't easily identify when looking at Rust code whether unsafe is explicitly being allowed in it or not, or that you are writing programs that are doing things that would require unsafe literally everywhere.

I've genuinely been trying to understand where you're coming from, but the more I try, the more it seems like you just genuinely seem to think that you're too smart to accidentally write memory safety bugs, or that the memory safety bugs don't matter much. Maybe you're right, but I don't think there's anything left for me to learn from your point of view.

> or that you are writing programs that are doing things that would require unsafe literally everywhere

You don't need unsafe "literally everywhere" to run into issues. First, what matters most are the areas that are most subtle/tricky in your program. If in those areas Rust doesn't add much safety and makes things worse due to language complexity, that's a problem. Second, when you want low-level control, you might well want it in quite large swaths of the code. For example, one thing that low-level languages currently, in principle, do better than Java is arenas. But the whole point of arenas is that you want _all_ allocations in some large and elaborate call chain to go in the arena (and you'd like to enjoy both the standard library and 3rd party libraries). Rust doesn't make that easy (and neither does C++, for that matter).

> the more it seems like you just genuinely seem to think that you're too smart to accidentally write memory safety bugs, or that the memory safety bugs don't matter much

I don't see how you've reached that conclusion. I told you that for most programs I choose a language that is more memory-safe than Rust, and when I choose a language that's less memory-safe than Rust it's when Rust doesn't offer much safety, either.

See, this is exactly the thing I find so annoying in the Rust discourse. There's no doubt Rust significantly helps avoid memory safety issues (i.e. Rust => more men-safety) but that doesn't mean that caring about memory safety issues means preferring Rust (more mem-safety => Rust). One simply doesn't follow from the other because the logical implication is reversed.

I just wanna give my perspective since I came from high level languages and pretty much exclusively use Rust now, so perhaps I can articulate why I find value in the language. And my apologies if my input is not wanted, no need to respond if so.

First of all I respect your point of view - I'm not a Rust absolutist, I think that garbage collected languages are a massive advantage for a lot of things and would never criticise someone choosing a higher level language. Likewise I wouldn't criticise someone choosing Zig or Oden or Jai or even C for tasks where you really need that low level control.

For me, I like to have a single language that I can use for pretty much everything. Afaik there is no other language that is a) popular b) has a modern toolchain with integrated build, formatting & linting etc, and c) can be used both in the kernel and for developing websites. Rust might not be the best choice for most of the spectrum of software, but it's good enough for everything. I can write a low level service + a web server and UI in the same language, where with other choices I would need to use two separate languages. This matters to me because I don't have the time to maintain mastery of multiple languages, I find a lot of value in focusing deeply on one language and learning it completely.

Now I also don't write a lot of low level rust, I've never written a block of unsafe before and I probably write "unidiomatic" rust with too much copying, too many Arc<Mutex>>'s etc. But I like knowing that I can if I need to.

Rust has a lot of other things going for it. A good type system with plenty of nice language constructs that are missing in a lot of higher level languages. It has Cargo and a healthy ecosystem (although I do worry about the number of dependencies used sometimes). And a large community of very smart people. I'm not saying this is exclusive to Rust, but as a whole Rust is a unique language with no alternatives if you value the things I do.

So I would say that it's approach to memory safety threads the needle where it can be used (although not the very best choice) for when you'd use a higher level language, but also gives enough control that you can do plenty of low level stuff in it safely, and with clearly delineated unsafe sections where you really can do anything.

I get that perspective and I agree it has value, but for me, Rust is a jack of all trades but master of none, all while being one of the most complicated languages ever made and requires very long build times. So I agree it continues C++'s dream of being "one language for everything", but I think that dream is misguided, and that Rust suffers from most of the same problems as C++.

For low-level programs, I already said that Rust doesn't offer much safety for the things I reach low-level languages for (or, conversely, its safe subset doesn't offer the very control I'm after in such a language). Furthermore, the complexity and implicitness of the language make it harder for me to carefully understand the kind of subtle code I write in such programs. The long build times could mean I write fewer tests.

For high-level programs, Rust's safe subset is technically sufficient, but the problems are even worse (and exactly match C++'s): High level Rust code looks quite good and is easy to write, same as in C++, but the problems start with the maintenance and evolution. Small local changes - to a returned object's lifetime or thread-share ability, or between static and dynamic dispatch - require non-local changes. That's because low-level languages have low abstraction, i.e. the same contract covers fewer possible implementations. True, unlike C++, Rust tells you what things you need to change, but you still need to change them. That was the main problem we had with C++: the code looks great and it's very easy to write at first, but the maintenance and evolution costs - especially when the program is large and long-lived - get high and remain high forever. Furthermore, once a program grows large, it starts suffering from similar performance ovhearheads large C++ programs suffer from: you find yourself needing more dynamic dispatch, which is slow in Rust and C++; you find yourself needing more shared objects with different lifetimes, which are also slow in those languages, so the program isn't even particularly fast or scalable (sure it's faster than a JS or a Go program, but that doesn't say much). Java (or C#) which is aimed at optimising the performance of large programs, removes many of these overheads. Lastly, deep always-on observability/profiling isn't quite poor (it's better now with eBPF, but still a long ways away from what you get with Java or C#).

So yes, Rust and C++ are intended as "one language for everything", and Rust is probably somewhat better than C++, but your high level programs pay for the low level feature (i.e. suffer from the maintenance and performance costs of low-level languages), while your low-level programs pay for the high-level features (the complexity needed for implicitness and safety). So yes, you can do everything, but rarely as well as could be done, and while I see the value in getting expertise only in one language, 1. it's a language that requires a lot of expertise as its "multi-functionality" makes it very complicated, so much so that you could probably become an expert at two more specialised languages for not much more effort, and 2. I think that if you really need to write low-level code, e.g. you're writing a kernel or a hardware driver or a controller or a GC, then expertise in the domain dwarfs expertise in the language anyway (i.e. we're talking years of required experience until you're really good at it).

BUT I acknowledge that the weight I assign to these things is subjective, and I'm certain others reach the opposite conclusion through arguments that are no less reasonable than mine.

I think it's also a matter of experience - someone used to writing code in unsafe low level languages has a different approach to solving problems and may find Rust gets in the way. I actually started with C++ and after writing a reasonable amount of it I found myself wondering why I had to keep track of lifetimes, nullability etc in my head when it was so easy to mess those up. I kind of discovered "why Rust" from first principles and from then on I was hooked.

I'm not sure I understand your point about dynamic dispatch being slow in Rust/C++ or shared objects? If you're targeting native (which I find important) neither Java or C# are going to be faster surely. Maybe if you're willing to run Java/C# JIT you might find some wins (skeptical it's faster across the board) but you also don't need dynamic dispatch in performance-critical areas. I rarely reach for a Box<dyn Something> even in my high-level code.

I haven't worked on large Rust projects (> 500k loc) so I can't speak to the maintenance costs of that, but for me it doesn't matter (at least yet).

But we see even experienced professionals making mistakes with low level languages and I think it's worth considering if it's worth some of the cons you bring up to avoid those. Kind of reminds me of Carmack talking about static code analysis years ago:

https://archive.is/qC9a

> The more I push code through static analysis, the more I’m amazed that computers boot at all.

> I kind of discovered "why Rust" from first principles and from then on I was hooked.

I get it. The language certainly does appeal to some people, and I can understand why, just as I understand why it does not appeal to others.

> I found myself wondering why I had to keep track of lifetimes, nullability etc in my head when it was so easy to mess those up.

And I agree with that, but my conclusion (after decades of experience with low-level programming) is somewhat different: Don't reach for a low-level language unless precise low-level control over the hardware is the exact thing you're after. And when that is the case, I find that safe Rust doesn't offer the control I need, and unsafe Rust (and/or a lot of custom code) is not what I want to use.

> Maybe if you're willing to run Java/C# JIT you might find some wins

Of course I use the JIT. That's exactly what it's for. Now, I don't care if the buffer from which the CPU reads instructions is memmapped from a file or generated by a JIT, but I do know that some people like the "single native file experience". To that end, we're working with Google to add a small feature to the JDK that would allow it to link the JVM, other native libraries, and Java classes into a single native executable (it's still going to JIT the Java code, but you'd be launching a "native binary").

> (skeptical it's faster across the board)

I wouldn't say it's faster across the board. You sometimes can write large programs in C++ (or Rust) that match and even exceed Java's performance, but it gets harder and harder the larger the program is. On average, I find that the "effort per performance" is, on average, significantly lower in Java in large programs. And it's not just the JIT. Another weak point of low level languages is that their pointers can't move, which means they can't use moving collectors, which also offer superb efficiency, again, mostly in large programs when you have lots of objects of varying sizes and lifetimes (especially now when we no longer have GC pauses).

> but you also don't need dynamic dispatch in performance-critical areas

You certainly don't start out needing it. Over time, however (and important codebases last at least 15-25 years), it either creeps in or it affects sufficiently many less critical paths to make an impact. You can try and re-architect things, but it takes a lot of effort (and it's this evolution effort that was a major reason for C++'s decline).

> But we see even experienced professionals making mistakes with low level languages

Absolutely, but my prescription would be to avoid low level languages altogether, and that has indeed been the industry's trajectory, and it's continuing. And when you absolutely do need to kind of control that low level languages offer, language complexity can also cause (or help hide) mistakes in code that is often very subtle, and the added safety, which is partial at best in those situations, isn't enough to offset that. Again, this isn't universal, but there are reasons to avoid Rust in low level code that are just as good as the reasons to pick it, and so different people will choose differently.

BTW, I've never worked on a browser, and it may well be the Rust is the best language for that, but I would be very curious to try Java. First, modern browsers run a lot of JS so you have a JIT and a GC, anyway, and so it might be both easier and more efficient to have everything use the same GC, and while process isolation would have required Java to re-JIT the rendering pipeline, Java is about to allow sharing JITted code (and even caching it from one run to the next) so that there would be no need to warm up the same code over and over.

I think the only argument I’d make about high level programming languages is that software continues to outpace hardware development in sucking up as much performance gains as possible. One program written in a slower, garbage collected, high level language is ok, when they are all it’s bad. I think eventually we’ll get to a point where we won’t have to think about memory management anymore but we aren't really there yet. Heck, software written in C++ like browsers are dog slow, imagine if they were written in Java…
I originally came to Java because of the better performance it offered compared to C++ in large programs. The JVM is specifically designed to remove some of the fundamental performance overheads that low-level languages suffer from, and manifest especially when programs grow large (and a browser is quite large). So when someone talks to me about "GC languages" being slow and low-level languages being fast, I know they've not had much experience with either Java or low level languages, nor do they understand modern compilers and memory management. Java offers strictly more optimisation opportunities than low-level languages, in compilation as well as in memory management. What it gives up in exchange is low-level control (including worst-case performance), but it is low-level languages that sacrifice performance (especially average-case performance) in exchange for the control they need. Not being able to move pointers freely and not being able to deoptimise and recompile at runtime are serious impediments to modern optimisation, but low-level languages gladly give that up because they're not optimised for performance but for precise control.

In particular, modern moving collectors were designed for the purpose of removing the high overheads of malloc/free allocators that make heap memory management so expensive in low-level languages (and in any language that uses non-moving memory management strategies). The reason code in low-level languages tries to avoid things like heap allocation and virtual dispatch on the hot path is not because these things can't be super-fast (most virtual calls in Java are faster than many static calls in C), but because they are slow in low level languages because of their constraints.