Yes, and I think it's good to push back on that. I personally feel it's pretty misguided. I've been meaning to write about this, but haven't... so I'll just leave a comment, heh. Someday...
A related comment I wrote a while back about this: https://news.ycombinator.com/item?id=24992747
However, that example commits the Rust "sin" I always see with "spawn" explanations in that it packs everything into a closure.
Please, please, please ... for the sake of newbies everywhere ... please define a function and call that function from inside the spawn closure. This is one of the Rust things I spend the most time explaining and unpacking for junior engineers.
The separate function makes explicit the variables that are moving/borrowing/cloning around, which names are external/passed/closure-specific, and how it all ties together. It breaks apart the machinery that gets all conflated with the closure. They can always make it succinct again later once they understand the machinery more completely.
This gets particularly bad with "event loops" (I have a loooooong rant I need to write up about event loops and why they are evil--and it's not Rust specific) and deep "builder chains" (which I consider a Rust misfeature that desperately needs to go away).
It is probably because of all my years with Ruby that this seems so off to me, but also, I hear you. Hmm.
The issue in Rust is that "spawn" is generally the first time that a programmer is forced to confront closures--before that they can completely ignore them.
It is a quirk of Rust that someone who "just wants to spawn a thread" suddenly gets all this language overhead dumped on them in a block.
> might be my bias coming from a more functional background before learning Rust
This is exactly the problem. The people I'm dealing with are coming from "imperative land" and haven't had a functional background. Someone stepping up to Rust as "C with memory safety" does not have any of that functional background.
Please do remember that a "closure" is built on a lot of prior abstractions. What is a "scope"? What is "variable capture"? What is "heap" and "stack"? Why does that matter here and not normally?
No programming language is only used by experts, and Rust is no exception.
The issue is that "spawn" smacks you with a bunch of that baggage all at once.
I don't understand how a function that is defined in-place is more clear than a nice function on its own. Perhaps because it gives a sense of continuity (like in async code), but man, it's really painful to read (at least to me). In fact, if I have to write some Rust right now, all my closures would be defined elsewhere, whenever possible.
A lot of embedded code NEVER calls malloc (thus the need for no_std in Rust). Consequently, you can go a long way in embedded without really knowing the difference between stack and heap.
"Embedded programmer" does not imply "Linux Kernel Hacker".
When I started with Rust, I actually thought that the common
spawn(|| …)
syntax was something special, not just a closure with no variables.
On the other hand "just use Arc" is definitely smelly advice. I think going down that route, a new Rust programmer is likely to try to do something that really won't ever compile, and wind up really frustrated. Maybe we can distinguish this often-really-bad advice from the other mostly-ok-at-first advice?
Yes, I guess to me they're distinct, but I can totally see how it may seem similar. I'll try to make sure to make that explicit when I talk about this, thanks!
But "scoped_threadpool" uses "unsafe".[1] They could not actually do this in Rust. They had to cheat. The language has difficulty expressing that items in an array can be accessed in parallel. You can't write that loop in Rust itself without getting the error "error[E0499]: cannot borrow `v` as mutable more than once at a time".
And, sure enough, the "unsafe" code once had a hole in it.[2] It's supposedly fixed.
If you look at the example with "excessive boilerplate" closely, you can see that it doesn't achieve concurrency at all. It locks the entire array to work on one element of the array, so all but one of the threads are blocked at any time. To illustrate that, I put in a "sleep" and some print statements.[3]
You could put a lock on each array element. That would be a legitimate solution that did not require unsafe code.
To do this right you need automatic aliasing analysis, which is hard but not impossible. (Been there, done that.) Or a special case for "for foo in bar", which needs to be sure that all elements of "bar" are disjoint".
[1] https://github.com/reem/rust-scoped-pool/blob/master/src/lib...
[2] https://www.reddit.com/r/rust/comments/3hyv3i/safe_stable_an...
[3] https://play.rust-lang.org/?version=stable&mode=debug&editio...
Using unsafe isn't "cheating". The whole point of using Rust is that you can encapsulate very small amounts of "unsafe" code within a safe abstraction, and know that there is no way to abuse the safe APIs to produce undefined behavior. The formal verification project "RustBelt" [0] has proven exactly this -- safe Rust code composes arbitrarily to produce safe behavior.
There is still a burden of proof on anyone writing a safe function that uses unsafe functionality internally. Keep in mind that "unsafe" Rust still poses more restrictions on the programmer compared to raw C/C++.
> The language has difficulty expressing that items in an array can be accessed in parallel.
The Rust borrow semantics is based on `exclusive XOR shared` references. At the first order of approximation it's not possible to mutate a value through a shared reference. Interior mutability complicates this image somewhat, but it's enough to explain why what you wrote is wrong.
Spawning `n` threads and giving each thread a mutable reference to the vector `v` goes against the above borrow semantics because that would result in `n` exclusive references. Only through exclusive references is mutation allowed. Since this is not allowed it becomes a compile-time error.
In other words, it's not that the language has "difficulty expressing" that scenario. It was explicitly designed to not allow it.
[0]: http://plv.mpi-sws.org/rustbelt/"Within a safe abstraction" is the question. The question is whether a piece of encapsulated code is always memory-safe for all uses. There's no guarantee of that from the language. The RustBelt people are working on tools for that, but it will probably require proof work in Coq for each bit of code containing "unsafe" to get there.
Look at the scoped_threadpools example again [2]
use scoped_threadpool::Pool;
fn main() {
let mut pool = Pool::new(3);
let mut v = vec![1, 2, 3];
pool.scoped(|scope| {
for i in &mut v {
scope.execute(move ||{
*i += 1;
});
}
});
println!("v: {:?}", v);
}
There's an implicit assumption here that all the values returned from the iterator at for i in &mut v
are disjoint. But iterators, in general, do not guarantee disjoint outputs. An iterator which returned each reference value twice, for example, is a valid iterator. But used in the context above, two threads would receive mutable access to the same element of a vector. That seems to violate a core Rust safety assumption.This would be a nice test to run through the version of the Miri interpreter that implements the dynamic "stacked borrow" checks from the RustBelt group.[2] That tool should catch this.
[1] https://news.ycombinator.com/item?id=24992747
[2] https://plv.mpi-sws.org/rustbelt/stacked-borrows/paper.pdf
> for i in &mut v
> are disjoint. But iterators, in general, do not guarantee disjoint outputs. An iterator which returned each reference value twice, for example, is a valid iterator. But used in the context above, two threads would receive mutable access to the same element of a vector. That seems to violate a core Rust safety assumption.
Your assumptions here are wrong. "An iterator which returned each reference value twice" could only be implemented using unsafe Rust, and code emitting multiple co-occuring mutable references to the same value are instant-UB. In other words, a "safe abstraction" being able to do this is actually not a safe abstraction at all. This would be a bug in the implementation of the custom iterator.
In fact, if you tried to prove such an implementation using the formal tools (Iris) from RustBelt you wouldn't be able to do it. I know, because I've done it as part of my course work under Lars Birkedal.
I'm not sure if you misunderstanding how `&mut v` turns into an iterator, which values it spits out etc, or what is happening. Let's look at how that code is translated into a program and typechecked. First, let's look at the for-loop itself. Anything on the form of
for x in y
must have an implementation of the `IntoIterator` trait for the type of the expression `y`. Whatever that type might be. In this case `y` is actually `&mut v`, where `v`'s type is `Vec<i32>`. Luckily enough, an implementation of `IntoIterator` exists for any `&mut Vec<T>`. [0]Looking at the expanded signature we can see that the associated type `Item` is `&mut T`. This tells us that the types the resulting iterator will produce are `&mut i32`. Keep in mind that this reference is into the backing memory store of the `Vec<i32>` of `v`. This is where Rust's semantic help us.
You say the following:
> But iterators, in general, do not guarantee disjoint outputs.
The issue with that statement is that iterators don't have to. Rust guarantees that condition. Safe Rust can't return multiple mutable references to the same value. Further, any unsafe implementation, even when exposed through a safe interface, giving this behavior is a bug. It would be great if the compiler could statically verify unsafe code too, but if it could there wouldn't be a need to call it unsafe.
> The question is whether a piece of encapsulated code is always memory-safe for all uses. There's no guarantee of that from the language.
That is ultimately impossible to prove completely and statically. Turing and Gödel made sure of that. However, some things that the compiler can't prove we can still prove as outside observers. Often these things will boil down to controlling and checking the states of several variables before performing an unsafe operation, but knowing that in a given context the operation is safe.
The first part of the above quote is partially true. However, if we assume a given unsafe implementation is indeed safe then the rules of Rust's semantics make the composition of any number of safe APIs a completely safe composition. This is a strong, useful statement because it means the very few places where unsafe is required can be easily checked, and every other piece of code doesn't have to spend mental energy for the programmer to consider the safety of their implementation.
[0] https://doc.rust-lang.org/std/iter/trait.IntoIterator.html#impl-IntoIterator-19So how does Vec return mutable references to elements? Unsafe code, apparently. You can't write your own safe collection class with an iterator and return mutable references. You'd need a proof of disjointness system to do that.
The question is whether a piece of encapsulated code is always memory-safe for all uses. There's no guarantee of that from the language. That is ultimately impossible to prove completely and statically.
One can construct code for which memory safety is not decidable. That's a good reason to reject it. The Microsoft Static Driver Verifier has a simple solution - if symbolic execution can't verify safety within some time limit, it rejects the driver.
If you have a mutable binding to a vector `v` you can get a single mutable reference to one of the elements of that vector at a time. Because getting a single mutable reference to an element requires taking a mutable reference to the vector the disjointedness invariant is upheld. This is the core of the Rust borrow checker. If you're unsure about the semantics you should be reading about it, not trying to sus them out from a fairly surface level discussion on HackerNews.
This code:
for x in &mut v {
*x = // ...
}
is perfectly valid. For every iteration of the loop only a single mutable reference to an element exists, and the reference is dropped before the next mutable reference is taken. This following piece of code is a full example that showcases how taking two mutable references is not allowed. struct ComplexNonCopyClone {
a: u32,
}
fn main() {
let mut v = vec![
ComplexNonCopyClone { a: 1 },
ComplexNonCopyClone { a: 2 },
ComplexNonCopyClone { a: 3 },
];
let mut_one = v.get_mut(0).unwrap();
let mut_two = v.get_mut(0).unwrap();
mut_two.a = 4;
println!("{}", mut_one.a);
}
Do note that using `mut_one` after `mut_two` is important, as otherwise the compiler will infer that `mut_one` can be dropped before `mut_two` is create, removing the clash.> So how does Vec return mutable references to elements? Unsafe code, apparently.
The implementation can be found quite easily[0]. It's essentially invariants upheld by runtime checks and knowledge about the state of the given references/pointers. Notice the comments starting with "SAFETY". They explain the assumptions/conditions/reasons that make the code inside the `unsafe` block safe. If these assumptions can never be violated with malicious input or calling patterns then the code is actually safe and it can be a safe wrapper.
[0]: https://doc.rust-lang.org/src/core/slice/index.rs.html#149-190To recap: the interesting issue is the thread pool example which spins off a new thread with a mutable reference to each element of an array. That's a rather unusual thing to do. It is only safe if each iteration returns a reference disjoint from all other references. Because Rust at the language level does not understand disjointness within an array, it needs a hack using "unsafe" to make this work. The borrow checker would not allow this in safe code.
chunks_mut uses the same trick, with more unsafe code.
My point in all this is that because the language doesn't have syntax for talking about disjointness of parts of an array, each time this comes up, another hack in unsafe code is required. But enough here; I may say more on the Rust forums.
That isn't true at all. The implementation of scoped_threadpool being in rust is all the evidence you need that they did not cheat. unsafe is a perfectly valid construct in rust that does not subvert its benefits. It just is a reversal of the defaults in other languages (unsafe by default - opt-in to safer constructs like smart_ptr).
People freak out too much over this stuff. If you are learning - feel free to use escape hatches from "idiomatic rust" but recognize that you aren't being idiomatic (and therefore may be handicapping your learning!). Its okay to use unsafe despite idiomatic rust trying to avoid it unless necessary (and even then providing safe wrappers over it). Its also okay to use Arc<Mutex> and the like if you need to just recognize there may be a better way.
thread::spawn uses unsafe code. sleep uses unsafe code. Is your own example not in Rust? Are you cheating?
(And yes, the spawn version isn’t optimally concurrent. The comment was long enough without getting into that. People struggle to get examples to compile before they worry about things like this. That would be next steps.)
There are a relatively small number of trouble spots. They include, at least:
- No way to talk about partially initialized arrays in Rust, which leads to Vec needing "unsafe".
- Back references, which makes some kinds of trees, and of course doubly-linked lists, difficult.
Those I've mentioned before. From the discussion above, add:
- Problems around interior mutability within arrays, where you need some way to say, and prove, "a is disjoint from b", before working separately on a and b.
These are classic proof of correctness areas. It's fairly straightforward to state the conditions that have to be proven, and usually not that hard to prove them. The RustBelt crowd seems to be working on this.
Maybe they'll come up with a way to prove out conditions like that, short of grinding away by hand in Coq. Most of those problems are within reach of a fully automatic prover, like a SAT solver.
You're right, but current Rust cannot express logical preconditions in code, much less compiler-checkable proofs. Many groups are working on this via a variety of approaches, but it will take some time before there's a common standard for expressing these things as part of the Rust language itself.
I agree with Animats here, to work around typesystem issues means that the type system still isn't expressive enough to define certain proprieties in a safe way.
This doesn’t seem correct at all?
GC merely solves freeing memory when it is no longer needed. But it does not solve parallel access.
One of the beauties of Rust is that you can write highly parallel code and if it compiles it works. Meanwhile Python languishes behind the GIL. GC does not even attempt to solve multithreaded memory access.
I’m happy to be corrected if I’m wrong or missing something here.
For example:
fn foo<'a>() -> &'a [u8] {
let v = vec![0, 1, 2];
// returns a value referencing data owned by the current function
v.as_slice()
}
vs Go's: func foo() []uint8 {
// subject to escape analysis, but code like this would likely return
// a fat pointer into the heap--no complaints because there's a GC.
return []uint8{0, 1, 2}
}
> One of the beauties of Rust is that you can write highly parallel code and if it compiles it works. Meanwhile Python languishes behind the GIL. GC does not even attempt to solve multithreaded memory access.Python's GIL is unrelated to GC, but yes, Rust's borrow checker guarantees correct parallel access of memory. But I think this benefit is overblown for a couple reasons:
1. contrary to popular opinion, if you've learned how to write parallel programs, it's not tremendously difficult to write them correctly without a borrow checker. In my experience, whatever time I've saved debugging pernicious data races is lost by the upfront cost of pacifying the borrow checker. Maybe this wouldn't hold for people who aren't experienced with writing parallel code (but I imagine such people would have a harder time grokking the borrow checker as well).
2. most data races in my experience aren't single threads on a host accessing a piece of shared memory, but rather many threads on many hosts accessing some network resource (e.g., an S3 object). The borrow checker doesn't help here at all, but you still have to "pay the borrow checker tax".
Again, this isn't a tirade against the borrow checker, but an insistence that tradeoffs exist and it's not just a "you're just doing it wrong" sort of thing.
Maybe I’m just a bad enough programmer I write parallel bugs. But C++ certainly doesn’t make it easy to write correct code in any way.
I personally think it’s pretty darn difficult to ensure correctness in a large program. Especially when multiple programmers are involved. And especially when you are adding features to an existing system.
However I will also admit that I haven’t written a large Rust program so I can’t claim to have run into all of its warts.
There’s no silver bullets in life. I work primarily in video games. GC is the bane of existence and is something that provides seriously negative value.
We definitely agree it’s all a trade off. GC provides some value and some costs. Borrow checkers provide some value and some cost.
> It only means that the data model doesn't agree with Rust's rules for modeling data (which exist to ensure memory safety in the absence of a GC).
I think my initial response was that Rust’s model exist for more than just that single reason. Whether those reasons or trade offs are useful depend on the program in question.
In my work I never want a GC, but damn would I love a borrow checker.
The problem with ownership-based safety for massive parallelism is that the mechanics of agreeing on and determining ownership don’t scale and often can’t be determined at compile-time. Some other safety mechanics don’t have these limitations. C++ doesn’t have them built-in but you can implement them.
IMO the key to writing parallel code is to keep the parallelism confined to a small kernel rather than sprawling throughout your codebase. If you try to bolt on parallelism then you’re going to have a bad time. It needs to be part of your architecture. It’s not easy, but it’s easier than writing parallel code that will pass the borrow checker IMHO. But yes, we may have to agree to disagree.
> We definitely agree it’s all a trade off. GC provides some value and some costs. Borrow checkers provide some value and some cost.
Agreed!
> I work primarily in video games. GC is the bane of existence and is something that provides seriously negative value.
I’m very curious about videogames development. In particular, I get the impression that aversion to GC in videogames comes down largely to experiences with Java back when pauses could be 300ms. I’m very curious if Go’s sub-millisecond GC (and its ability to minimize allocations, etc) would be amenable to videogame development. Thoughts?
> I think my initial response was that Rust’s model exist for more than just that single reason. Whether those reasons or trade offs are useful depend on the program in question.
Heartily agree.
> In my work I never want a GC, but damn would I love a borrow checker.
In my line of work, I like the idea of using Rust but realistically the economic sweet spot is something like “Go with sum types” or “Rust-lite”.
It really just comes down to average/worst case time.
60Hz - 16ms 90Hz - 11ms 120Hz - 8ms 144Hz - 7ms 240Hz - 4ms
Modern games are expected to run anywhere from 60 to 240 frames per second. 60 is the new baseline. VR runs anywhere from 72 to 120. Gaming monitors regularly hit 144Hz. And esports goes as high as 240 and even 360.
In Unity C# GC can take tens of milliseconds. This is, uh, obviously very bad. High tier unity games spend a LOT of time avoiding all allocs. This is not fun in a GC language. Most indie games just hitch and its pretty obvious. I’m not sure if Unity’s incremental GC has graduated from experimental mode.
If a GC had a worst case time of less than a millisecond that’d be fine. That’s actually a pretty big chunk of a 7ms frame, but hey probably worth it. If it’s usually 250us but once a minute spikes to 3ms that’ll cause a frame to miss. If once every 5 minutes it’s a 50ms GC that’s a huge hitch. For a single player game it’s sloppy. For a competitive multiplayer game it’s catastrophic.
Unreal Engine actually has a custom garbage collector. But it’s only for certain days types and not all memory. That’s a nice compromise. Games in particular are good at knowing if the lifetime of an allocation is short, per frame, long-term, etc.
Well, Minecraft is written in Java, and it runs fine from what I’ve heard. In .NET land, there was a short lived toolkit for C# called XNA - Terraria is (was?) written in it. Both Java and C# are garbage collected.
I haven’t looked at Unity too deeply, but isn’t Unity (and the games made in it) built in C#?
I think C# is popular because it allows the above. When Java has proper value types it might be suitable for writing games.
To be clear, the problem isn’t pointer indirection, but rather lots of objects on the heap, right? Pointers should be fine as long as there aren’t many allocations (e.g., pointers into an arena)?
> I think C# is popular because it allows the above. When Java has proper value types it might be suitable for writing games.
Go also has value types, FWIW, and they are a lot more idiomatic than in C# from what I’ve observed.
AKA Ocaml.
That’s a dangerous misconception. The Rust ownership model only guarantees that the program is free of data races. That’s a necessary but not sufficient condition of program correctness.
Only for threads accessing in-memory data structures, it does nothing for other kinds of data access scenarios either to external resources or via OS IPC mechanisms.
I'd add: some GC'd languages solve this with sending around deeply immutable objects (functional languages mostly), in a way that can be more flexible than the way Rust handles immutable objects.
I hypothesize that this mostly comes from a laziness in response as it's an easy response to give to people who are used to having a garbage collector. I come from the C world (still learning Rust) and every time I see one of these pieces of advice given I'm forced to facepalm.
Some people just try to force their way into a new language and don't realize that if you keep doing something that looks stupid or weird it probably is (and no, yours is not a special case)
I think the use case is "I haven't yet completely grokked the borrow checker and/or I don't have time to pacify it, but I would prefer not to copy potentially large data structures all over with Clone".
> Some people just try to force their way into a new language and don't realize that if you keep doing something that looks stupid or weird it probably is (and no, yours is not a special case)
You're responding to my comment which is about the Rust community prescribing this as a solution to newcomers. We're not talking about newcomers obstinately refusing to learn new idioms in the language they allegedly want to learn (although no doubt this happens, especially if the language in question is Go :p ).
For basic stuff I agree, though I wouldn't use it.
> about the Rust community prescribing this as a solution to newcomers.
There's probably a sweet spot for using those constructs in not so obvious places while going through the simpler stuff in a more idiomatic way
To me it's not about performance. A little bit of time spent now appeasing the borrow checker will pay off ten fold later when you don't have to deal with exploding memory usage and GC stalls in production.
GC is great for quick hack jobs, scripts, or niches like machine learning, but I believe at this point it's a failed experiment for anything else.
I'm confused by the "it's not about performance. [reasons why it is, in fact, about performance]" phrasing, but in general a lot of applications aren't bottlenecked by memory and a GC works just fine. Even when that's not entirely the case, they often only have one or two critical paths that are bottlenecked on memory, and those paths can be optimized to reduce allocations.
> GC is great for quick hack jobs, scripts, or niches like machine learning, but I believe at this point it's a failed experiment for anything else.
That sounds kind of crazy considering how much of the world runs on GC (certainly much more than the other way around). I feel the need to reiterate that I'm not a GC purist by any means--I've done a fair amount of C and C++ including some embedded real time. But the idea that GC is a failed experiment is utterly unsupported.
That's the story that GC sold us. History has proven it wrong. Citation: the fire hose of articles on HN about how GC bit people in the ass, and they now have to go back into their code and write a bunch of duct tape code to work around Garbage Collector Quirk #4018 de jure that results in hitching, insane memory usage, and random OOMs.
> That sounds kind of crazy considering how much of the world runs on GC
And much of HN runs on comments complaining about the _absurd_ amounts of memory all those non-bottlenecked applications use to do otherwise simple tasks. Or the monthly front page articles about developers and companies working to fix their otherwise straightforward, non performance critical production services that are choking themselves because the GC is going wild.
I say GC is a failed experiment because it promised that programmers would be able to write code without worrying about memory. But ever since its popularization 26 years ago with the dawn of Java, coders writing in garbage collected languages have been doing nothing but worrying about memory. The experiment failed. It's time to move on.
The borrow checker is an infantile incarnation of a bigger idea that is finally panning out. Rather than garbage collecting during run-time; garbage collect during compilation using static analysis. Being in its infancy it's not as easy and free to use as we'd like. But it's the path forward. And just like garbage collection before it, in the vast majority of cases, programmers don't care whether it's more or less performant. Garbage collection was vastly less performant than manual management. But it required _so_ much less developer time to build the same applications. My argument is that Rust's borrow checker, as painful as it is, results in more developer time up front, but less developer time overall when you consider the long tail of code upkeep that garbage collected applications demand.
Hence my comment: "A little bit of time spent now appeasing the borrow checker will pay off ten fold later when you don't have to deal with exploding memory usage and GC stalls in production."
It's not about performance; it's about saving yourself the time of having to come back to your code a month later because your TODO app is using a gig of RAM and randomly hitching.
If your GC program is using excessive RAM, that’s because of a memory leak, not the garbage collector. This can happen in C/C++ as well; just malloc/new and forget to free/delete. Last I checked, C and C++ aren’t garbage collected languages.
Note also that GC languages don’t even have the circular references case to worry about since they don’t have any need for reference counting in general.
That’s my memory and battery capacity, not theirs.
I follow HN daily and very rarely do I see articles lamenting GC (I'm only familiar with a small handful of incidents including some pathological cases with Go's GC on enormous heaps (many terrabytes) and some complaints about Java's GC having too many knobs), certainly not in the general case. Indeed, for the most part people seem quite happy with GC languages, especially Go's sub-ms GC. In particular, memory usage (and thus OOMs) have nothing to do with GC--it's every bit as easy to use a lot of memory in a language that lacks GC altogether. This is incorrect, full stop.
> I say GC is a failed experiment because it promised that programmers would be able to write code without worrying about memory.
GC promises that programmers don't have to worry about freeing memory correctly, and it delivers on that promise. I'm not a GC purist--there's lots of criticism to be had for GC, but we don't need point at patently false criticisms.
> The borrow checker is an infantile incarnation of a bigger idea that is finally panning out. Rather than garbage collecting during run-time; garbage collect during compilation using static analysis. Being in its infancy it's not as easy and free to use as we'd like. But it's the path forward.
Maybe. I like the idea, but I'm skeptical that putting constraints on the programmer is going to be an economical solution, at least for so long as the economics favor rapid development over performance. Conceivably rather than rejecting code that aggrieves the borrow checker, we could picture a language that converts those references into garbage collected pointers transparently, but we kind of have this already today via escape analysis--and indeed, I think this is the economic sweet spot for memory management because it lets users have a GC by default but also minimize their allocations for hot paths.
> Hence my comment: "A little bit of time spent now appeasing the borrow checker will pay off ten fold later when you don't have to deal with exploding memory usage and GC stalls in production."
But the borrow checker is strictly less effective in preventing memory leaks than a (tracing) GC (borrow checker will happily allow circular refcounts). More importantly, having to pacify the borrow checker on every single codepath when only 1-2% of code paths are ever going to be problematic is not a good use of your time, especially when you can do some light refactoring to optimize. With respect to GC stalls, these are particularly rare if you have a GC that is tuned to low-latency (Go's GC can free all memory in less than a millisecond in most cases).
> It's not about performance; it's about saving yourself the time of having to come back to your code a month later because your TODO app is using a gig of RAM and randomly hitching.
That sounds like the textbook definition of a performance concern, but again memory usage is orthogonal to GC and random hitching isn't a problem for latency-tuned GCs. Even while Rust is faster than many of its GC counterparts, this difference typically comes down to the ability of the compiler to output optimized code--not the memory management system. That said, for realtime applications, nondeterministic GCs aren't appropriate.
tl;dr: there are significant GC performance problems that a borrow-checker doesn’t solve. I can imagine cases where the performance improvement is significantly less than you might hope.
I'm guessing this performance difference isn't caused by GC but rather correlated with GC. I.e., GC langs tend to output relatively poorly-optimized code relative to the absolute beastly C/C++/Rust compilers or else idiomatic code results in objects scattered all over the heap (killing cache coherency) while idiomatic C, C++, Rust, etc tend to allocate coherent objects (objects which tend to be accessed in succession) next to each other in memory.
TL;DR: No doubt a GC can be slower than manually managed memory in some cases, but it's insufficient to conclude that the entire performance gap between C/C++/Rust and Java/Go/etc is attributable to GC.
> If you find yourself slapping Mutexes and Arc/Rc all over the place it probably means that there's something messed up with the way you modeled data ownership within your program.
It only means that the data model doesn't agree with Rust's rules for modeling data (which exist to ensure memory safety in the absence of a GC). This doesn't mean that the programs the user wants to express are invalid. And this really matters because very often it doesn't make economic sense to appease the borrow checker--there are a lot of code paths for which the overhead of a GC is just fine but lots of time spent battling the borrow checker is not fine, and I think Rust could use a better story for "gracefully degrading" here. I say this as a Rust enthusiast.
EDIT: I can also appreciate that Rust is waiting some years to figure out far it can get on its ownership system alone as opposed to building some poorly-conceived escape hatch that people slap on everything.