> Say you have 4 cores, 1 would be dedicated to GC
In my opinion, idiomatic golang is not that GC heavy. A slice (a bit like ArrayList) of structs can be a contiguous chunk of memory as opposed to array of object references, which requires allocating each object separately.
There are an order of magnitude less allocated objects than in Java for example. (Perhaps the situation will change once Java gains value types in maybe Java 9).
> I am not sure about D, but Go also uses escape analysis so that data structures are allocated on the stack instead of the heap when possible which can help to reduce garbage and hence memory usage.
D sometimes does this but for the most part it depends on how you write the code. strings, large arrays, and class objects are typically GC'd but struct instances and small arrays are often not.
The code in the blog post appears to mostly use small stack memory.
The only weird thing i see in the blog post is 30 second compile. Yikes, this should be more like 3 second from scratch, max - it must use some heavier-than-necessary dependencies.
GC languages often do and also often do not. Most modern GC languages have escape analysis. So if the compiler can deduct that an object does not escape the current scope, it is stack allocated instead of heap allocated. Modern JVMs do this and Go does this also. Furthermore, Go is way more allocation friendly than e.g. Java. In Go an array of structs is a single item on the heap (or stack). In Java, you would have an array of pointers to separately allocate on the heap (Java is just now trying to rectify this with the "record" types). Also, structs are passed by value instead of reference.
As a consequence, the heap pressure of a Go program is not necessarily significantly larger than that of an equivalent C or Rust program.
> Unlike C, you don't need to worry about the heap-vs-stack in Go. […] It's an implementation detail.
I'm really surprised to read that: yes a beginner can get his code working without wondering about stack vs heap (and that's one of the big reason why Go is easier to learn than Rust for people coming from non-system language), but as soon as you care about performance (which many Go users do!), you need to write code that reduces allocations to the minimum, because Go's allocator is really slow (compared to Java for instance). Interestingly enough, doing so forces you to think about the ownership and lifetime of your objects, like you'd do in C (or Rust).
> Golang only has a GC, it doesn't have an option to manage memory in other ways.
But as far as I understand golang's memory internals, they still offer you to use the copy-based stack directly ("var some SomeStruct;") or to allocate things directly on the heap (via "new(SomeStruct) / make(SomeStruct)".
I might be wrong about this, but this is what I understood from casually reading the spec [1]; while they never mention stack or heap specifically and describe it more as memory being allocated at run time, which kind of hints to a copying garbage collector underneath. But they also seem to implement a mark and sweep mechanism [2] so I'd say it's a hybrid GC, similar to how ECMAScript VMs work these days.
Nevertheless you're right with the argument that it doesn't offer a way to manage memory yourself, which I think is a good thing. Technically you could use "C.malloc()" and "C.free()" though.
> Were you referring to unsafe pointers and calls to Cgo?
Yeah, I was kind of referring to the possibility to implement C-interface adapters using CGO (the internal "C" and "unsafe" packages). Personally I would only use C APIs if there's no way around them, though, and keep as much code in golang as possible.
> But as far as I understand golang's memory internals, they still offer you to use the copy-based stack directly ("var some SomeStruct;") or to allocate things directly on the heap (via "new(SomeStruct) / make(SomeStruct)".
Those are equivalent calls, you can't explicitly choose to allocate on the stack/heap.
Conceptually there's no distinction between the stack and heap in Go, you just allocate whatever memory you want and the runtime handles cleanup for you.
In practice the compiler will perform escape analysis to place everything it can on the stack, but that's an implementation detail, you don't get to explicitly choose when allocating.
At best you can get the escape analyzer to show what it thinks escapes to the heap and try to coax it to allocate on the stack instead.
> Go couldn't afford not to have a generational GC without value type
And Java couldn't afford not to have value types without a generational GC ;-)
> C, C++ and Rust can't use a bump allocator since you need a compacting GC to do so. But allocations are really expensive in these languages and removing them is often the first step of optimization.
Agreed. This is why Go programmers use the same optimizations as C, C++ and Rust programmes is such a case (by allocating from a pool or an arena).
Are you aware of cases where what is gained thanks to the bump allocator is lost because of compaction?
> I considered stack-allocated structs in Go, but honestly that doesn't strike me as a particularly major thing;
But for me they're one of the major ways in which I control memory use in Go programs. So, yeah, I think you underestimate them. No condescension implied. Apologies if it came across that way.
Go's GC has been precise for heap-allocated data since go1.1 (released 3 years ago) and precise for heap and stack-allocated data since go1.3 (released 2 years ago), completely fixing any GC problems with values that look like pointers.
I agree that the statement about avoiding large numbers was farcical, but it was not made by "Golang" but rather by someone on the go-nuts mailing list. Your sentence makes it sound like this was general advice for people programming in go from the go team, but to the best of my knowledge this is not true.
> In Go, the user of a type decides whether it will be used by reference (on the GC heap) or directly on the stack or embedded in an object: you can take a pointer to any type, or use the type directly.
Actually the distinction between heap and stack is not determined by how it is referenced. The compiler is free to stack-allocate any value as long as it does not escape the function. We can do this because pointers are opaque; there's no arithmetic.
The main point is that Go does not have classes. You just define methods on values. Values are no bigger than the data they represent, so you don't suffer from the same kinds of overheads seen in other "OOP"-centric languages.
> in practice there's a LOT of different ways the memory management can be handled, many of which are deemed "pauseless GC" (though the term is somewhat misleading).
Yes, but I'm pretty sure those "pauseless GC" schemes impose other tradeoffs.
> My statement was considering that reality though. While not true for some use cases, in the vast majority of cases, the runtime optimizes the allocations more than sufficiently.
I'm not sure I follow. The same could be said for Go--in the vast majority of cases, Go's tradeoffs (slow allocations, low latency / non-moving GC) are also suitable.
> Allocators can do a pretty good job of minimizing the overhead of allocation, to the point the amortized cost isn't much more than a single machine instruction.
As far as I know, speeding up allocations to this degree requires a moving GC which imposes a bunch of other constraints (including copying a bunch of memory around).
> Allocating gigabytes of memory quickly is possible. Copying the data can be a lot more work, and often objects have copy semantics that add a lot more additional work.
Yes, but the bottleneck here wasn't the copying, it was the allocations. And if you optimized away allocation cost entirely such that only the copy cost remained, that cost would be so small that the OP would never have bothered to profile because copying small objects like this is so cheap compared to everything else (even if it is expensive compared to bump allocating).
> I think you're implicitly saying "a runtime that minimizes heap allocations" there, in which case I'd agree.
Yes, the allocator and GC are concerned with heap allocations and not stack allocations. I'm using "allocations" as a shorthand for "heap allocations".
It's a short article. I was looking for any distinction between heap allocated and stack allocated objects. The compiler does escape analysis and will stack allocate anything that it can to a large degree.
It then goes on to say
> While allocation is as efficient as possible, it doesn’t avoid the memory pressure on the L1/L2 caches of your CPUs and when many cores are busy, they are contending for memory in the shared L3 cache.
So I suspect that it's not all about gc, but also the overhead of object memory and especially object packing for caching and use of objects between threads. That's why we have things like LMAX Disruptor[0].
Until we get Project Valhalla value objects, you're likely better off using Go than Java for the object packing efficiency.
- in general, understand which types are values, and which have to be allocated. E.g. the slice structure itself is a value type (typically 24 bytes), but it points to an array, which might be heap allocated. Re-slicing creates a new slice structure, but does not touch the heap - extending a slice might.
- to analyze your code, use go build -gcflags="-m", this will print out optimization information, especially which allocations "escape to heap", which means, they are heap allocated and not stack allocated.
- Interfaces are really great, but copying values of interface{} does involve heap allocation of 16 bytes. Also method invocation via interfaces has some overhead. Method invocation of non-interface types is a fast a plain function call.
I believe that fragmentation isn't such a problem in Go programs because they make less use of the heap than Java programs. I can't remember where I read this though.
I don't know much about the internals of Java, but I know that Go will stack allocate any kind of object if the compiler can prove it doesn't escape, and that in Go lots of things (not just native types) are passed by value on the stack instead of by reference to a heap value.
You can still stack allocate things in Go. And use closures when you need nested scopes.
This lets you take garbage off the heap, and avoid GC all together if you want to.
I believe Go does escape analysis so you can still shuffle around pointers to local structs within a stack frame without causing a heap allocation.
edit: That being said you can't really implement something like refcounting for structs you pass around by pointer outside the local stack frame without the compiler emitting an allocation on the heap. To totally avoid using the GC you'll need to do a lot of copying. And there aren't destructors.
Go doesn't have a generational & compacting GC (where allocation can be made really efficient: just bumping a pointer), hence heap allocation are expensive in Go (but idk the details, and maybe they're not as expensive as they are in C or Rust).
Then to avoid performance penalties, you need to reduce allocations to the minimum, but since Go use escape analysis to decide whether to allocate on the heap or not, you don't have full control on what is heap-allocated or not, and avoiding allocations can be quite tricky.
Golang still uses one global heap; it just allows for stack based allocation in situations it can do it (avoiding putting stuff on the global heap), so copies _can_ be cheap re: GC.
In my opinion, idiomatic golang is not that GC heavy. A slice (a bit like ArrayList) of structs can be a contiguous chunk of memory as opposed to array of object references, which requires allocating each object separately.
There are an order of magnitude less allocated objects than in Java for example. (Perhaps the situation will change once Java gains value types in maybe Java 9).
reply