← Back to context

Comment by fpoling

20 hours ago

For me the main advantage of Go over Rust is compilation speed. Then compared with Go Rust still rely on many C and C++ libraries making it problematic to cross-compile or generate reproducible builds or static binaries.

The minus side of Go is too simplistic GC. When latency spikes hit, there are little options to address them besides painful rewrite.

I've run into GC pauses, I think in many (most?) cases there is some class of bulky data that you can either move into slices of pointer-free structs (so the GC doesn't scan them) or off-heap entirely. The workload where GC is slow is also likely prone to fragmentation so whatever the language you'll have to deal with it.

  • Java with its copying GC deals fine with fragmentation albeit at the cost of more upfront memory. And even in Rust one can change the allocator to try to deal with fragmentation. But with Go there is simply no good options besides the rewrite.

    • Possibly in your specific application, usually there are a handful of options far less painful than a rewrite.

      For the original issue of GC pauses, a narrow change is to move problem data to non-pointer-carrying types, or the bigger hammer of manually managed slices of those types. The second helps with fragmentation too. Some workloads can be split into multiple processes as a direct way to have smaller heaps. If none of those options are enough then off-heap storage lets you do whatever you want.

      I do have some complaints about Go, but one of the big ones has been fixed since I last wrote much Go code and it seems like a fine choice for a lot of applications.

> For me the main advantage of Go over Rust is compilation speed.

Interestingly, Rust has quite good failed compilation speed. That's almost good enough. The usual Rust experience is that it's hard to get things to compile, and then they work the first time.

  • I've never been bothered by long compilation times, it gives me time to think about what the code should actually do.

    To other people's usage patterns though, I imagine the group of people who don't do much with the type system rely more on running a built binary to see if it worked, which means they'll pay the full compile/link time cost more often.

Rust compilation speed is a matter of tooling, they could have something like OCaml or Haskell interpreters, which so far hasn't been a priority.

Or having Cranelift as default backend.

if you are hitting pauses due to GC issues, you should into putting appropriate data structures into a memory arena, here's a reasonable read:

https://uptrace.dev/blog/golang-memory-arena

These are all tools. Java used to have this all the time, and we (ex-java programmer) had ways around this until the JVM improved.

Isn’t it somewhat easy to remove allocations in Go? I haven’t had to “rewrite” as such, but rather lifting some allocation out of loop. Am I misunderstanding the scenario?

  • Pauses are a problem with heap size and structure, not allocation rate, because the pause is caused by GC code that is O(heap size). Making garbage slower reduces the frequency but not severity. This is an issue with most GCs to some degree, there are phases of collection where the GC stops execution and the duration is relative to how much work it has to do which is based on how many objects and how much memory needs to be checked. "Concurrent" garbage collection is the approach of trying to reduce the pauses by doing more of the work while program execution continues. It's complicated and hard to get right, so Go's original GC was IIRC fully stop-the-world.

    There are some fine points to the O(heap size), for example it's clearly unnecessary for the GC to scan objects that do not themselves contain pointers, and work is somewhat proportional to the total number of objects. Combining numerous small objects into manually managed slices, coming up with ways to make the most numerous items pointer-free, etc.

    I learned a bit about this when an analytics workload I had ended up with unacceptable pauses (I think over 1 second), Go's GC is more sophisticated now but I think in any GC runtime you have the same considerations to some degree. Some of the best writing at the time was by Gil Tene, one of the principal authors of the C4 concurrent collector at Azul Systems, starting point here:

    https://groups.google.com/g/golang-dev/c/GvA0DaCI2BU/m/SmEel...

  • With backend serving many clients with widely varying performance profile of individual requests when latency spikes happen there is no particular hot loop. Just many go routines each doing reasonable thing but with a particular request pattern hitting pathological case of GC.

    • Extreme variance in usage patterns will always be challenging. But pre allocating some reasonably sized buffers can go a long way.

> Rust still rely on many C and C++ libraries

Yes but Rust has a lot more availability of libraries to do stuff as a result. Want to do anything ML or scientific? You at least have a route in Rust where you don’t with Go.

  • With Go basic stuff like url parsing or HTTPS support is written in Go and comes with the standard library. With Rust too many necessary things are just wrappers around C and C++ making cross-compilation and reproducible builds much harder to archive.

    As for availability if CGO is ok, then calling C or C++ code from Go is not that hard. Also, there is always an option to just start C++ process if extra data copies are OK.

    • C APIs are much more annoying to wrap in Go than in Rust because of lack of enums (important) and unions (less important).

  • Has there been a lot of progress with ML in Rust? I don't really keep up with it because it seems like every crate ends up getting abandoned and I just gave up caring.