16 ms·
Simple techniques to optimise Go programs
- tedunangst 7y agoReplacing strings with ints would be more believable with a real world example.
- sjwhitworth 7y agoGood point. Here's one off the top of my head: a giant map that caches some count by date. You can represent this as a map[time.Time]int (native Go time type), a map[string]int (time represented in some string format), or a map[int]int (time represented as an integer like 20190718).
- ryanworl 7y agoDictionary encoding strings to integers is a common compression technique. In GC'd languages it fakes out the garbage collector because your dictionary codes are essentially pointers but not actually pointers from the GC's perspective. You also have the benefit of potentially using a smaller integer than a pointer if you know roughly the number of values you're encoding beforehand.
- weberc2 7y agoDoes this hold for Go? The tracing is the only relevant cost and tracing is very, very fast, so I would expect this to be negligible? Or am I misunderstanding the hypothetical scenario?
- kevinconaway 7y agoMost of these techniques sacrifice readability so before implementing them, you should really profile to make sure that the code in question is a hot spot in terms of allocations or CPU usage. That said, the example using sync.Pool is not quite right. The New function should always return a pointer type to avoid incurring an additional allocation on Get [0] The code should look like: var bufpool = sync.Pool{ New: func() interface{} { buf := make([]byte, 512) return &buf }} b := *bufpool.Get().(*[]byte) defer bufpool.Put(&b) [0] see example in https://golang.org/pkg/sync/#Pool https://golang.org/pkg/sync/#Pool
- likeabbas 7y agoSo maybe we should sacrifice compiler speed for having a better compiler optimize more readable code?
- SallySwanSmith 7y agoIsn’t that the entire point of higher order programming languages?
- weberc2 7y agoWhat are "higher order programming languages"? Did you mean "higher level programming languages"? If so, no, the point is that you can program more abstractly, almost always at the expense of performance. EDIT: Downvoters, I'm terribly curious which of these points you disagree with? Do you believe higher level languages (like Python, JS, or even Go) are faster than lower level languages (like C, C++, etc)? Or do you believe that HLLs are actually faster (or meant to be faster) than LLLs? Or maybe you are downvoting because I don't know what a "higher order language" is?
- sagichmal 7y agoThe downvotes are because you have chosen to make an irritating pedantic point about word use when the meaning was entirely clear in context.
- weberc2 7y agoWasn't trying to be pedantic; whatever is "entirely clear in context" is not entirely clear to me at all. Did he use a term that I'm unfamiliar with or did he use the term I'm familiar with in a way that is incongruent with my understanding?
- sagichmal 7y agoInformally, there is no difference between higher order programming languages and higher level programming languages.
- kstenerud 7y agoThe JSON marshaler actually caches struct serializations so that you don't incur the reflection cost on every run.
- 0815test 7y agoMissing technique: rewrite the performance-critical parts of your Go programs in a different language, and use Cgo to make them accessible to Go code via the standard C ABI. K.I.S.S.
- sjwhitworth 7y ago'I am deliberately ignoring techniques that require significant effort, or large changes to program structure.'
- justinclift 7y agoHopefully that's /s, as debugging CGo stuff is a royal pita. ;)
- vishvananda 7y agoalso the overhead of calling out to c can actually be quite high: https://www.cockroachlabs.com/blog/the-cost-and-complexity-of-cgo/ https://www.cockroachlabs.com/blog/the-cost-and-complexity-o...
- 0815test 7y agoWhile some overhead and increased debugging effort may be inevitable, it's a mistake to blame them on the use of Cgo itself; the root cause is Go's custom ABI and user-level-threading ("goroutines") model. And I really have to dispute OP's claim that the techniques they mention do not "require significant effort, or large changes to program structure" of their own. In many ways, rewriting some portion of the code can actually be simpler.
- jerf 7y agoWell, it may be true that the "blame" for why CGo is slow is up for debate, it's just a fact that it's rather slow to use. Go isn't unique in this; there are many languages with runtimes that require similarly intensive amounts of copying and context conversion before C code can run on whatever the data is. However, most of those languages, like CPython, are themselves slow enough that the penalty isn't as noticeable against the general background noise. (In general CPython requires a lot more copying too; Go is closer to C struct and array semantics and can more often get by with some form of memcpy, the internals of Python look nothing like that.) Go is fast enough that it's much easier to get into scenarios where in a tight loop you're spending 90% on CGo overhead if you're not careful with data flow. For those languages that have to copy a lot out of their runtime and are also fairly fast, they'll face the exact same issues. It's not really a "Go" issue per se, but the challenges faced by any language that wants a runtime significantly different from C. (One of the miracles of Rust is building an environment and runtime that isn't stuck on C's limitations but at the same time can still speak to C really, really cheaply. Plenty of languages have one or the other of those, but there aren't very many that have both. I'm not sure there's any other language that has threaded that particular needle so cleanly.)
- astockwell 7y ago> Allocate capacity in make to avoid re-allocation Am I the only one who has done this and used append() within a loop, and resulted in a slice that is 2x as long as the original desired length, and the first 1x of items are all empty? I quickly caught it and fixed the approach (instead of append, I used the `i` as the insertion index into my new empty slice which already had capacity allocated), but the ease with which that subtlety could be overlooked turned me off to this approach unless I profile and really find it's a hot path. Edit to add: I tried his code, and it resulted in the new slice being as expected.
- capo64 7y agoThe make function takes the type, length and capacity. If you don’t supply capacity, it defaults to length. So if you want to use append, do something like: make([]int, 0, length) That will allocate a slice that can fit length ints but has a length of 0 (so append starts at index 0). You can call len(slice) and cap(slice) to see the difference, but append will insert an element at index len(slice), growing the capacity if necessary
- astockwell 7y agoAhh thanks for the great explanation! Love HN.
- ben0x539 7y agoYou're running into the difference between make([]T, 0, n) and make([]T, n)
- weberc2 7y agoI occassionally hit this bug, but it's pretty easy to spot after I introduced it a couple of times. Allocations in Go are slow enough that I usually do this by default unless I know that performance absolutely will never be critical (e.g., doing CLI boilerplate that just needs to be fast on human timescales).
- jerf 7y agomake can take three parameters for slices, which is the type, the initial length, and the initial "capacity": https://play.golang.org/p/8d1mS1mGsW6 https://play.golang.org/p/8d1mS1mGsW6 By default, the slice is filled in and fully initialized. On the one hand, IMHO and IME that's not really what you want if you're going to append things which is a pretty common case, but on the other hand, worrying about "capacity" for slices, while perfectly sensible once you understand the abstraction, is something you can get in trouble with if you're not paying attention, because it makes you really pay attention to the fact the slice has an underlying array. (Many languages have this concept somewhere, Go just happens to put it in the syntax: A "slice" is an object with a reference to an underlying array, an index of the first element, a length, and a "capacity" which can be smaller than the underlying array has room for. If you want to grow past the underlying array, the runtime then has to create a new one and copy the elements out into that.) Similarly, if you have a slice of ten elements and you want to take a subset of it and start appending your own bits and pieces, you have to do that carefully: https://play.golang.org/p/QcZOndSp-9x https://play.golang.org/p/QcZOndSp-9x There's a syntax for reducing the capacity of the new slice deliberately, which forces any new "append" to copy to a new slice, producing the behavior you want. For as tricky as this can get, I'm surprised it doesn't come up more often, but so far in the last five years, this was the source of my bug I believe twice, and one of them I was doing rather inadvisable things anyhow. It turns out that in most "just code" you're not cutting lists up and distributing them various places very often.
- innocentoldguy 7y agoFor me, the best way to optimize a Go program is to write it in Rust.
- kchr 7y ago> In A/B tests, we tried delaying the page in increments of 100 milliseconds and found that even very small delays would result in substantial and costly drops in revenue. - Greg Linden, Amazon.com Just curious, do vendors on Amazon get reimbursed for the drops in revenue during tests like this?
- dymk 7y agoThey'll imperceptible to individual vendors, as it's distributed across thousands. "Substantial" is extrapolated from a small (but just large enough to draw strong statistical conclusions from) A/B rollout. Should Amazon charge vendors more when they identify where to invest more engineering effort, as a result of these A/B tests, which eventually lead to far higher increased revenue?
- Groxx 7y agoI'm going with "yes, it's part of the cost of using Amazon as a service to sell your goods"
- alex179ohm 7y agofirst technique: consider using rust :)
- Corrado 7y agoI had the same thought but then had a better thought. What type of optimizations would you need in Rust? Are they similar or completely different? I know that Rust doesn't have GC so that removes some of the methods in the original post, but I'm sure there are places in Rust that could be better/faster.
- pcwalton 7y ago> During a garbage collection, the runtime scans objects containing pointers, and chases them. If you have a very large map[string]int, the GC has to check every string within the map, every GC, as strings contain pointers. This would, of course, be much less of an issue with a generational GC, which doesn't have to scan the entire heap on every collection.
- cafxx 7y ago> a previous version of this blog post did not specify that the New() function should return a pointer type. This avoids an extra allocation when returning through the interface{} type. While this is good advice, it's not entirely correct. Even with the current go compiler there are ways to use sync.Pool with non-pointer values without incurring in the extra allocation, e.g. using a second sync.Pool to reuse the interface{}. Although I would not recommend it as it's slower, and much less maintainable. > The safe way to ensure you always zero memory is to do so explicitly: // reset resets all fields of the AuthenticationResponse before pooling it. func (a* AuthenticationResponse) reset() { a.Token = "" a.UserID = "" } I think this is safer, in face of modifications to the AuthenticationResponse structure, and much clearer in its intent: // reset resets all fields of the AuthenticationResponse before pooling it. func (a* AuthenticationResponse) reset() { *a = AuthenticationResponse{} }