41 ms·
Go does not need a Java-style GC
- jayd16 5y agoDid Go ever fix that problem where short term high memory usage can cause large empty heaps because of the lack of compaction? I remember a rash of articles about this phenomena a while back. This article seems to think its not an issue so I'm wondering when it was fixed.
- jatone 5y agothat was never an issue? maybe your thinking of the slowness golang had in returning memory to the OS? that was resolved.
- Jweb_Guru 5y agoNo, it was never fixed and probably never will be, because it would make Go perform worse in the latency-sensitive microbenchmarks it wants to do well in.
- azth 5y ago> Modern languages such as Go, golang is modern? it's stuck in the 70's
- azth 5y ago> In C#, for example, reference objects and value objects live entirely separate lives. Value objects are always passed by value. *You cannot take the the address of a value object and pass that around.* The last sentence is wrong. You can pass structs by ref in C# (ref, in, out parameters). Java will also allow taking references to value types and passing them.
- SureshG 5y agojfyi: here is the latency GC numbers (JDK17) - https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html . The experience is vastly different compared to an 8 year old jdk version (jdk 8)
- hopsas 5y agoDo not add articles behind pay wall.
- seunosewa 5y agoI'm cautious about believing this sort of claim because I remember reading about why Go doesn't need generics, yet here we are waiting for Go Generics to be ready. However, the article convincingly explains who Java has a greater need for a compacting GC - it creates more garbage. This doesn't necessarily mean Go won't benefit from having a generational, compacting GC at some point, for some applications.
- runevault 5y agoEven if it doesn't need specifically a compacting QC having a swappable more tunable one might still be a big deal for people who need something other than the current GC. Like a simple example is the Discord article about how their use case could not work with Go because it always ran the GC every two minutes. If they could optionally disable that feature they theoretically could have kept using Go instead of rewriting to Rust.
- pkaye 5y agoYou can disable the GC in go. https://pkg.go.dev/runtime/debug#SetGCPercent https://pkg.go.dev/runtime/debug#SetGCPercent
- kaba0 5y agoAlso in java, with the Epsilon “GC”. But it is not really meant for programs that have to run for weeks.
- Andys 5y agoThe go gc was greatly improved in the versions subsequent to the ones used by Discord. The timing there was unfortunate.
- geodel 5y agoBesides sometimes engineers just want to use Rust.
- bitwize 5y agoWhat it needs is Rust style static lifetime management.
- Yoric 5y agoAs a Rust developer, contributor and generally a big fan of Rust, I'd tend to disagree. Having a garbage-collector is a lifesaver for many algorithms. Static lifetime management is great but isn't something you want to force developers to use when they're more interested in coming up with solutions quickly. Also, I really don't see how it would work in Go. You require a pretty strong type system to make static lifetime management work. Unless something has changed radically in the recent past, that's not something that the Go community had much interest in (yes, I know that generics are one step in that direction, but I'm not aware of further steps being discussed).
- IceWreck 5y ago> You require a pretty strong type system to make static lifetime management work Go is strongly and statically typed.
- Yoric 5y agoIt's a spectrum. Rust is further than Go in the direction of "strong type system", and somewhat lateral wrt Haskell, F# or OCaml, for instance (each is more strongly typed in different directions). Idris or Coq are further than Rust in most directions, etc.
- lucian1900 5y agoFor some value of "strongly". It has nil and lacks sum types, so it is comparatively much weaker than even Rust/Swift.
- masklinn 5y agoGo's type system is much, much weaker than what is needed for safe static lifetime management be even remotely close to workable. Static typing is not an on-off, there are statically typed languages with extremely weak type systems (e.g. C), languages with somewhat weak type systems (e.g. Java, Go), languages with strong type systems (e.g. OCaml, Haskell) and languages with extremely strong type systems (e.g. ATS, Idris). And that's a simplification because it's not linear either.
- natanbc 5y ago> 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 objects in memory. Most java GCs are concurrent[3], if you want super low pauses you can get those too[4][5]. Pointers can get fixed while the application is running with GC barriers [1]: https://shipilev.net/jvm/anatomy-quarks/4-tlab-allocation/ https://shipilev.net/jvm/anatomy-quarks/4-tlab-allocation/ [2]: https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacement/ https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacemen... [3]: https://shipilev.net/jvm/anatomy-quarks/3-gc-design-and-pauses/ https://shipilev.net/jvm/anatomy-quarks/3-gc-design-and-paus... [4]: https://wiki.openjdk.java.net/display/zgc/Main https://wiki.openjdk.java.net/display/zgc/Main [5]: https://wiki.openjdk.java.net/display/shenandoah https://wiki.openjdk.java.net/display/shenandoah
- majou 5y agoZGC[4] in particular has me excited, enough so to want to pick up a JVM language.
- the-alchemist 5y agoZGC is already available, since JDK 15, September 2020. =) https://wiki.openjdk.java.net/display/zgc/Main#Main-ChangeLog https://wiki.openjdk.java.net/display/zgc/Main#Main-ChangeLo...
- Yoric 5y agoI'm a bit skeptical. Yes, Go has value types and pointers. But whether you need a modern GC will undoubtedly depend a lot on the type of algorithms you need to execute. Also, it's great that you can implement (some form of) allocators, and that will definitely help for many algorithms, but that's definitely a case of tradeoff between convenience and readability. Similarly, unless I'm mistaken, TCMalloc "solves" fragmentation in two cases: either allocations are small (very common) or memory allocation maps neatly to threads (much less common). That's two good cases to have, but I wouldn't count on it solving memory allocation on its own for, say, a browser engine or a videogame. Oh, and hasn't Java's GC been fully concurrent for a while now? That being said, revisiting the assumptions made by Java (and other languages) is a very good idea.
- avita1 5y agoCool article, I'm not sure I agree with the headline. I used to write low-scale Java apps, and now I write memory intensive Go apps. I've often wondered what would happen if Go did have a JVM style GC. It's relatively common in Go to resort to idioms that let you avoid hitting the GC. Some things that come to mind: * all the tricks you can do with a slice that have two slice headers pointing to the same block of memory [1] * object pooling, something so common in Go it's part of the standard library [2] Both are technically possible in Java, but I've never seen them used commonly (though in fairness I've never written performance critical Java.) If Go had a more sophisticated GC, would these techniques be necessary? Also Java is supposed to be getting value types soon (tm) [3] [1] https://ueokande.github.io/go-slice-tricks/ https://ueokande.github.io/go-slice-tricks/ [2] https://pkg.go.dev/sync#Pool https://pkg.go.dev/sync#Pool [3] https://openjdk.java.net/jeps/169 https://openjdk.java.net/jeps/169
- munificent 5y ago> Both are technically possible in Java, but I've never seen them used commonly (though in fairness I've never written performance critical Java.) I don't know about the Java world, but in C#—especially in games written in Unity—object pooling is very common.
- TideAd 5y agoWriting High Performance .NET Code (https://www.writinghighperf.net/ https://www.writinghighperf.net/) has a chapter on this. In C#, time spent collecting depends on the number of still-living objects. That means you want objects you allocate to be short-lived (dead by the time GC happens) or to live forever (they go to the gen 2 heap and stay there). The book suggests object pooling when the lifetime of objects is between those two extremes, or when objects are big enough for the Large Object Heap. But at the end of the section, the book says: I do not usually run to pooling as a default solution. As a general-purpose mechanism, it is clunky and error-prone. However, you may find that your application will benefit from pooling of just a few types. What kind of things do you pool in Unity?
- 5y ago
- jeffbee 5y agoThere are very few falsifiable statements in this article but the extant ones are all demonstrably false, starting with this whopper: "This typically causes Java programs to have complete freezes of several hundred milliseconds where objects get moved around" Yeah, i mean, definitely not. The only language I regularly work with that has this property is, surprise, Go. The proof is in the tasting, as they say. Go look at the gRPC daily benchmarks if you want hard data. Java beats Go latency at the median and at the tail while having substantially better specific throughput. In other words the Java GC has no tradeoff versus the Go GC; it is better in every way.
- Thaxll 5y agoThe Java gRPC SDK is heavily optimized by Google, much more than the Go one. Until recently Java pauses were a real issue, I worked with a lot of Java based solutions ( elastic search, hadoop etc ... ) and was never impressed by the GC.
- jeffbee 5y agoMaybe, but it's still not possible that the Java test has a median latency of 150µs, a tail latency of 350µs, and a regular need to stop the world for hundreds of milliseconds. That statement is simply not compatible with reality.
- Thaxll 5y agoAs I said the Java SDK is heavily optimized to do the least minimum of allocations. I mean if you don't allocate much the GC is not really an issue.
- jeffbee 5y agoOK but again, that is a statement totally contrary to the article. The article is claiming two things: that it is easier in Go to avoid the heap while Java is utterly dependent on allocating everything on the heap - which is not consistent with the experience of actual Java and Go programmers - and that Java has a "preference for high throughput and high latency" which it doesn't.
- Ericson2314 5y agoThis is mostly stupid. Being able to have many GCs is a good thing. The big reason for "value types" is controlling spacial locality in memory, not GCs being bad per-se. Also, they undersell the java/c# situation. C# has "ref, out, or in", but even without those, you can always make a reference wrapper that has the value type as a field. So "reference types suck because coppying" is nonsense garbage.
- throwaway894345 5y agoI mean, I’ve dabbled in all of these languages, and I much prefer Go’s value types to ref, out, in, and a half dozen GCs. It’s nice that these VM languages have a distinct thing for every eventuality, but I much prefer a single thing that works 99% of the time.
- Ericson2314 5y agoI usually am bashing Go :D, but I am not this time. I am saying this blog author is confused on what the runtimes are capable of. I don't mean to say ref out and in are good language ergonomics or whatever, but simply that they show the compilation target is capable of expressing thing these things. The only thing I know of that the go runtime can do that these others can't is "interior pointers", i.e. pointers to a field of a larger object. You can always just copy the field into a new box, but that breaks mutation semantics. Java gets away with this precisely because most fields are themselves boxed...bu that's exactly the no-control-over-locality problem we're trying to solve.
- joelfolksy 5y agoWhy do you much prefer func changeName(p *Person) { p.firstName = "Bob" } to void changeName(ref Person p) { p.firstName = "Bob"; } ?
- yakubin 5y ago> The big reason for "value types" is controlling spacial locality in memory, not GCs being bad per-se. Spacial locality is one reason. Another very important reason is not generating unnecessary garbage. If you have a lot of microallocations, then your program is going to be slow regardless of you using manual memory management, RAII or a tracing GC. There is just a lot more stuff to do. With manual memory management, you're just going to call free() a lot (using big forward linked lists and freeing them with a function iterating over them, calling free() on each node? yikes). With RAII you're going to spend a lot of time in destructors due to an avalanche of objects freeing the objects they own (structs holding unique_ptr/shared_ptr to structs holding unique_ptr/shared_ptr to...? std::vector<std::string>/std::vector<std::unique_ptr>? these are all antipatterns when it comes to performance). With GC, you're going to have long GC pauses, because the GC needs to traverse a bigger graph of pointers (deep/broad trees of heap allocated objects? arrays of boxed objects? yikes). Another reason is eliminating unnecessary pointer indirection, which helps make sure that instead of loading data with two MOVs from memory polluting the cache, you need only one. For my current project, I use arenas to opt-out of RAII and have just a couple points in my project where memory is freed, instantly in one go. Needless to say, I don't use arrays of boxed elements. It's working great for me. Not only is it faster, but also simpler to understand and I don't need to fight with the borrow-checker as much as in normal (RAII) Rust, because my objects are grouped into longer lifetimes, so I don't need to think about separate lifetimes for every distinct little object. That said, I'm spending some time gathering ideas for a toy language I'd design for myself to write an OS from the kernel up to userspace in, and I'm leaning towards a GC-based design that relies a lot on value types. One way or another, allocating a lot of little objects is stupid, because in every scenario it makes your program slower and, code quality-wise, at best it doesn't make a difference, but a lot of times it makes your code harder to reason about, because now a lot of things are remote to the place in code you're thinking about at any given time.
- hashmash 5y agoThe author doesn't really understand how Java escape analysis works, and just focuses on one key aspect: "It does not replace a heap allocation with a stack allocation for objects that do not globally escape." The author then implies that escape analysis is only used to reduce lock acquisition. Java escape analysis will replace a heap allocation with a stack allocation if the code is fully inlined. This is known as scalar replacement.
- aktau 5y agoI don't know much about this, but upthread various people say that scalar replacement happens very rarely if at all, currently. E.g.: https://news.ycombinator.com/item?id=29324132 https://news.ycombinator.com/item?id=29324132. Could you perhaps comment on that, since you seem to have experience?
- native_samples 5y agoThe issue is not that it doesn't happen - it does, all the time. The problem is it's unpredictable, hard to control and hard to measure. So it's sort of magic that gets blurred into all the other optimizations the VM is doing, and refactoring your code can make the difference between it happening or not.
- pjmlp 5y agoFirst of all that blog post is wrong, it uses Optional which isn't a value type to start with, and contains internal fields. It is considered a value like class, that will eventually be turned into a value type when Valhala arrives, until then there are no guarantees in scalar replacement. Secondly, GraalVM or Azul are much better at it, which the author didn't bother to try their blog post on.
- dragontamer 5y ago> In a multithreaded program, a bump allocator requires locks. That kills their performance advantage. Wait, what? What's wrong with: char theHeap[0x1000000]; atomic_ulong bumpPtr; void* bump_malloc(int size){ uint32_t returnCandidate = bumpPtr.fetch_add(size, std::memory_order_relaxed); if(returnCandidate + size >= HEAP_SIZE){ // Garbage collect. Super complicated, lets ignore it lol. // Once garbage collect is done, try to malloc again. If fail then panic(). } return &theHeap[returnCandidate]; } ------ You don't even need acq_release consistency here, as far as I can tell. Even purely relaxed memory ordering seems to work, which means you definitely don't need locks or even memory barriers. The exception is maybe the garbage-collect routine. Locking for garbage collect is probably reasonable (other programmers accept that garbage-collection is heavy and may incur a large running + synchronization costs), but keeping locks outside of the "hot path" is the goal. ------ This is what I do with my GPU test programs, where locking is a very, very bad idea (but possible to do). Even atomics are kinda-bad in the GPU world but relaxed-atomics are quite fast. ------- > In Java, this requires 15 000 separate allocations, each producing a separate reference that must be managed. Wait, what? points [] array = new points[15000]; This is a singular alloc in Java. 15000 _constructors_ will be called IIRC (My Java is rusty though, someone double-check me on that), but that's not the same as 15000 allocs being called, not by a long shot. ------ Frankly, it seems like this poster is reasonably good at understanding Go performance, but doesn't seem to know much about Java performance or implementations.
- GreenToad 5y agoPoint[] array=new Point[15000]; In java would create an array with null references, to fill it up you need to create each object so that point is valid.
- dragontamer 5y agoI stand corrected on that point.
- comex 5y agoEven with relaxed memory ordering, you’re still bouncing the cache line around between every CPU that tries to access bumpPtr. I would expect that to be significantly slower than using per-thread heaps (especially if you have a large number of CPUs).
- esarbe 5y ago> Java is a language that basically outsourced memory management entirely to its garbage collector. This turned out to be a big mistake. Given that Java is not far behind C and C++ for many kinds of workload and in quite some scenarios can outperform C code, I'm not sure that I buy this line of reasoning. > Doing these updates requires freezing all threads. Eh, what? There are multi-threaded GCs, what is this article even talking about? > However, this does not put C# and Java on equal footing with languages like Go and C/C++ in terms of memory management flexibility In which world are Go and C in the same worlds when it comes to memory management flexibility? That's like comparing a language that uses a Garbage Collector to one that requires manual memory management. Because - it is. > Modern Languages Don’t Need Compacting GCs > If need be, the Pacer slows down allocation while speeding up marking. So, you just traded in one drawback for another? Heck, I get it. The JVM is not a thin graceful fawn. It's a complicated beast that requires years of experience to tame it - and even then it will come back from time to time an bite you. There are many good points to critique the JVM - but I don't get the feeling that the author of this article has spent much time with modern JVMs because he's not pointing out any of them.
- hopsas 5y ago[flagged]
- maxpert 5y agoLow Java knowledge and more Fan babel rather than true critique.
- dandotway 5y agoThe 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 <==== Lisp (SBCL) 2.84 yes Ada (GNAT) 3.12 OCaml 4.68 yes Racket 4.81 yes C# .NET 4.81 yes Haskell (GHC) 5.02 yes Erlang 5.19 yes F# .NET 6.06 yes Node.js 7.20 yes Julia 7.43 yes Chapel 7.96 yes Dart 9.90 yes Go 12.23 yes <==== Swift 16.15 * "Automatic Reference Counting" Smalltalk (VW) 16.33 yes PHP 18.64 yes Ruby 23.80 yes Python 3 48.03 yes Lua 48.15 yes Perl 53.02 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/performance/binarytrees.html https://benchmarksgame-team.pages.debian.net/benchmarksgame/... [Edited link]
- geodel 5y ago> So Java has the fastest GC for this test, 2.48 secs vs 12.23 secs for Golang. Further not mentioning memory used Java/Go programs makes it very fair comparison. Because GC perf does not depend on memory allocated.
- madmax108 5y agoAs 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.
- geodel 5y agoFundamentals of Java about data/class layouts in memory have remain same for decades. So author is right at big picture. > 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. This is of course true. However the point is for Java it is absolute necessity for Go it may be nice to have.
- pjmlp 5y agoI guess it is the same reasoning like Go not needing generics.
- geodel 5y agoNo, it is same reasoning as Java not needing dense memory layouts
- kaba0 5y agoWork on Valhalla is not funded as a joke though. They are working on that one.
- geodel 5y agoOfcourse. The point was about above snarky comment. Go devs took time to implement generics balancing with competing priorities. Java devs taking time to implement value types balancing their competing priorities.
- pjmlp 5y agoFrom 1996 point of view it seemed it was a correct decision from the language landscape with Java coming from Smalltalk and Objective-C heritage, where 9 years later all languages had generics, except one created by the same authors of Go, and their newly born language. Also there have been extensions, and the work to fix it goes back to 2014, after escape analysis proving not being good enough, with IBM and Azul having their own extensions for value types. Even Dart down the corridor got generics first, during the years they weren't needed for Go.
- geodel 5y agoAll said and done it is basically: "There is a nuance when people I like do not do desired thing and excuse when people I don't like do not do same desired thing"
- 5y ago
- gwbas1c 5y agoSkimmed this and found that a lot of claims are inaccurate and don't have data... Then realized, ugh, it's another Medium post. One particular wrong claim: > In C#, for example, reference objects and value objects live entirely separate lives. Value objects are always passed by value. You cannot take the the address of a value object and pass that around. No, you need to copy the whole object every time. In C#, there are normal, C-style pointers. (Although they aren't used very often, mostly when interoperating with C APIs.) Furthermore, you can pass a struct by reference if copying the struct is a problem. I once wrote some performance critical code where I used structs and past them around by pointers. I ran the code through a profiler, and then refactored to use normal idiomatic non-pointer code. After running through a profiler there was no performance difference. --- The author of this post really needs to base their claims on actual data. Some well-tuned programs in the languages compared and then compare the performance.
- sitkack 5y agoIn general when talking about quantitative subjects, we need to use quantitative measures. I think the author is nearly there, but in general, unless you have data to refute your claims, it should be ignored. I don't say this snarkely, but in a domain where we actually have hard quantitative measures, they should be used and required for argument.
- WatchDog 5y agoQuantitative measures would be nice, but I would imagine that it's going to be really difficult to quantitatively compare go and java garbage collectors, without a billion other factors about the language/runtime getting in the way.
- kaba0 5y agoJava’s GC is the state of the art, without doubt. Other managed languages can be faster for some workloads (by not generating much garbage in the first place, eg go), but where it do come up: https://news.ycombinator.com/item?id=29323468 https://news.ycombinator.com/item?id=29323468
- netheril96 5y agoGo may have less need of an advanced GC, but surely it could only get faster if it had a Java-like GC.
- phonicwheel 5y agoHeadline follows logically from "Does not need Go"
- DeathArrow 5y agoLots of flaws in the article. Rust doesn't need a Java like GC because Rust doesn't have a need for GC. C# has pointers. "Old" languages like C and C++ also don't need a Java like GC, so it's not something related to "modern" languages.
- DeathArrow 5y agoI like how memory management is done in Nim [1]. You can chose among several garbage collectors, manual memory management or no memory management at all, depending on what better fits your use case. [1] https://nim-lang.org/docs/gc.html https://nim-lang.org/docs/gc.html
- winrid 5y agoThe default memory manager also won't do any stop the world pauses due to the way that cross thread communication works. That's not to say the latest Java GCs aren't incredibly impressive - they are.
- joelfolksy 5y agoIs there a single statement about C# in this piece that isn't a complete falsehood?
- pipeline_peak 5y agoWhat exactly does Go solve that can’t be done in C#?
- p5v 5y agoIt’s an insightful article, but I can’t get around the fact that almost every paragraph contains subtle typos. Makes me wonder, whether the author bothered to do a final pass on the text, or just dumped everything out of his mind, and hit Publish.
- rbetts 5y agoOr maybe isn't a native English speaker and did a wonderful job in a second language?