Writing slower Go programs

Writing slower Go programs

umbrella.png

Why you should be optimising for readability, not performance

Wait, what? Slower? Shouldn't we be worrying about making our Go programs faster?

Not really. Optimizing Golang code for performance is almost certainly a waste of your time, for several reasons:

  1. Performance doesn't matter
  2. Go is fast
  3. Readability beats speed

These rather terse statements need a little elaboration, which I'll give them. There are exceptions to them, as there always are to any non-trivial statements. And it's fair to say that all three points are hardly the consensus of opinion among software engineers. So before you take to the internet to register your disgust ("Worst blog post ever"), read on.

Performance doesn't matter

For most of the programs that most of us write, it really isn't critical exactly how fast they execute. For example, here are some recent programs I've worked on:

  • Website link checker (spiders a site to find and report broken links)
  • Terraform provider (manages site monitoring checks via Terraform code)
  • Data analyser (downloads and computes tables of statistics on monitoring data)
  • Site migration tool (moves website files and databases between servers)

All these programs are run relatively rarely (once or twice a day, perhaps), and take a brief time to run (a minute or two). Is it, a priori, worth spending any engineering effort making them faster? Almost certainly not. (If you're convinced already, you can stop reading, but I sense I may need to say a little more.)

It ain't Go that's slow

Even in the rare cases where we care that the program runs quickly, it's not the Go code that's the problem. For example, the link checker spends 99% of its time just waiting for HTTP requests. The Terraform provider spends most of its time talking and listening to a remote API. And the bottleneck in the site migration tool is, by a huge margin, copying multi-gigabyte tarballs over the network.

So if we did need to make performance optimizations of such programs, we wouldn't be doing it at the Go level. CPUs are ludicrously fast compared to memory accesses, which are much faster than disk accesses, which are much faster than network accesses, which are much faster than interactive users. So the execution speed of our code is probably literally the last thing we need to worry about.

When every ns/op counts

"Hold it right there, buster," I hear you say; "I'm writing a 3D game in Go, and I have an unbelievably tight frame budget. Every nanosecond counts, and you're saying I shouldn't worry about optimization? Are you dumb?"

No. Games are a legitimate exception to this rule, because certain small parts of the game code (for example shaders) are speed-critical. Note, though, that even there we introduced a caveat: performance matters only for a small subset of game code. And unless you're writing a 3D engine from scratch (don't do this), your own code is unlikely to be the bottleneck in frame drawing.

You are not Google... or in space

But most Go programs aren't games. Are there other exceptions? Yes: network servers such as web servers and APIs have performance-critical sections. In a large, complex distributed system, the overall request latency is important, and thus the latency of each individual subsystem can matter down to the nanosecond. Again, you and I are unlikely to be writing such programs, and when we do, we better know what we're doing.

And of course there are many special-purpose and embedded systems where speed counts, such as I/O drivers, real-time hardware like satellites and industrial control systems, and I grant you exceptions for all these, while maintaining firmly that they don't represent typical Go programs for typical Go programmers.

Go is fast

Even if performance isn't a major consideration for the majority of programs, that doesn't mean we shouldn't think about it. The choices you make as a programmer have an impact on the performance of your program, and that includes things like memory and disk usage as well as raw speed. It's possible to write programs that perform pathologically badly, so that they're slow and irritating to use, or waste resources, such as reading an entire file into memory at once instead of processing it line by line.

However, in general Go programs are pretty fast and efficient. If you're coming to Go from interpreted languages like Ruby, PHP, or Python, Go simply blazes with speed. This is mainly because it's a compiled language. No interpreter, no matter how clever or how well-optimized, can match the performance of something that compiles to pure machine code.

Travelling light

Compiled Go programs are also small. Interpreters are big and complex, and do a lot of work just to interpret your program before they even start to execute it. And you need to distribute the interpreter along with the program.

Compared to that, a Go binary contains only the machine code for the program itself, plus the Go runtime, which provides facilities like goroutines and garbage collection. This makes Go programs light on memory and disk usage, quick to execute, and easy to build, package, and distribute.

The first optimisation is the choice of language

So we generally don't need to worry quite so much about the performance of our code in Go, given that we're starting from a very fast baseline, thanks to Go's compiled nature. Sure, there are faster compiled languages (Rust, for example), but that's not the point. If performance is really critical for your application, then don't waste your time tuning and tweaking your Go code: just use a different language. I hear C++ is pretty good.

Readability beats speed

Here's the thing. Most programs spend a good deal more time being read than they do being executed. And programmer time is a lot more expensive than CPU time (good luck finding a developer for five cents an hour). So doesn't it make sense to optimise programs for readability instead of raw speed?

Of course, we'd like our programs to be readable as well as fast, and that's often possible. But when our engineer's spidey-sense starts twitching, and telling us "Hey, I could make this a few nanoseconds faster per call by using an array instead of a map, and computing the index using the low-order bits of the key", it's important to think about what we're giving up in exchange for the nanoseconds.

The cost of everything and the value of nothing

Every optimisation is a trade-off. What we're losing when we trade off against speed and memory is usually:

  • Brevity. There's more code, so it's harder to read.
  • Clarity. It's no longer obvious how this works.
  • Simplicity. More functions, more abstractions, more indirection, more data structures to read and understand.

Simplicity is part of the Tao of Go; it's built into the language design, and our programs should inherit the same values. Clear is better than clever.

A great way to teach yourself the value of readability is to try to read someone else's code. Often as developers we don't do a lot of this. We maybe either wrote the code ourselves, or were in the room while it happened, so we have a pretty good idea what it's all for and how it all fits together. We may even have a sense of how it's changed over time due to shifting requirements and feature updates.

A strange house in the dark

When you're looking at code for the first time, you don't have any of that context. It's like stumbling around in a dark room, tripping over wires and stubbing our toes on unexpected furniture. "What the heck was that?" Dip into any large Go project on GitHub and you'll see what I mean. (Which file do you start reading? Does a non-trivial program even have a beginning?)

Now, it's easy to make a program simple if it doesn't do much. Some programs are complex because the problem domain is complex (Kubernetes). There's a certain irreducible complexity to anything we write. But that's not an argument for further obscuring it with clever, tricky, hyper-optimized code. Rather, it suggests we should do everything we can to make our code as simple, straightforward, and obvious as possible.

Write better code, less comments

It should be possible for the average Go programmer to drop into any of your functions, at any level, and understand more or less at a glance what this code is for and how it works. By this I don't mean that you should slather it with comments. Comments should be resorted to only when we've done everything else possible to make the code simple and obvious, and in a way writing a comment is admitting defeat. It's saying "I know you won't understand this as written, so here's my attempt to explain it a different way."

I'm the first to admit that this is far easier said than done. Writing simple code is hard. Firstly, in order to explain a concept to anyone, you need to understand it yourself. To explain it to a computer, you need to understand it inside out and backwards. That's often not the case.

Why do you hate fun?

On the other hand, tuning code for performance is fun and easy. We all enjoy it. Very often, we're like the drunk searching for his keys under the lamp-post, even though he actually dropped them somewhere else: the light is better here. We see something that can easily be optimised and made faster, so we do it. Like the scientists at Jurassic Park, we're so preoccupied with whether or not we could, we don't stop to think if we should.

Secondly, we became engineers because we love being clever and doing clever, tricky, ingenious things that other people wouldn't have thought of. It's completely legit to do that in our own private projects and playtime. Commercial software engineering is entirely different. "Clear is better than clever", says a Go proverb, and that sums up my whole message pretty neatly.

Zen mind, beginner's mind

As a Go mentor and teacher, I find that people who are new to programming have no trouble with this idea. All code is puzzling to them at first, so they're absolutely on board with the idea that it might be difficult for other people, too. They're not tied up with ideas of their own cleverness or an identity to maintain as a hot-shit programmer, so they're happy to write simple, elegant, straightforward code that does what it says on the tin.

By contrast, people with lots of experience in other languages sometimes struggle to get to grips with simplicity in Go. If they come from interpreted languages like Ruby, they tend to express themselves in complicated, non-obvious, highly individual ways, which those languages allow. If they come from the world of C++ and Java, they write code which seems to the average Gopher to be absurdly over-engineered. That's just how things are done over there. In many cases they're actually relieved to learn that that's not the Go way.

Conclusion: Slow your Go, bro

The 'Slow Go' movement, if I may coin the phrase, is about doing less, more simply, and doing it slower.

Naturally, I'm not advocating making programs unnecessarily slow. What I am saying is that, firstly, you don't need to worry about performance as much as you probably think. When you do, there are generally easier ways to improve it than by writing obscure and ingenious code.

Secondly, you should worry much more about simplicity and readability than you probably do right now. When you think about optimisation, that's what you should be optimising for. Spend an hour improving readability for every minute you spend tuning for speed.

Thirdly, be willing to invest in readability. If you can refactor your program to make it significantly clearer, at the cost of making it fractionally slower, then do it. When performance does matter, remember it doesn't come free. If you're contemplating adding extra complexity to your code in pursuit of pure speed (a special case here, a clever bit-twiddling trick there), then think again. You can always buy a faster computer, but you can hardly buy a faster brain.

Are you a Go black belt?

Are you a Go black belt?

Go maps FAQ

Go maps FAQ

0