4 ms·
This is a rather strong claim not supported by real-world data on how Go’s GC behaves under load. Which, in fact, isn’t the first time for your account as searc
by neonsunset 2y ago
This is a rather strong claim not supported by real-world data on how Go’s GC behaves under load. Which, in fact, isn’t the first time for your account as search across the discussion history indicates so you do you.
Sustaining very small allocations with median period of say 66ns (and I’m going to be charitable here) is not that difficult with single-threaded workload. Things get interesting as it becomes multithreaded and object graph becomes more complex.
As I said before, on any microbenchmark that is meant to evaluate allocation+collection throughput Go performs worse than the competition and there is no workaround to this because that’s just how its GC is designed: https://benchmarksgame-team.pages.debian.net/benchmarksgame/performance/binarytrees.html https://benchmarksgame-team.pages.debian.net/benchmarksgame/... And this is without even involving any sort of cross-thread data sharing between the allocations.
It’s okay to praise things you like and criticize those you don’t, but it can be kept within the realm of actual pros and cons of a particular technology.
- tinco 2y agoI think you're missing the parents' point. I think you and the parent agree would that for these general purpose benchmarks a more advanced garbage collector would win out over Go's garbage collector. But the point that has to be made is that those simply don't apply to the sort of application Go is built for. If your Go application is not a network service, and frankly, is over 10.000 lines of code, you're not using Go for its intended purpose. An application with a complex object graph seems exactly like the sort of application that shouldn't be written in Go. As an aside, it's a bit weird that you'd react to their citing of real-world data with a dismissal and a sort of ad hominem, and then proceed to cite a microbenchmark of precisely the sort of thing people shouldn't be building in Go. Do you have a microbenchmark that disproves that the Go GC can get 130 microsecond collect times while under tens of millions allocations per second load?
- neonsunset 2y ago> If your Go application is not a network service, and frankly, is over 10.000 lines of code, you're not using Go for its intended purpose. An application with a complex object graph seems exactly like the sort of application that shouldn't be written in Go. It seems the industry is moving to adopt Go for this exact use case, unfortunately. Not sure if I read the parent's argument the same way however, as what I'm objecting to is the continuous assignment of capabilities to language implementations that they don't have. On 130us - pause time is frequently quoted in Go discourse but it is somewhat misleading as this omits the "front-loaded" nature of its design. If you spend >5x CPU time executing GC code than more, err, pause-focused designs and have lower throughput, then those 130us no longer exist in a vacuum. It is more accurate to specify that Go favors the sweet spot of "smaller" amount of work performed by a single node and focuses on maintaining consistent latency for light to moderate allocation rates with low heap size. In a way, .NET's GC is evolving in a similar direction but attempting to maintain the throughput or ensure the loss is within single digit %.
- lossolo 2y ago> It is more accurate to specify that Go favors the sweet spot of "smaller" amount of work ... focuses on maintaining consistent latency ... It seems like you wrote the same thing that I wrote, but you used different words for it. What you described is basically I/O-bound workloads with a particular emphasis on latency—in other words, the category of networked services, which is precisely what Go was created for and why we use it. > In a way, .NET's GC is evolving in a similar direction but attempting to maintain the throughput or ensure the loss is within single digit %. Let me know when it finally evolves! I would like to try it out for some of our smaller services then. When we started building our core services in Go many years ago, .NET Core wasn't as good in terms of network throughput, latency, and handling multiple I/O-bound tasks simultaneously. I was actually evaluating it back then too. PS. From one of our production nodes from the last 24h: GC Pause time: Min: 85.1 µs Max: 343 µs Avg: 150 µs https://imgur.com/0HjUyM7 https://imgur.com/0HjUyM7
- neonsunset 2y agoMainly responded in a sibling comment. I think to get the closest representation of the direction the GC is moving towards you could try `dotnet new webapiaot` template with .NET 9 preview SDK: https://dotnet.microsoft.com/en-us/download/dotnet/9.0 https://dotnet.microsoft.com/en-us/download/dotnet/9.0 Can't say I'm a huge fan of it but it gets the job done. In general, ASP.NET Core and Kestrel are great, particularly when you look at just how much more CPU and RAM (and boilerplate) Spring and Node.js require to deliver the same amount of functionality (validation, rate limiting, caching, middlewares, etc.), but because the pipeline is quite a bit UTF-16 centric and feature-focused, it mostly manages to provide competitive performance through sheer performance of the foundation it is built on. .NET could use a custom implementation, much like what Garnet does under the hood, to fully take advantage of compiler and memory management improvements and reclaim top positions in techempower :) You may notice that GC is more size-efficient on Linux and Windows than on macOS. There is an internal feature regarding its heap management (regions) that is not enabled for the latter due to some tooling integration issues. It will eventually get fixed but due to minor impact (people aren't using macs for hosting) of it, it hasn't been a priority. Another project which I think is a good showcase even if unrelated to the domain you work with: https://github.com/codr7/sharpl https://github.com/codr7/sharpl - can be compiled with dotnet publish -o . The sort of thing people often build in Go. Because it is completely custom it is really good at demonstrating just how much native AOT compilation tooling has improved and how low base application footprint gets wrt GC and runtime. Might take a bit to compile for the first time - it needs to pull ILC (IL AOT Compiler) from nuget.org. Not going to be as fast as Go build times (.NET 9 improves it) but you often don't need that - for quick iteration during dev there is dotnet watch for hot-reload instead.
- lossolo 2y ago> This is a rather strong claim not supported by real-world data on how Go’s GC behaves under load The only thing you linked is an old binary trees benchmark from the benchmarks game, which is not equivalent to the workload in a typical networked service. For that kind of workload, I would not pick Go. > It’s okay to praise things you like and criticize those you don’t, but it can be kept within the realm of actual pros and cons of a particular technology. Result of basic allocation micro-benchmark for 64 byte slices, one million per op, 4 cores: BenchmarkAllocation/procs-4-10 115 10469109 ns/op 64000517 B/op 1000007 allocs/op One operation makes 1000007 allocations. Each operation takes 10469109 nanoseconds (0.010469109 seconds). Per second 1 / 0.010469109 ≈ 95.52 operations. So, allocations per second = 95.52 * 1000007 ≈ 95.5 million You can keep your ad-hominem comments to yourself.
- neonsunset 2y agobinary-trees benchmark is using Go 1.23.0 which is the latest version. Go submissions do not do anything differently compared to other GC-based languages for the most part. The fastest Java submission has interestingly much lower execution time than other Java submissions, and I plan to look into it, but that would be pretty much the only caveat. I think it is relevant given the topic of the discussion is GC implementations and their characteristics. I have constructed the following micro-benchmark to replicate your data: https://gist.github.com/neon-sunset/c6c35230e75c89a8f6592cacbbd636ee https://gist.github.com/neon-sunset/c6c35230e75c89a8f6592cac... It tests - Time to allocate 1M 32B/64B/128B arrays/slices and pass each to an empty outlined method in a loop (to ensure heap allocation) - Same as above but 1M allocations are split between NumCPU Goroutines/Tasks. The environment is macOS 15, M1 Pro (8C), Go 1.23.0 and .NET 8. The Go results are 16.2ms, 18.2ms and 104.7ms for single-threaded execution and 11.1ms, 22.6ms and 48.3ms for multi-threaded. The C# results are 4.7ms, 5.9ms and 8.2ms for single-threaded and 1.8ms, 2.6ms and 3.8ms for multi-threaded. When selecting Workstation/Desktop GC instead of Server (Server is default for networked container workloads via asp.net core), the numbers don't exhibit multi-threaded scaling and are 4.7ms, 5.3ms and 7ms for single-threaded and 5.9ms, 9.4ms and 16.8ms for multi-threaded. This GC mode is used by default for console and light GUI application templates. Complex applications override it to Server GC. It is likely that it will be eventually deprecated because its target scenarios are now better addressed by the new mode of Server GC called DATAS, which is enabled by default in the upcoming .NET 9. If you're interested, feel free to replicate the data on your hardware, add more languages, etc. My assumption that OpenJDK will perform even better since Java code is more allocation-heavy. Regarding highly-loaded networked services with predictable or hand-tuned data lifetimes, in C# you are expected to completely bypass the allocations with the use of built-in ArrayPool<T>, stack-allocated buffers or NativeMemory.Alloc/Free which directly call into malloc/free. This is made possible by wrapping such buffers into Span<T> or Memory<T>. Things like Kestrel (web server) or Garnet (Redis server) do just that. I know Go has Arena API but am not sure regarding its current status. Are there newer/better alternatives?