Go is slower than C and Rust, IME, for two big reasons:
1) It puts a lot of stuff on the heap, that could've been on the stack. This wrecks locality and makes Go call malloc/free a lot. (Actually not the malloc, just a malloc.)
2) Rust and C++ offer fine control over things like dispatch, checked and unchecked casts, control for allocation, strings, etc. Go usually offers one unified thing that's simple to use. So a lot more calls are virtual, a lot more things are checked at runtime, etc.
I don't understand why garbage collection is so mythologized. TBH I don't even understand why people think `Arc` and `shared_pointer` aren't garbage collection. Is this just the effect of Swift and Rust's marketing ("look, ma, no GC")?
"This wrecks locality and makes Go call malloc"
it didn't make sense to me, thank you!
Because when people talk about garbage collection in the context of the dichotomy between Rust and Java or Go, they talk about the latter being automatic memory management based on various heuristics, with the related runtime overhead, non-deterministic, etc.
Whereas Rust Arc is just reference counting and explicit, not to mention enforced at compile time.
I don't understand how and why anyone would consider the two as the same thing. They are both GC in the same sense that cars and boats are both vehicles.
Instead, I'll use the term "automatic dynamic lifetime determination".
In a Java-style GC, there exists code that runs at runtime to determine whether or not the lifetime of a piece of memory can be determined to have expired.
In a reference-counted system, there exists code that runs at runtime to determine whether or not the lifetime of a piece of memory can be determined to have expired.
Of course, in the latter case the "code" is just checking a single counter rather than doing fancy concurrent root-tracing, but the similarity is established, and the alternatives are clearly delineable:
1) "Automatic static lifetime determination": this is what Rust does with ownership, by statically analyzing your code and automatically inserting code to free memory at statically-determined points.
2) "Manual static lifetime determination": this is the classic C-style memory management, where the author manually inserts code to free memory at statically-determined points.
You also need to do some fancy tracing for reference counting, because when a big data structure 'dies', you need to trace it completely.
Of course, reference counting also pays the price of fiddling with its counters on every single read. So every read-only workload turns into a read-write workload.
Garbage collection has no such overhead until you start the collector. (Basically all the overhead is amortised.)
* ARC is not deterministic outside of toy examples
* The runtime overhead of modern GC algorithms has a lower bound than ARC, and this has been proven formally
* Reference counting is usually atomic, so it implies other wonderfully expensive things like CPU cache flushes, memory fences, etc.
* The runtime overhead of deferred GC also happens outside of the hot path, and doesn't do unpredictable non-local things if you release a reference at an inconvenient moment
* ARC is in fact a GC strategy. E.g. obj-c has autorelease pools that postpone deletion until later, so the hot path doesn't get interrupted if reference count of something reaches zero early. These things are a spectrum.
Rust leans a lot on the borrow checker, and both Rust and C++ nudge you toward using fewer heap small allocations because it is inconvenient to use them. This is where a lot of the performance wins come from, not because reference counting somehow shaves off 20% of the program's runtime.
In fact, ARC is not only a GC strategy, it is close to being the worst possible GC strategy.
* ARCs non-determinism doesn't matter outside of contrived cases. In practical use it's as deterministic as expected.
* The runtime overhead of modern GC algorithms might have been proven formally to have a "lower bound than ARC", but like many things proven formally, it's seldom the case in practice. And this isn't also counting the memory overhead from using GC. Just like the oft-repeated "JIT can be faster than compiled because it has knowledge of the runtime" but in practice that only happens on special cases, and compiled C/C++/Rust still beats its ass.
As for the runtime overhead of deferred GC happing "outside of the hot path", and yet every domain with a GC and high load/low latency has historically suffered GC pauses, and GCs have to be reworked to ever more complex byzantine schemes to try to reduce the issue.
>*ARC is in fact a GC strategy. E.g. obj-c has autorelease pools that postpone deletion until later, so the hot path doesn't get interrupted if reference count of something reaches zero early. These things are a spectrum.
Yes, like cars and 18-wheelers are a spectrum. And C++/Rust is on the other side of that spectrum compared to Java and Go.
As soon as you use atomic counters in a multi-threaded system you can wave goodbye to your scalability too!
You think it's because ARC is a better way of managing memory, but that's false. Rust is not fast because it uses ARC, it uses ARC because Rust is fast. Rust is fast because it has a good ownership model for memory, because it encourages patterns that mostly get rid of the need to dynamically manage lots of heap objects. These qualities make it cache-friendly, among other things. What's left is generally small enough that even a poor GC strategy like ARC is good enough. A state-of-the-art GC wouldn't be worth the extra complexity in Rust. (And would probably conflict with other features.)
Go needs a state of the art GC, because it wants to hide "stack vs heap" from the programmer, and so it puts a lot of things on the heap. If it used ARC, it would be unusable.
To people who have worked on programming languages, saying "ARC is good, actually" is either a sign that you're about to present a very nuanced argument informed by decades of experience, or, more likely, that you have no idea what you're talking about.
If you're going to make a claim that lies this far outside the mainstream, I'd say the onus to present a good argument is on you, not me.
With age, I've found it good to moderate my fervor in proportion to how much I know about a subject. If people learn that you're someone who talks with confidence only about things you know, you'll have an easier time getting them to listen.
Apple did not add ARC to Objective-C because it was a great design, rather they failed to add a tracing GC in Objective-C 2.0 that wouldn't crash left and right due to the underlying C semantics, thus they settled with the 2nd best option, having the compiler automate Cocoa's retain/release calls (similar to VC++ extensios to deal with COM AddRef/Release).
Then sold the failure as a success, and improvement, in typical Apple's fashion.
Likwise, Swift had to adopt a similar approach if it was to stay compatible with Objective-C's runtime, otherwise it would need a complicated interop layer like .NET has with COM (RCW/CCW).
Not that is matters much outside Apple's ecosystem, though.
It needs, but unfortunately it doesn’t have. That category is won by Java multiple times.
This one stresses the GC quite a bit (so it doesn’t make much sense for non-managed languages that can “cheat”), and there is only Haskell as a managed language that could get ahead of Java (but it is not really apples to oranges due to the different execution model), and in other’s case it is not even close.
Go is particularly bad in this benchmark, though it sort of makes sense as it optimizes for low latency, which is fundamentally at odds with throughput.
Here's a different reading:
Apparently after the whole WinRT/UWP mess, C++ is now left for writing low level stuff, with everything else on WinUI 3.0/WinAPPSDK being done in C#.
Devil May Cry 5 on PlayStation 5 seems to be doing quite alright with Campcon's in-house fork from .NET.
So in the end, other than some corner cases where no kind of automated memory management is at all allowed, people will rather take a regular car instead of a F1.
Now, do you really don't see how a Java/GO style GC and reference counting are different?
It's not like I didn't enumerate some characteristics that make all the difference - and that explain why people do not think of those two cases as just a single thing called "garbage collection", and also why people believe that ARC would have less overhead than a Java/GO style garbage collector.
The language and runtime specification don't say anything about GC algorithms required by compliant implementations.