Modern Garbage Collection Article

1,166 views
Skip to first unread message

Tyler Compton

unread,
Dec 19, 2016, 11:56:39 PM12/19/16
to golang-nuts
https://medium.com/@octskyward/modern-garbage-collection-911ef4f8bd8e#.c48w4ifa7

Thoughts? How accurate is this article? If it is, why, historically, is the work being done on the GC so concentrated on pause times?

Dave Cheney

unread,
Dec 20, 2016, 12:31:17 AM12/20/16
to golang-nuts
>  If it is, why, historically, is the work being done on the GC so concentrated on pause times?

Predictable latency is good for servers.

Ian Lance Taylor

unread,
Dec 20, 2016, 12:31:52 AM12/20/16
to Tyler Compton, golang-nuts
On Mon, Dec 19, 2016 at 8:56 PM, Tyler Compton <xav...@gmail.com> wrote:
> https://medium.com/@octskyward/modern-garbage-collection-911ef4f8bd8e#.c48w4ifa7
>
> Thoughts? How accurate is this article? If it is, why, historically, is the
> work being done on the GC so concentrated on pause times?

If you click on the comments link near the bottom you will see that
Rick has commented on part of the essay.

I do not work on the GC myself, but these are my personal observations.

I think the key observation in Go's GC is that modern systems have
multiple cores, and are rapidly getting more cores. Throwing more
cores at the problem is a sensible approach on modern hardware. Since
the cores are no longer getting faster, making your code run faster on
a single core is not the optimal design for the hardware of the near
future.

For a language like Go, stop-the-world pause times really are the most
important thing, because during a pause your server isn't doing
anything useful. The essay suggests that the Go runtime is "willing
to slow down your program by almost any amount" in order to reduce
pause times; that is clearly hyperbole, and it isn't literally true.
The slowdowns in compiled Go code occur when writing pointers into the
heap and when allocating new memory. The compiler works hard to let
you store variables on the stack rather than the heap, where no
slowdown occurs. The language is designed to let you control when and
how memory is allocated, giving you control over when memory is
allocated. The effect is that in Go you can adjust your program to
reduce GC overhead, rather than tuning the GC. This is far more true
in Go than in Java, since Java has no stack variables or structs. I'm
not familiar enough with C# to say anything serious about it.

That said, it should be obvious that nobody thinks that work on Go's
garbage collector has finished.

The essay mentions the request-oriented collector, which is indeed
similar to a generational garbage collector. If everything works, the
request oriented collector can be cheaper than a generational
collector because no copying is required. The essay suggests that it
can be simulated by a generational GC "by ensuring the young
generation is large enough that all garbage generated by handling a
request fits within it" but that is somewhat meaningless if any of
your requests can use a lot of memory, since you have to waste a lot
of space by allocating that much memory for each goroutine.

Once the request-oriented collector is working or abandoned, I believe
the GC team plans to focus on increasing throughput, since latency is
now low enough that it hopefully doesn't matter for non-realtime
programs.

Ian

Tyler Compton

unread,
Dec 20, 2016, 12:52:38 AM12/20/16
to golang-nuts, xav...@gmail.com
Thank you for your input!

> Throwing more cores at the problem is a sensible approach on modern hardware.  Since the cores are no longer getting faster, making your code run faster on a single core is not the optimal design for the hardware of the near future.

I had not considered increasing core counts as something that could drive such important decisions about GC design, but it makes sense. Now that I think of it, it falls right in line with Go's general goal of being useful on newer, multi-core machines.

Konstantin Khomoutov

unread,
Dec 20, 2016, 1:32:56 AM12/20/16
to Tyler Compton, golang-nuts
The article is a typical case of the "sofa theoretist" write-up: take a
couple of phrases out of several Go announces, do no research, read no
source code, be not familiar with any developer working on the Go's GC,
have not to hear their reasoning, lump together whatever you know about
your pet platform (Java), point at decades of academic research. Done.

Thanks for the author's point of view, but I personally would trade all
those decades and tuning put into the JVM's GC for tests done by Go
engeneers in the field. Evidence always trumps any theoretisation. :-)

andrey mirtchovski

unread,
Dec 20, 2016, 1:35:58 AM12/20/16
to Tyler Compton, golang-nuts
My apologies to the original author, but the "state of the art" of
modern garbage collectors is not acceptable if it means this:

"To run dex in process, the Gradle daemon needs a larger heap.
It currently has 1024 MB.
For faster builds, increase the maximum heap size for the Gradle
daemon to at least 1536 MB.
To do this set org.gradle.jvmargs=-Xmx1536M in the project
gradle.properties."

note that this GC information is exposed to me, the end user, through
several layers of abstraction: I'm using a build mechanism to coerce a
Go program to be acceptable as a C native program with Java bindings
in Android's virtual machine.

adon...@google.com

unread,
Dec 20, 2016, 11:18:12 AM12/20/16
to golang-nuts, xav...@gmail.com
On Tuesday, 20 December 2016 00:31:52 UTC-5, Ian Lance Taylor wrote:
[Go] is designed to let you control when and
how memory is allocated, giving you control over when memory is
allocated.  The effect is that in Go you can adjust your program to
reduce GC overhead, rather than tuning the GC.  This is far more true
in Go than in Java, since Java has no stack variables or structs.

Regarding stack variables, Java and Go may be more similar than they first appear. Although Java's virtual machine places only primitive values and object references on the stack, and objects themselves on the heap, in practice, with similar escape analysis algorithms, Java compilers and Go compilers place the same set of objects on the stack. Structs/classes are where the real difference lies: Go lets you avoid unnecessary indirection (and its principal data types use fewer indirections: compare 1 pointer in Go's string to 3 in java.lang.String), and it is much harder for a compiler to do the analogous optimization to eliminate unnecessary references to indirect struct fields.

Alan Donovan

unread,
Dec 20, 2016, 11:29:36 AM12/20/16
to golang-nuts, xav...@gmail.com
On 20 December 2016 at 11:17, <adon...@google.com> wrote:
compare 1 pointer in Go's string to 3 in java.lang.String

Sorry, "2 in java.lang.String".

Jesper Louis Andersen

unread,
Dec 20, 2016, 1:59:21 PM12/20/16
to Tyler Compton, golang-nuts
The key observation of a GC: It must match modern hardware, and it must match the programming language for which you write it for.

Historically, Java allocated all objects on the heap. This adds GC pressure and in order to remove such GC pressure, you are soon reaching for a generational GC model. Even if you undo that mistake later on, you still have the generational GC.

Functional languages are very keen on adding GC pressure as well as they tend to allocate more data than imperative programs, because their data structures are persistent and not ephemeral. Furthermore, they usually have few to no pointers from the old/tenured generation into the young/new generation, which simplifies the need of the GC forward set between generations. This makes them highly amenable to generational collection.

Some languages are not exposing pointers to the programmer at all. They can usually rearrange data on the heap freely and thus have an easier time with compaction. Some implementations even does hash-consing: we hash every object under compaction and if two objects happen to have the same structure, we can just point to the single object (this of course requires immutability of said data).

Even better: suppose you allocate data inside a given "region" and you have a point in the program after which you can prove that nobody needs data in that region anymore. Then you can reclaim all of that region in O(1) time. And you can also just "reset" the region and reuse its contents again and again. In general this is known as "Region Inference" (Tofte, Talpin 1994) and in some cases it performs vastly better. Years later, Makholm, Niss and Henglein showed how you can relax the Tofte/Talpin rule of using the scope of the language as region boundaries to get improved inference. There are some similarities to generational collection as well as the request-region proposal I've heard rumored Hudson/Clements are working on (Typical examples: HTTP requests, game rendering of a frame or a tick in the game logic. I also know ITA used that trick manually in Common Lisp: http://www.paulgraham.com/carl.html , point 9)

Also, hardware is interesting today. At $work, we regularly hit situations where Amdahl's law hits us. There is a bottleneck in the code base, which then limits CPU utilization and we can't get it past 70% even if we try. One core is tied up trying to squeeze data through the bottleneck, and the rest of the system sits patiently waiting until that is done. Optimization can help in some cases, especially through parallelisation, but there are also situations where it is hard to make code parallel, in which case you end up with ample excess CPU time for the GC.

Finally, I think the choice in Go of focusing on low latency over throughput as a first target is spot on. Once achieved you can try to regain the throughput behind the scenes by algorithmic optimization. Which is bound to happen sooner or later.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages