That framing may seem intellectually satisfying, but it's not useful in practice. Consider the extreme edge case of C: We can clearly mechanically delineate between the empty program and a non-empty one, we call the empty program safe and any program that isn't empty unsafe (i.e. C is memory-safe if you want to do nothing and not if you want to do anything). And so, we also have this property that in C you can't do anything unsafe without noticing.
Now, that's ridiculous, but something not too different happens to me with Rust. I reach for a low-level language when I want to do low-level things in a more convenient way than in Java, but the very things that would make me reach for a low-level language in the first place are unsafe in Rust. So in ~100% of the programs I want to write in a low-level language, Rust and Zig offer the same level of memory safety (but I need to pay a higher price for Rust). That Rust reminds me that what I want to do is unsafe doesn't help me.
Of course, other people may want to reach for a low-level language in other situations and their perspective could be different, but if I pay the price and get little in return I can't see how that would be "table stakes". Table stakes imply some universality that is obviously not here.
Do you find most of your rust code is unsafe? Because I also find I do some unsafe stuff, because of the algorithms I work on I often end up with some unchecked array accesses and a couple of raw pointers into those arrays I pass around. But 98% of the code is safe and I find this makes it easier to reason about.
Yes, it's pron. Pron works on a JVM so he looks at everything from a language runtime lens. To him, the entire code base might as well be unsafe.
The argument that you can contain unsafe in safe abstractions does not interest him, because of the nature of the projects he works on.
> To him, the entire code base might as well be unsafe.
Not at all. I would very much like the code to be safe, it's just that I reach for a low level language to get the thing that low-level languages are designed to offer me, which is control, and no language offers control and safety at the same time. So when the interesting parts of the code could be written in safe Rust, I have to give up control, and in that case I'd rather use a more convenient language that doesn't give me full control. And when the interesting parts of the code need full control, Rust doesn't give me safety anyway, and I still pay for its complexity. As to encapsulating unsafe, that doesn't help at all if the most tricky parts of the code are inside. What happens there is that Rust makes the hard parts harder and the easy parts easier, and for me that's a net negative.
P.S.
And that brings me back to the universality point, that you're actually repeating, so let me explain it again. All programming languages bring a certain aesthetic that appeals to some and repels others within their intended domain. For example, I prefer Java to C# and Kotlin because I place a value on language simplicity, but I'm well aware that some other people like a lot of convenience features and they have the opposite preferences. I don't expect everyone to like Java even among those who work in the domains in which it is intended. For the same reason, I prefer Haskell over Scala and Clojure to Common Lisp, yet I fully recognise there are those who have the opposite preference.
Yet when people say they don't like Rust, the reaction among those who do is often one of: you don't want it to work, you don't care about correctness, your situation is special, or you just don't get it. I.e. they expect Rust to be universally liked unless there's a some good exceptional reason not to. But this is not the case for any language, let alone a language as complicated and as rich as Rust, which is certain to evoke a strong aesthetic reaction either for or against. So I have a long, long record of working with low level languages, I have a long record of working with formal methods and studying software correctness in general, I've worked extensively on safety-critical and mission critical systems, and I find Rust unappealing. There is nothing that should be surprising about that, there is nothing special about Rust or any other language that should lead anyone to expect it to be universally liked among those in the domains for which it is intended, and indeed, it is not the case for Rust as it is not the case for any other language. This amazement that quite a few people, even those with the relevant expertise, don't like your favourite language is exactly this sense of universality that I find annoying.
That some people get it yet don't want to choose a language is the case for Java, it's the case for Haskell, it's the case for Zig, and it's the case for Rust. Some people who really care about writing correct low-level code will have good reasons to choose Rust over other languages, while others who really care about writing correct low-level code will have equally good reasons to choose other languages over Rust. It's not because our programs are special, but it's because we think we can do it better with other languages. That's just the reality of all programming languages ever made, and that's what I mean by no universality. There is no external, objective requirement for any program that makes any (general purpose) language objectively best for that program. Choices always involve some kind of personal aesthetic preference.
That your favourite language isn't "objectively best" and isn't universally preferred even among those who care about the things you care about isn't something that should bother you, and pointing out this simple fact isn't trolling. It's something you should expect, because it's always true.
P.P.S.
Assuming you are replying to me, you seem to be projecting very powerfully. Rust isn't my favorite language, and I just asked about how much of your code was safe vs unsafe.
No, I was replying to imtringued.
But to answer your question, I reach for a low-level language when I need the one and only thing low-level languages are designed to do best, which is give me full and precise control over the hardware. In those situations, Rust offers little safety in the interesting/subtle/critical code, and for me it even makes things worse. It helps with the simple uninteresting code. So it's not a matter of quantity but of quality.
Of course, if the tricky parts of the program don't require precise control over the hardware, I don't use a low-level language in the first place. For example, if I'm going to let the language manage memory for me, why suffer the high overhead of heap memory management in the Rust/C runtime when the Java runtime offers me lower overhead?
> That framing may seem intellectually satisfying, but it's not useful in practice.
Honestly, that's exactly how I feel in reverse. The framing you gave is more intellectually interesting, but it doesn't help explain the actual real-world outcomes where in practice, Rust and Java both don't have much problem with unsafety, whereas C does, and at least from what I've heard, Zig does as well.
> I reach for a low-level language when I want to do low-level things in a more convenient way than in Java, but the very things that would make me reach for a low-level language in the first place are unsafe in Rust. So in ~100% of the programs I want to write in a low-level language, Rust and Zig offer the same level of memory safety (but I need to pay a higher price for Rust). That Rust reminds me that what I want to do is unsafe doesn't help me.
I mean, sure, if you want to do things that are fundamentally not possible to validate because you think you're smart enough not to screw up, that's going to make Rust a tough sell. My issue with it is that history has shown that the best C and C++ programmers in the world still write code where memory safety rears its head, so I'm distrustful of the claim that being smart and diligent is enough to prevent the sort of bugs that we're still dealing with after half a century of us learning how not to write C. You need to have an excess of either talent or hubris to consider that a reasonably safe path, and given that the amount of talent needed is a lot higher than the amount of hubris, it seems way more likely that it's the latter.
The alternative is just learning how to write code that doesn't require expressing things in a way that can't be validated. While there are some things that fundamentally are not possible to, I'm dubious that it's anywhere close to as high as you seem to expect if your experience is that you literally can't reduce the amount of unsafe code you need in Rust below "literally my entire program is unsafe".
> Rust and Java both don't have much problem with unsafety, whereas C does, and at least from what I've heard, Zig does as well.
I'm not interested in the definition so much as I am in calling it "table stakes", and so the fact that these languages satisfy their promises is uninteresting in isolation. What matters is the value of their promises. The majority of Rust programs I see, I wouldn't have written in a low-level language, so the fact that it offers memory safety for the things I don't need it to do does nothing for me.
Now, clearly, Rust's originators didn't consider what Java offers (or at least what it offered 20 years ago when Rust was first conceived) to be table stakes or they wouldn't have wanted Rust. Java exacted some price in exchange for its memory safety that was unacceptable to Rust's originators and trumped its memory safety. But the same thing happens with Rust vs Zig. Rust exacts a heavy price for its memory safety, that - just as in Rust's case vs Java - is sometimes unacceptable. So I can't see how any of these could be "table stakes".
> I mean, sure, if you want to do things that are fundamentally not possible to validate because you think you're smart enough not to screw up, that's going to make Rust a tough sell.
What Rust can validate and what can fundamentally be validated are two very, very different things. Compared to what ATS can validate, what Rust can validate is almost indistinguishable from C. In Rust you have to do lots and lots of things that require you to be "smart enough not to screw up" that you could prove in ATS, and still no one (including Rust programmers) would say that what ATS offers is "table stakes" because, obviously, it comes at a high price that the people who choose Rust don't want to pay.
So clearly different languages offer different capabilities and charge a price for them. Sometimes the price is worth it and sometimes it isn't.
> so I'm distrustful of the claim that being smart and diligent is enough to prevent the sort of bugs that we're still dealing with after half a century of us learning how not to write C
But Java or Rust programs still suffer from a lot of bugs that ATS could eliminate, if you're willing to pay the price, and you're clearly unwilling. ATS programmers could say about Rust programmers what you say about C++ programmers. Clearly there's no universal table stakes here.
> I'm not interested in the definition so much as I am in calling it "table stakes", and so the fact that these languages satisfy their promises is uninteresting in isolation. What matters is the value of their promises. The majority of Rust programs I see, I wouldn't have written in a low-level language, so the fact that it offers memory safety for the things I don't need it to do does nothing for me.
I mean, if you're already going to say "I don't want a low level language for anything other than what I can use unsafe for", then of course Rust will seem like overkill. 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. That doesn't mean that the ones that need to be lower level can't be written in Rust though.
> Now, clearly, Rust's originators didn't consider what Java offers (or at least what it offered 20 years ago when Rust was first conceived) to be table stakes or they wouldn't have wanted Rust. Java exacted some price in exchange for its memory safety that was unacceptable to Rust's originators and trumped its memory safety. But the same thing happens with Rust vs Zig. Rust exacts a heavy price for its memory safety, that - just as in Rust's case vs Java - is sometimes unacceptable. So I can't see how any of these could be "table stakes".
Yes, "table stakes" is a value judgment, and one some people will disagree with. The cost for memory safety in Java is performance overhead though, and the cost for memory safety in Rust is not being able to express certain valid things that can't be validated; those are both objectively different from not offering memory safety at all, and my point is that the cases where what you want to express is literally impossible in Rust to do safely while actually being memory safe are pretty rare. There are some cases where what you're trying to do are fundamentally unsafe, in which case you need to use an unsafe block, but that's not anywhere close to the same as removing validation from the entire program. I'm fairly skeptical that you're basing your view that there are so many cases where you want to do something that's guaranteed to be safe but impossible to write in safe Rust on objective criteria, and extremely skeptical that the programs you write are anywhere close to entirely comprised of logic that can't be expressed safely.
> What Rust can validate and what can fundamentally be validated are two very, very different things. Compared to what ATS can validate, what Rust can validate is almost indistinguishable from C. In Rust you have to do lots and lots of things that require you to be "smart enough not to screw up" that you could prove in ATS, and still no one (including Rust programmers) would say that what ATS offers is "table stakes" because, obviously, it comes at a high price that the people who choose Rust don't want to pay.
> So clearly different languages offer different capabilities and charge a price for them. Sometimes the price is worth it and sometimes it isn't.
Sure, no one is disputing that. 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. It's obvious we won't see eye to eye on whether that's table stakes or not, but that's a difference of opinion, and having a different opinion than you isn't literally illogical; I find your take on it to be as hard to understand as mine is to you.
> But Java or Rust programs still suffer from a lot of bugs that ATS could eliminate, if you're willing to pay the price, and you're clearly unwilling. ATS programmers could say about Rust programmers what you say about C++ programmers. Clearly there's no universal table stakes here.
You're again taking an empirical argument as an abstract one and ignoring the real world outcomes that languages produce. 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, and that's fine, but it's meaningful for people who care about software actually getting used in the real world for real things. 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. 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 don't agree at all that not drawing any line at all is the only logical choice in a scenario when there are multiple places to draw it.
> 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.
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.
I don't think it's true that Java offers strictly more optimisation opportunities than low-level languages, but rather different optimization opportunities. C++ and Rust have other opportunities that Java does not generally have:
- Explicit object lifetime and deterministic destruction.
- Stack allocation by default.
- Value types and direct embedding of values in data structures.
- Precise control over data layout, alignment, padding, and SIMD-friendly representations.
- Explicit allocation strategies: arenas, pools, slab allocators, region allocators, custom allocators, memory mapping, etc.
- No mandatory tracing, write barriers, object headers, or garbage-collector scheduling.
- Native interoperation without an FFI boundary.
- More predictable latency behavior.
- Compile-time specialization through templates in C++ and monomorphized generics in Rust.
Rust’s ownership and borrowing model can also provide strong non-aliasing and mutability guarantees in safe code which are useful for optimization.
Saying that "Low-level languages sacrifice performance for control" is also not true imo, since they can avoid allocation entirely, store data contiguously rather than as individually allocated objects, avoid all gc work, control cache behavior and eliminate pointer chasing, and importantly, guarantee hard or soft latency bounds.
Saying that "Most virtual calls in Java are faster than many static calls in C" is not a meaningful claim either. I think? Cuz "it depends" :)
And remember that GC is not free. Objects must eventually be traced. Reference writes may require write barriers. Objects have headers and alignment overhead. Heap size must often be larger to maintain throughput. Concurrent collectors consume CPU and memory bandwidth. Stop-the-world phases or other latency issues remain even with modern collectors. Object movement can complicate native interoperability and pinning. Malloc can cause performance issues but Java is not beating an arena allocator in C++ or Rust.
Keep in mind speed and throughput are not the only performance metrics. So is startup time and memory footprint, where Java loses badly here.
I wish we had real published research to go off of here but real world examples are all we really have. I'm not seeing AAA game studios building their engines in Java. I don't see any OS's building their kernel (or anything really) in Java. If Java is faster than low level languages, why is that?
And look, I don't dislike Java or the JVM, and I'm actually a really big fan of C#. I just like Rust more.
> Stack allocation by default, Value types and direct embedding of values in data structures.
This used to be the big one, but not anymore: https://openjdk.org/jeps/401 (so Valhalla is integrating in JDK 28; it's not complete and this JEP is only the first step, but Java will have everything it needs on that front very soon.
> Precise control over data layout, alignment, padding, and SIMD-friendly representations.
Since the JVM controls layout, this is a point for Java.
> No mandatory tracing, write barriers, object headers, or garbage-collector scheduling.
Java doesn't require these things. JVM implementations choose to have them as an optimisation.
> Explicit object lifetime and deterministic destruction.
Hypothetically, this could have been an optimisaton opportunity. In practice, this is not a problem for Java but it is a problem for low-level languages, and especially Rust. The problem isn't knowing when an object's life is over, but needing to do something about it then and there. The whole idea of moving collectors (and arenas) is that it is more efficient to do nothing when an object becomes unreachable, and knowing when that happens doesn't help.
> And remember that GC is not free
Of course it isn't, but moving collectors are cheaper than free-list-based approaches, which is why we use them (most objects are never traced; those that are, are traced rarely etc.). On the whole, moving GCs are a speed improvement, reducing the overheads in C's runtime, and what they take in exchange is footprint (i.e. they use RAM chips as hardware accelerators). Now, that footprint cost could be expensive in smart watches and smaller devices, but in larger ones, the tradeoff is almost always worth it. I gave a talk about exactly that at Java One that should be up on YouTube eventually.
> Saying that "Low-level languages sacrifice performance for control" is also not true imo, since they can avoid allocation entirely, store data contiguously rather than as individually allocated objects, avoid all gc work, control cache behavior and eliminate pointer chasing, and importantly, guarantee hard or soft latency bounds.
I don't agree with that, and that's the very crux of my point. What you're really saying is that fine-grained control over the hardware lets a user who works hard enough to optimise their program to any level they choose. This does work well in small programs, but it fails in larger ones, and the JVM is designed to solve the performance issues that we C++ programmers experience in large programs. Over time, as programs grow and become more elaborate, it gets harder and harder to do those manual optimisations, which are very intrusive. E.g. you could perhaps use arenas in Rust more-or-less safely, and maybe Rust will make it easier in the future (right now the only language that makes that easy is Zig), but changing from no-arena to arena or vice versa, or finding out that you need special cases to some objects, requires a huge change to a low-level program. Similarly, trying to keep virtual calls to a minimum is very easy in the beginning, but becomes harder over time. So the idea that with enough control you can do anything is true in principle, but in practice it's very hard as programs evolve. The idea of modern runtimes is that you write the code naively, and the runtime performs global optimisations that help the average-case performance.
> I'm not seeing AAA game studios building their engines in Java. I don't see any OS's building their kernel (or anything really) in Java. If Java is faster than low level languages, why is that?
Much more performance-sensitive software is written in Java than in C++ these days, and your question assumes that the main thing that's important in these particular is speed, but that is not the case. What is the main thing kernels need to do? Directly control hardware. And what is the one thing that low-level languages are optimised for? Direct control over the hardware. Low-level languages fit the domain of OS kernels like SQL fits data queries; that's what they're for.
As for games, first, performance isn't the issue here. The most performance-critical parts of a game are not written in C++ but in CUDA, and the main important part for the CPU is a good algorithm for scheduling the data to the GPU, and that can be done in any language. What is very important for games is hardware support, and the JVM simply doesn't target most consoles. Second, games do care about latency, at least up to the length of a frame, and until very recently Java had GC pauses, and those could sometimes exceed the latency needed by games. GC pauses were removed in HotSpot only 3 years ago. So these are the reasons game engines are normally written in C++ (except, of course, for the most successful game in history, which is written in Java).
> 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.
> 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.
> Regarding the silly point: Fil-C is less of a rock than Safe Rust as everything in C just works.
I'm going to challenge this. Is this from your experience? InvisiCaps, which Fil-C is based on, turns out of bounds accesses into panics. You're saying that turning any "benign" out of bounds into "abort the program" in all the C you? anyone? everyone? uses Just Works?
I'd rather the majority of those failure modes be caught by linters/compiler passes, not runtime safe aborts.
It certainly works in my C programs. And Fil-C demos imply that this also works for a lot of other complex real world programs, i.e. a basic Linux distribution with libreoffice on top... So yes, I would say this just works. I agree that catching this at compile-time is better, and to some degree this also works: https://godbolt.org/z/sse74vK9o
Rust does not offer compile-time guarantee that an out-of-bounds access does not panic either.
People find it immensely useful in practice, though.
They find it immensely useful in practice only when the safe subset is useful. In C, you could say that the same definition of memory-safety exists, only the safe subset is empty, and in that case people don't find the fact that C could be described as memory-safe in that way useful at all.
And that's exactly my point. The safe subset of Rust doesn't sufficiently cover the very things that I choose a low-level language for in the first place, and the parts it does cover I can do at least as well in even safer high-level languages.
Now, I'm not saying there aren't programs where Rust's precise mix of safety and unsafety is better than the alternatives. For example, I've never written a browser rendering engine, and it's possible that I would find Rust to be the best fit for that task. I don't know that I would, but I also have no experience with that domain to suggest that I wouldn't.