Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

GC is complicated because what works well for one thing might not work well for other things. GC can be both about clever things and about hard trade-offs. That said, I'll try to talk about GC without having a religious war erupt. Keep in mind, something here might be wrong.

Go's GC is tuned for low latency. People hate pause times. They hate it even more than they hate slowness. Go makes a trade-off for short pause times, but in doing so they do sacrifice throughput. That's great for a web server. With a web server, you care a lot about pauses offering a bad experience. A 500ms pause is going to give a customer a bad experience. That's less good for batch processing. Let's say you're running a backfill job that you expect to take 10-15 hours. You don't really care if it pauses for 1 minute to GC every 30 minutes. No one will even know. However, you will know whether it took 10 hours or 15 hours.

Go's GC is meant to be simple for users. I think the only option is "how much memory should I be targeting to use?" That makes it dead simple for users. I think the Go authors are right that too many tuning knobs can be a bad thing. People can spend months doing nonsense work. Worst is that knobs are hard to verify that anything is really different. If you're running a web server, was the traffic really the same? What about other things happening on the machine? Wouldn't you rather be writing software instead?

Go's GC is a non-copying GC. One thing this means is that Go's (heap) memory allocation ends up being very different because Go's memory is going to become fragmented. So Go needs to keep a map of free memory and allocations are a bit more expensive. Java (with GCs like G1) can just bump allocate which is insanely cheap. This is because Java is allocating everything contiguously so it just needs to move a pointer. How does that work once something becomes freed? Java's G1 (the default in the latest LTS Java) will copy everything that's still alive to a new portion of memory and the old portion is then just empty. You kind of see this in Go's culture. Web frameworks obsess about not making heap allocations. Libraries often have you pass in pointers to be filled in rather than returning something.

Go misses out on the generational hypothesis. The generational hypothesis is one of the more durable observations we have about programming - that most things that are allocated die really quickly. C# and Java both use generational collectors by default and they've done way better than what came before. C# and Java don't have as-low pause times as Go, but part of that is that they're targeting other things like throughput or heap overhead more.

Go doesn't need GC as much. Go can allocate more on the stack than Java can and, well, Go programmers are sometimes a bit obsessed with stack allocations even when it makes for more complicated code. Having structs means creating something where you can just have contiguous memory rather than allocating separate things for the fields in your object. Go's authors have observed that a lot of their objects that die-young are stack allocated and so while the generational hypothesis holds, it's a bit different. Go has put a good amount of effort into escape analysis to get more stuff stack allocated.

Java has two new algorithms ZGC and Shenandoah which are available in the latest Java. They're pretty impressive and usually get down to sub-millisecond pause times and even 99th percentile pauses of 1-2ms.

Go's new GC was constrained by the fact that "Go also desperately needed short term success in 2015" (Rick Hudson from Google's Go team) and the fact that they wanted their foreign function interface to be simple - if you don't move objects in memory, you don't have to worry about dealing with the indirection you'd need between C/C++ expecting them to be in one place and Go moving them around. Google's going to have a lot of code they want to use without replacing it with Go code and so C/C++ interop is going to be huge in terms of their internal goals (and in terms of what the team targeted regardless of whether it's useful to you). And I think once they had shown off sub-millisecond pause times, they were really hesitant to do something that might introduce things like 5ms pause times. I think they might have also said that Google had an obsession with the long-tail at the time. Especially at Google, there's going to be a very long tail and if that's what people are all talking about and caring about, you end up wanting to target that.

Go has tried other algorithms. They had a request-oriented-collector and that worked well, but it slowed down certain applications that Go programmers care about - namely, the compiler. They tried a non-copying generational GC, but didn't have a lot of success there.

Ultimately, Go wanted fast success and to solve the #1 complaint people had: extreme pause times (pause times that would make JVM developers feel sorry for Go programmers). Going with a copying GC might have offered better performance, but would have meant a lot more work. And Go gets away with some things because more stuff gets stack allocated and Go programmers try to avoid heap allocations which would be more expensive given Go's choices (programming for the GC algorithm).

I don't think that JVM languages have a programming style that lends themselves to an over-reliance on churning through short-term garbage. Well, Clojure probably since I think it does go for the functional/immutable allocate-a-lot style. Maybe Kotlin and Scala if you're creating lots of immutable stuff just to re-allocate/copy when you want to change one field. That doesn't really apply to most Java programs. And I have covered the way that Go potentially leads to more stack allocations. However, I don't think most people know how their Go programs work any more than their JVM programs and this really just seems to be a "I want to dislike Java" kind of thing rather than something about memory.

Java programs tend to start slow because of the JVM and JIT compilation. Java has been focused on throughput more than Go has (at the expense of responsiveness). That is changing with the two latest Java GC algorithms (and even G1 which is really good). Java is also working on modularizing itself so that you won't bring along as much of what you don't need (Jigsaw) and AOT compilation. Saying that Java is slow just isn't really true, but it might feel true - things like startup times and pause times can inform our opinions a lot. There's absolutely no question that Java is a lot faster than Python, but Python can feel faster for simple programs that aren't doing a lot (or are just small amounts of Python doing most of the heavy work in C).

I mean, are you including Android in "all Java apps"?

Java, C#, and Go are all really wonderful languages/platforms (including things hosted on them like Kotlin). They're all around the same performance, but they do have some differences. I think Go should re-visit their GC decisions in the future, especially as ZGC and Shenandoah take shape, but their GC works pretty well. But there are certainly trade-offs being made (and it isn't around language features that make the platform productive for programmers). I think GC is very interesting, but ultimately Java, C#, and Go all have very good GC that offers a good experience.



Nice writeup!

If a programmer really cares for performant batch-processing (not sure it's a direction we're going for anymore but), you'll just reuse your objects and dataspace. Understanding Go in order to minimize GC is pretty trivial and the standard library includes packages and tooling exactly for such purposes. Most likely, there are only a few hotspots needing to be tended to like this.

So this sounds like Golang is optimized for what people love about computing: fast response time. Also providing programmers with basic performance "out of the box", which is another good tradeoff for me.

The tradeoff works best when you make simple designs, not huge behemoths. Spending more time reiterating clever designs, rather than jamming the keyboard until done.


Java is slower than Python for simple short-running programs. When Python is finished Java is still struggling through start-up and class-loading.


Is this actually true? Have you measured this, or seen measurements?


Here are measurements for an extreme case of a short-running simple program:

  $ cat Hello.java 
  class Hello { public static void main(String[] args) { System.out.println("Hello from Java!"); } }

  $ cat hello.py 
  print("Hello from Python!")

  $ time /usr/lib/jvm/java-13-openjdk/bin/java -Xshare:on -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Hello
  Hello from Java!

  real 0m0.102s
  user 0m0.095s
  sys 0m0.025s

  $ time python3 -S hello.py 
  Hello from Python!

  real 0m0.034s
  user 0m0.020s
  sys 0m0.013s
It's a bit faster if you create a custom modular JRE with jlink:

  $ /usr/lib/jvm/java-13-openjdk/bin/jlink --add-modules java.base --output /tmp/jlinked-java13-jre
  $ /tmp/jlinked-java13-jre/bin/java -Xshare:dump
  $ time /tmp/jlinked-java13-jre/bin/java -Xshare:on -XX:+TieredCompilation -XX:TieredStopAtLevel=1 Hello
  Hello from Java!

  real 0m0.087s
  user 0m0.050s
  sys 0m0.035s


Yes, I often write small command-line tools and that's what I found. Profiling seemed to indicate that doing anything at all in Java, such as reading a config file, is fast the second time but super slow the first time.

You can test my assertion simply by writing a "hello world" in Python and Java.


Thanks for the in-depth write-up @mdasen. I certainly learnt a lot about general GC properties and trade-offs.

takes hat off in appreciation


Great reply. Thanks for taking the time to write it.


Nicely written




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: