Live data from Hacker News

Go Optimization Guide

goperf.dev

151–160 of 173 posts

Re: Go Optimization Guide

#151

Every perf guide recommends to minimize allocations to reduce GC times, but if you look at pprof of a Go app, GC mark phase is what takes time, not GC sweep. GC mark always starts with known live roots (goroutine stacks, globals, etc) and traverse references from there colouring every pointer. To minimize GC time it is best to avoid _long living_ allocations. Short lived allocations, those which GC mark phase will ne…

Interesting, and I think that is not specific to Go, other mark-and-sweep GCs (Java, C#) should behave the same. Which means that creating short lived objects (like iterators for loops, or some wrappers) is ok.

Not entirely. Go still doesn't have a generational collector so high allocation rates cause more GC's that must examine long-lived objects.

As such, short-lived objects have little impact in Java (thank god for that!). They will have second order effects in Go.

Re: Go Optimization Guide

#152
post #73

Earlier quoted context omitted.

That's just not true. Pool contents are GCed after two cycles if unused.

What do you mean? Pool content can't be GCed , because there are references to it: pool itself. What people do is what this article suggested, pool.Get/pool.Put, which makes it only grow in size even if load profile changes. App literally accumulated now unwanted garbage in pool and no app I have seen made and attempt to GC it.

https://github.com/golang/go/blob/master/src/sync/pool.go#L2...

The GC calls out to sync.Pool's cleanup.

Re: Go Optimization Guide

#153
post #90

Earlier quoted context omitted.

Yup, it's a fairly low-level language intended as a replacement to C/C++ but for modern day systems (networked, concurrent, etc). You don't have manual memory management per se but you still need to decide on heap vs stack and consider the hardware.

"you still need to decide on heap vs stack" No, you can't decide on heap vs stack. Go's compiler decides that. You can get feedback about the decision if you pass the right debug flags, and then based on that you may be able to tickle the optimizer into changing its mind based on code changes you make, but it'll always be an optimization decision subject to change without notice in any future versions of Go, just lik…

Just an anecdote from work to back this up. I wrote a system that was taking requests, making another request to a service (that basically wrapped elasticsearch) and then processed the results and returned to the results to the caller.

By default the elastic-search results were paginated and defaulted to some small number in the order of 25..100. I increased this steadily upwards beyond 100,000 to the point where every request always returned the entire result in the first page. And it _transformed_ the performance of the service. From one that was unbearably slow for human users to one that _felt_ instantaneous. I had real perf numbers at the time, but now all I have are the impressions.

But the lesson on the impact of the overhead of those paginated calls was important. Obviously everything is specific and YMMV, but this something worth having in the back of your mind.

Re: Go Optimization Guide

#154
post #120

Earlier quoted context omitted.

> But it's simply a fact that using the `any` type means that certain properties of the program can't be checked at compile time Yes, structural typing removes the ability to check certain properties at compile-time. That doesn't make it typed like Python, though.

"any" is not structural typing.

any isn't a special type. It is an alias for interface{}.

The empty set is trivially satisfied by all types, but obviously can be narrowed as you see fit.

Re: Go Optimization Guide

#155
Calling mmap “zero copy” is generous. I guess we glaze over the whole page fault thing, or the fact that performance is heavily dependent on how much memory pressure the process is under.

This is the same n00b trap that derailed the llama.cpp project last year because people don’t understand how memory maps and paging works, and the tradeoffs.

Re: Go Optimization Guide

#156
post #104
post #69

Earlier quoted context omitted.

Not at all. Most objects die young and thus are never moved. Also, the time before it is moved is very long compared to CPU operations so it is only statistically relevant (very good throughput, rare, longer tail on latency graphs). Also, write-only barriers don't have that big of an overhead.

It doesn't matter if objects die young — the other objects on the heap are still moved around periodically, which reduces performance. When you're using a moving GC, you also have additional read barriers that non-moving GCs don't require.

Is that period really that big of a concern when your threads in any language might be context switched away by the OS? It's not a common occurrence on a CPU-timeline at all.

Also, it's no accident that every high-performance GC runtime went the moving, generational way.

Re: Go Optimization Guide

#157
post #89
post #70

Earlier quoted context omitted.

This is still strong typing, even it it's not static typing. It's static vs. dynamic and strong vs. weak. https://stackoverflow.com/a/11889763

It is strong, static, and structural. But structural typing is effectively compile-time duck typing, so it is understandable that some might confuse it with dynamic typing.

Ggp is not talking about structural typing, but about sync.Pool type erasing (it takes `any` values, and returns `any` values). So you can put (and will retrieve) random garbage from it.

Re: Go Optimization Guide

#158

Can someone explain to me why the compiler can’t do struct-field-alignment? This feels like something that can easily be automated.

It can. Rust does.

That requires a way to opt out tho, because there are situations where you need a specific field ordering, so now the langage needs to provide way to tune struct compilation behaviour.

Re: Go Optimization Guide

#159
post #156
post #104

Earlier quoted context omitted.

It doesn't matter if objects die young — the other objects on the heap are still moved around periodically, which reduces performance. When you're using a moving GC, you also have additional read barriers that non-moving GCs don't require.

Is that period really that big of a concern when your threads in any language might be context switched away by the OS? It's not a common occurrence on a CPU-timeline at all. Also, it's no accident that every high-performance GC runtime went the moving, generational way.

That time may seem negligible, since the OS can context switch threads anyway, but it’s still additional time during which your code isn’t doing its actual work.

Generations are used almost exclusively in moving GCs — precisely to reduce the negative performance impact of data relocation. Non-moving GCs are less invasive, which is why they don’t need generations and can be fully concurrent.

Re: Go Optimization Guide

#160
post #88
post #76

Earlier quoted context omitted.

Comparing with a fairly optimized malloc at $COMPANY, the Go allocator is (both in terms of relative cycles and fraction of cycles of all Go programs) significantly more expensive than the C/C++ counterpart (3-4x IIRC). For one, it has to do more work, like setting up GC metadata, and zeroing. There have recently been some optimizations to `runtime.mallocgc`, which may have decrease that 3-4x estimate a bit.

How can that be true? If it is 3-4x more expensive than malloc, then per my measurements your malloc is a bump allocator, and that simply isn't true for any real world malloc implementation (typically a modified free list allocator afaik). `mallocgc` may not be fast, but I simply did not find it as slow as you are saying. My guess is it is about as fast as most decent malloc functions, but I have not measured, and it…

I should correct and clarify: I meant 3-4x more expensive in relative terms. Meaning:

  - For C++ programs, the allocator (allocating+freeing) consumes roughly 5%  of cycles.
  - For Go programs, the allocator (runtime.mallocgc) used to consume ~20% of cycles (this is the data I referenced). I checked and recently it's become closer to 15%, thanks to optimizations.
I have not tested the performance differential on a per-byte level (though that will also differ with object structure in Go).
Post reply on HN