4 ms·
> * Rust is a little faster, and avoids GC. I always wondered: how is it not much faster? Dealing with the borrow checker is immense overhead, yet performance
by diarrhea 3y ago
> * Rust is a little faster, and avoids GC.
I always wondered: how is it not much faster? Dealing with the borrow checker is immense overhead, yet performance gains are low compared to Go. Is Go's GC simply that good? Is Rust's compile-time GC about something other than performance somehow?
I realize Go still needs to ship a minimal runtime in its binaries, but don't feel that's a meaningful burden compared to borrow checker fighting.
- amedvednikov 3y agoGC is often faster than ownership. All the extra clones are costly.
- _ahxg 3y agoThat's a fair point, although there is a simple solution in Rust (and other RAII languages) to use heap allocation if what you want is moving big chunks of data (so only the pointer is moved) or in case you really want copies, reference counting (Rc/Arc in Rust).
- thumbuddy 3y agoYou don't have to clone... Really you don't. If you want fast rust code, learning how to avoid that is probably step 1.
- lordnacho 3y agoIt's not just a question of on average how much faster it is, it's a question of whether the distribution of latencies matters.
- antonvs 3y ago> Is Rust's compile-time GC about something other than performance somehow? Predictability and determinism: In some environments, GC pauses are problematic. Modern GCs in Go and Java running on fast enough CPUs can generally keep pause times down below 1 ms, but on a multi-GHz CPU that's multiple million clock cycles - a virtual eternity in some contexts. Memory usage: GCs waste memory in exchange for performance. Go's GC doesn't compact (last I checked), so fragments the heap. Ability to have no runtime: in some environments, the need for a runtime is problematic. That's a big part of why the Linux kernel supports Rust but not Go. All of these issues tend to be more significant in e.g. hard real time contexts, in embedded environments with less powerful processors, etc. Also btw "borrow checker fighting" is not much of an issue for experienced Rust developers. You quickly get used to how to write code to avoid borrow checker issues. And in general, writing code that way tends to be a good thing anyway.
- kaba0 3y ago> Memory usage: GCs waste memory in exchange for performance. Go's GC doesn't compact (last I checked), so fragments the heap Both of your sentences are true, but are not related. GCs indeed make a memory tradeoff for better performance, even if it is a compacting GC. But dynamic allocation even with Rust does fragment the memory and is in fact solved better by a compacting GC.
- fl0ki 3y agoGo's GC isn't compacting, and might never be, because it made the short-sighted decision that Go pointers can be handed directly to asm or C code. It's also stuck on an old fork of tcmalloc which isn't anywhere near the state of the art today and will only fall further behind if they don't at least start on a plan to make allocators interchangeable. Bear in mind that Rust has had interchangeable allocators for years, and Java has had interchangeable GCs for decades, while Go still has neither. Go's GC is pretty good for what it is, especially now that it has become more of a control system that converges on a target collection time ratio. But it is neither a state of the art GC -- not even by the standards of 10 years ago given how far along the JVM was already -- nor is [de]allocation done with a state of the art allocator. People praise that Go promises a 10ms max duration for GC Assist -- the mechanism that makes your actual critical-path work pause and help out the struggling GC -- and while 10ms is fine for a logs pipeline or something, it's extraordinarily bad for many kinds of API endpoints which should have a 99th percentile latency under 1ms (bearing in mind that even local networking is going to add to that considerably so the floor should start low). That's despite Go's GC being hailed for targeting predictable low latency rather than overall throughput... this is the predictable low latency they're talking about. 10ms is a failure in a world of sub-ms targets, and Go as a language gives you very limited options to avoid it, even after abandonding community idioms and popular libraries.
- kaba0 3y agoYes, you are absolutely correct (and while I was familiar with Java’s GCs and that Go’s are way behind, I wasn’t well read on what exactly Go’s do, so thanks for writing that!), I wasn’t defending Go’s GC, only making a nitpick.
- kaba0 3y agoGo’s GC is very naive and not particularly good. In that regard the JVM is far above other platforms. With that said, I find it questionable to say that Go would be faster than Rust - the former does barely any optimizations (hence its fast compile time).
- _ahxg 3y ago> Is Rust's compile-time GC about something other than performance somehow? AFAIK, memory safety and language features as RAII is also available in C++, for instance. About the reasons for slow compilation, take a look at https://www.reddit.com/r/rust/comments/xna9mb/why_are_rust_programs_slow_to_compile/ https://www.reddit.com/r/rust/comments/xna9mb/why_are_rust_p... Not having a GC is also about not having a runtime as you mention (e.g. nice for creating Python extensions and embedded systems programming) and also more runtime deterministic performance: on that, if I'm not mistaken that was the reason for Discourse switching to Rust and also, e.g.: "the choice of Rust as the main execution language avoids the overhead of GC pauses and results in deterministic processing times" https://github.com/apache/arrow-ballista/blob/main/README.md https://github.com/apache/arrow-ballista/blob/main/README.md
- lelanthran 3y agoGCs in general can batch memory allocations, reallocations and frees. With idiomatic non GC programs, an object that is freed is deallocated at that point of execution. Think of a loop running 15 times that calls a function which allocates objects, uses them and then returns. In a GC language, there is only a single free. In a non GC language, each return of that function will cause a free to occur. There's some other interesting optimisations that can only be done in a GC language. The only way to get those optimisations in a non-gc language is to effectively implement your own allocation and deallocation using pools.
- diarrhea 3y agoThat is a great insight, thanks. I have always been under the impression that non-GC performance is strictly better under all circumstances, but your example is an obvious counterpoint.
- SkiFire13 3y agoIt depends on the workload. If you have some tight loop that's heavy on computations and memory accesses then the borrow checker will allow you get some huge wins by removing allocations and improving memory locality. If instesd you're writing some IO heavy application then the overhead of allocations will be minimal compared to the IO you're doing, so the borrow checker wins will be really minimal, or Rust might even be slower due to the runtime not being built into the language.
- fl0ki 3y agoIt is much faster for CPU-bound work. In my experience, 2x-5x is the most common range I get. There are many reasons besides just memory allocation and LLVM that Rust code ends up faster. Let me just focus on one very commonly used data structure: hash maps. Rust hash maps: * Use SwissTable because they didn't make a short-sighted promise about delete-while-iterating behavior that makes Go incompatible with SwissTable's tombstone design and makes Go never shrink maps once created. * Let you customize the hash function to whatever performs best for your key type & target hardware. * Let you find & insert/update in one hash table lookup, where Go always requires at least two, and its compiler doesn't optimize even the most common idioms. * Let you reference a key/value in-place instead of having to copy it out each time. Go won't allow this because the reference may become stale, but Rust can enforce this with lifetimes. * Let you clone a map without rehashing every key to a new seed. I generally measure at least 15x speedup from this alone, unlocking very useful design patterns like "clone a map and apply a few temporary updates for a one-off operation like validation or simulation" with no extra code complexity. Go gives you no better option than slowly rehashing the entire map. And that's just hash maps. How about Go's regex engine being one of the slowest in the world while Rust's regex crate being one of the fastest: https://github.com/mariomka/regex-benchmark#optimized https://github.com/mariomka/regex-benchmark#optimized https://github.com/BurntSushi/rebar#summary-of-search-time-benchmarks https://github.com/BurntSushi/rebar#summary-of-search-time-b... Further, Go refusing to have macros means that many libraries use reflection instead, which often makes those parts of the Go program perform no better than Python and in some cases worse. Rust can just generate all of that at compile time with macros, and optimize them with LLVM like any other code. Some Go libraries go to enormous lengths to reduce reflection overhead, but that's hard to justify for most things, and hard to maintain even once done. The legendary https://github.com/segmentio/encoding https://github.com/segmentio/encoding seems to be abandoned now and progress on Go JSON in general seems to have died with https://github.com/go-json-experiment/json https://github.com/go-json-experiment/json . Remember that next time someone parrots the myth that Go is ideal for HTTP JSON APIs. Many people claiming their projects are IO-bound are just assuming that's the case because most of the time is spent in their input reader. If they actually measured they'd see it's not even saturating a 100Mbps link, let alone 1-100Gbps, so by definition it is not IO-bound. Even if they didn't need more throughput than that, they still could have put those cycles to better use or at worst saved energy. Isn't that what people like to say about Go vs Python, that Go saves energy? Sure, but it still burns a lot more energy than it would if it had macros. Maybe someone's recipe website doesn't care but world-scale datacenter applications do. Rust can use state-of-the-art memory allocators like mimalloc, while Go is still stuck on an old fork of tcmalloc, and not just tcmalloc in its original C, but transpiled to Go so it optimizes much less than LLVM would optimize it. (Many people benchmarking them forget to even try substitute allocators in Rust, so they're actually underestimating just how much faster Rust is) Oh yeah, aside, tcmalloc was designed for C++ code with lots of temporary allocations that are allocated & freed by the same thread so they're very cheap when cached per thread/core. Go almost always frees things on a separate GC thread, completely undermining this design goal. So tcmalloc isn't even a good fit for Go, yet there's no other choice available. People don't even discuss it any more because what is there to discuss, Go and all of its users are just stuck with it. Finally, even Go Generics have failed to improve performance, and in many cases can make it unimaginably worse through -- I kid you not -- global lock contention hidden behind innocent type assertion syntax: https://planetscale.com/blog/generics-can-make-your-go-code-slower https://planetscale.com/blog/generics-can-make-your-go-code-... It's not even close. There are many reasons Go is a lot slower than Rust and many of them are likely to remain forever. Most of them have not seen meaningful progress in a decade or more, and I'm not holding my breath for the next decade either.