Live data from Hacker News

Go does not need a Java-style GC

erik-engheim.medium.com

51–60 of 226 posts

Re: Go does not need a Java-style GC

#52
post #37

Earlier quoted context omitted.

ZGC is already available, since JDK 15, September 2020. =) https://wiki.openjdk.java.net/display/zgc/Main#Main-ChangeLo...

Max pause times of 0.5ms is what got me really interested. It feels like a huge trade-off of GCs is almost completely gone. https://malloc.se/blog/zgc-jdk16

> It feels like a huge trade-off of GCs is almost completely gone.

FWIW the tradeoff of low latency GC is usually paid in throughput.

That is definitely the case for Go, which can lag very much behind allocations (so if your allocation pattern is bad enough the heap will keep growing despite the live heap being stable, because the GC is unable to clear the dead heap fast enough for the new allocations).

Re: Go does not need a Java-style GC

#53
post #38

Earlier quoted context omitted.

There's still a memory tradeoff, some due to GC, some due to Java (lots of runtime reflection...). Guessing 2-4x.

This is a bit of a tangent, but you can get into situations where Java's memory-overhead becomes pretty untenable. I was in a situation of having to keep track of ~1 billion short strings of a median length of maybe 7 characters. In terms of just data, that should clock in at about 10 Gb; in practice it was closer to 24 Gb. I tried going with just byte[]-instances instead, which didn't help a lot. Using long byte[]-i…

The Lilliput project aims to address this: https://wiki.openjdk.java.net/display/lilliput

Re: Go does not need a Java-style GC

#54
post #38
post #37

Earlier quoted context omitted.

Max pause times of 0.5ms is what got me really interested. It feels like a huge trade-off of GCs is almost completely gone. https://malloc.se/blog/zgc-jdk16

There's still a memory tradeoff, some due to GC, some due to Java (lots of runtime reflection...). Guessing 2-4x.

The GC memory overhead affects all languages with a GC more advanced than refcounting. It certainly does affect Go as well.

Re: Go does not need a Java-style GC

#55
post #13
post #6

> In a multithreaded program, a bump allocator requires locks. That kills their performance advantage. Java uses per-thread pointer bump allocators[1] > While Java does it as well, it doesn’t utilize this info to put objects on the stack. Correct, but it does scalar replacement[2] which puts them in registers instead > Why can Go run its GC concurrently and not Java? Because Go does not fix any pointers or move any o…

ZGC[4] in particular has me excited, enough so to want to pick up a JVM language.

ZGC and Shenandoah can be slower than G1, those are not silver bullets. The fact that there is 4-5 GCs explains the situation, there is not a single GC that is better than the others.

It really depends of the workload.

Re: Go does not need a Java-style GC

#56
post #38

Earlier quoted context omitted.

There's still a memory tradeoff, some due to GC, some due to Java (lots of runtime reflection...). Guessing 2-4x.

This is a bit of a tangent, but you can get into situations where Java's memory-overhead becomes pretty untenable. I was in a situation of having to keep track of ~1 billion short strings of a median length of maybe 7 characters. In terms of just data, that should clock in at about 10 Gb; in practice it was closer to 24 Gb. I tried going with just byte[]-instances instead, which didn't help a lot. Using long byte[]-i…

FWIW. This would probably present a challenge in most (all?) languages.

For example in libc++ due to SSO an std::string has a minimum size of 24 bytes.

For a billion strings less than 15 chars (+ the null byte) that gets you to 24GB, and that’s optimistically assuming each string is allocated in place.

I doubt heap allocated char* would do much better either. Just having a billion 8 byte pointers eats a lot of memory. You’d really need some sort of string packing scheme similar to what you did in Java.

Re: Go does not need a Java-style GC

#57
The binary-trees benchmark on The Debian Language Shootout[1] involves allocating millions of short-lived trees and traversing them. It is informative about GC performance even with the caveat that there are 'lies, damned lies, and benchmarks', because many real-world graph analysis and brute force tree search algorithms similarly allocate zillions of short-lived nodes. For non-GC languages like C/C++/Rust it gives a decent idea of the performance difference between malloc'ing and freeing individual objects vs doing bulk arena allocations:

  language         secs       GC'd language?
  ========         ====       ==============
  C++ (g++)        0.94
  Rust             1.09
  C (gcc)          1.54
  Free Pascal      1.99
  Intel Fortran    2.38
  Java             2.48       yes  
So Java has the fastest GC for this test, 2.48 secs vs 12.23 secs for Golang. The Java code is also notably perfectly idiomatic for multicore, it doesn't do heroic "avoid GC by writing C-like code manipulating a fixed global memory array" tricks. The Java code is also more concise.

The 'plain C' code that uses Apache Portable Runtime memory pools instead of standard malloc/free and uses OpenMP #pragma's strikes me as more 'heroic' than 'idiomatic', whereas C++ and Rust use standard libraries/crates and idiomatic patterns. (Note that OpenMP is 'standard' for high-performance C and well supported across GCC/LLVM/Microsoft/Intel compilers. But still....)

OCaml and Haskell made impressive showings for functional languages which are in practice the easiest for dealing with complicated tree algorithms, which is perhaps why the formally verified C compiler, CompCert, is implemented in OCaml, as is Frama-C for formally verifying ISO C programs, as is the Coq theorem prover, etc.

[1] https://benchmarksgame-team.pages.debian.net/benchmarksgame/... [Edited link]

Re: Go does not need a Java-style GC

#58
post #38

Earlier quoted context omitted.

There's still a memory tradeoff, some due to GC, some due to Java (lots of runtime reflection...). Guessing 2-4x.

This is a bit of a tangent, but you can get into situations where Java's memory-overhead becomes pretty untenable. I was in a situation of having to keep track of ~1 billion short strings of a median length of maybe 7 characters. In terms of just data, that should clock in at about 10 Gb; in practice it was closer to 24 Gb. I tried going with just byte[]-instances instead, which didn't help a lot. Using long byte[]-i…

I would never write something like this in java, but to be fair, a program shouldn't be written like this in the first place. If you "need" a billion strings in memory and you didn't design for that with something that would scale better, you messed up a long time ago.

Re: Go does not need a Java-style GC

#59
As some of the other comments in the thread allude, this is quite a rudimentary (or rather outdated) understanding of how Java GC operates and ends up (unfortunately) turning an otherwise good comparison into a straw-man argument.

As someone who's worked with Java from the days where "If you want superhigh performance from Java without GC pauses, then just turn off GC and restart your process every X hours" was considered a "valid" way to run high-performance Java systems, I think the changes Java has made to GC are among the biggest improvements to the framework/JVM and have contributed vastly to JVM stability and growth over the last decade.

Re: Go does not need a Java-style GC

#60
post #25
post #23

Earlier quoted context omitted.

As far as I know (I’m not too familiar with Go), Go mostly stack allocates based on the developer’s intent, eg. by using structs. Java doesn’t (yet) have an option for value types that can be reliably stack allocated, so it resorts to very complex escape analysis. Calling the former escape analysis is a bit misleading imo, even if technically true.

Go is a little more elaborate than that: if a value is initialized as a pointer-to-struct (e.g. foo := &Foo{...}) and it doesn't escape the function, Go will allocate it as if were a value type.

You can stack-allocate in C, with alloca() or by taking the address of a local, and use it like a pointer. So long as you're extremely sure nothing is going to hang onto the pointer beyond the lifetime of that stack frame, it's fine.

Same thing with Go, except that the compiler makes the decision.

Post reply on HN