Moving faster

Last updated 2021-10-23

This post is part of a series, starting at Reflections on a decade of coding.

I don't think I'm very fast in an absolute sense, but I'm much much faster than I was 5 years ago.

These are the things that I think made the most impact.

Care

The main thing that helped is actually wanting to be faster.

Early on I definitely cared more about writing 'elegant' code or using fashionable tools than I did about actually solving problems. Maybe not as an explicit belief, but those priorities were clear from my actions.

I probably also wasn't aware how much faster it was possible to be. I spent my early career working with people who were as slow and inexperienced as I was.

Over time I started to notice that some people are producing projects that are far beyond what I could do in a single lifetime. I wanted to figure out how to do that, which meant giving up my existing beliefs and trying to discover what actually works.

The main theme for most of the ideas below is being systematic about improvement. I had always been onboard with team processes like continuous integration, code review and root cause analysis, but for a long time I was completely haphazard about my own processes.

Now when I finish a chunk of work I look back and ask why it took me as long as it did and whether it could have been faster. This process is usually uncomfortable and I often manage to avoid thinking about the things I'm doing wrong so that I can stay in my comfort zone.

But even haphazardly applied and mostly ignored, this most basic amount of trying has lead to big improvements.

Make decisions based on goals

I wrote already about setting goals. Having explicit goals helps avoids decision paralysis. Whenever I notice I'm stuck on a decision I go through this process:

Example: What data structure should I use for text in my text editor?

Some decisions don't matter much individually but come up often eg coding style, variable naming, code organization. For these it's worth spending a little time to make rules of thumb rather than thinking about them each time they come up.

Example: I used to spend a lot of time thinking about how to split code up into files and directories based on what code 'belongs' together. But that's a completely vague criteria and also doesn't have any obvious connection to my actual goals. There are also usually multiple different axes along which things could be grouped eg a compiler could be split by pass, or by language features. So now I tend to put everything into one file until I start to notice difficulty navigating, and then I split out things that I've noticed I tend to read or edit at the same time. Unlike the previous criteria this is easy to evaluate by just remembering what I did and is easy to connect to my goals - it means less jumping around in future edits which will make coding and debugging slightly faster.

The most important class of decisions is 'what should I do next?'. There are always far more options than time. Having explicit goals makes it easier to prioritize the list.

For tools that I use myself, I prioritize by time saved or quality improved. For commercial projects, priorities come from customers. For research projects, the priority is whatever will give the most information about the research question/hypothesis.

It's often possible to raise quality on one axis by lowering it on another, like improving throughput by consuming more memory or vice versa. If I don't know what the requirements on throughput and memory are then there is no way to decide which tradeoff to make.

A more subtle decision is how long to work on something. There are usually declining returns on time invested so it might be more valuable to half-ass three tasks than to do one task perfectly.

Example: It took me a really long time to get comfortable writing 'ugly' or 'messy' code. In an ideal world I would maintain the quality of all my work to the limit of my ability. But I find that I can write substantially more if I relax a little. I'd rather have a finished project that is 80% polished than a 1/3rd of a project that is 100% polished. This is especially true for experimenting, prototyping and testing, where quality doesn't matter as much. But even production code often has a short half-life - I'd guess maybe half of my code gets thrown away or rewritten in it's first year.

I used to not think much about any of this, and the result was that I would spend a lot of time:

Focus

I work in blocks of 2-3 hours during which I don't do anything else - no email, slack, twitter, hacker news, chatting to my neighbour etc.

I take small breaks within those work blocks but don't do anything that might take over my focus. So I'll walk around, stretch, make tea etc but not look at my phone or check my email.

Music definitely hurts my concentration, but it also improves my mood so I'll sometimes play a single album if I'm having trouble getting started in the morning. Usually by the time the album finishes I'm deep enough that I don't notice it's gone.

If I have to stop at a specific time (eg for a meeting) I'll set an alarm rather than trying to remember and having that background worry.

I find I work best in the morning. I usually struggle to get started but once I do it's pretty easy to keep going. I find it easier when I have a consistent morning routine - wake up, have breakfast, go for a walk, plan what I want to work on while walking and then start as soon as I get back. I don't always manage to stick to this, but when I do I don't have to expend effort to force myself to start working - it's just what happens next.

These changes may sound trivial but I can't overemphasize how much difference they made when applied consistently. Attention and short-term memory are the bottleneck that everything else has to flow through but they are incredibly fragile and, increasingly, exposed to adversarial input.

See also Deep Work, Your Brain at Work (the latter is a bit pop-psych but still sound).

Batch

In the last couple of years I've been applying the idea of avoiding multi-tasking on a more fine-grained level.

For example, I notice that when I try to mix actually writing code with deciding what code to write, I often forget subtasks or start feeling uncertain and slowing down. So now when I start on a block of work I try to separate the subtasks:

  1. Write down what I'm trying to achieve.
  2. Figure out roughly how I'm going to do it.
  3. Walk through the code and make a brief list of the changes I need to make.
  4. Make the changes one by one in the order they appear on the list.
  5. Read the diff and fix obvious bugs, improve comments, pick better variable names etc.
  6. Test and debug. Go back to step 2 if I realize I made a poor choice and need to do something differently.
  7. Commit, merge, make some tea.

Often in step 3 I realize that my plan isn't actually going to work. Discovering that before I write a bunch of code saves a lot of time, and not having that sunk cost also makes me less likely to try to push it through anyway.

In step 4 if I notice other changes that need to be made I add them to the list instead of trying to make two changes at once (unless it's something really small like correcting a comment as I pass by). Whenever I think I can get away with making multiple changes together it usually ends up taking longer, or I realize that one of the changes was a bad idea but now I can't just roll it back because it's mixed in with the others.

I do this kind of batching all over the place. Pretty much any complex task involves switching between multiple subtasks. Whenever I can figure out how to rearrange them to reduce the amount of switching I tend to find that it saves time and reduces mistakes.

Make small changes

Whenever I have a big change to make - anything that will take more than a couple of days - I want to break it down into small changes that can be individually merged. This makes life easier in so many ways:

I have a nice self-contained example of breaking up a big change here. I wanted to change the internal representation used throughout the compiler. Instead of trying to do it all at once, I made a parallel pipeline for the new version and built it out stage by stage while keeping the old version working. Once the new version was complete and produced the same results on all the tests I deleted the old version.

This only took 4 work blocks but they were spread out over a couple of weeks. When I was younger I probably would have tried to just edit the representation and make all the changes at once, which would have meant that this project wouldn't even have been able to compile successfully for a couple of weeks. Every time I came back to it I would have had to remember what I had finished editing and what still needed doing. When I finally tried to compile it I would have been trying to fix type errors from code that I had written two weeks earlier and that was no longer fresh in my mind.

See also Programming Incrementally.

Shorten feedback loops

The longer it takes between making a mistake and finding out, the harder it is to track down - the number of possible causes grows and the context fades.

Similarly, the quicker I can evaluate the impact of decisions, the more different things I can try and the less likely it is that a bad decision gets baked in and becomes hard to undo.

I've come to prioritize fast feedback more and more over time. It's the main thing I look for when choosing tools and when planning systems.

Examples:

Sometimes it's possible to work around long feedback loops by working in parallel. When I work on projects which have long compile times (eg materialize incremental builds sometimes took 14 minutes) I sometimes get distracted and when the compile finishes I've forgotten what I was testing. To reduce the impact I'll sometimes make a list of multiple experiments and run one per desktop, with a text file open on each desktop explaining what I'm doing. By the time I've finished setting up the last experiment hopefully the first one has finished running. This still isn't as effective as just having shorter feedback loops, and for benchmarking it requires having a lot of extra hardware to avoid interference. But sometimes that's the best I can do.

Keeping feedback loops tight is particularly important on large projects. Once some slow process has been added it quickly becomes hard to remove. By far the easiest way to have short feedback loops is to start out that way and then be very strict about not letting them regress. An example I found inspiring is bitsquid's subsecond live reloading. They set that goal from the start and just refused to accept any architecture choices that broke it.

Write stuff down

I keep a work journal - a single very large append-only text file. I have a bunch of different uses for it:

None of this takes much work. I typically write about 100 words per day.

Reduce frequent mistakes

Often I make the same kind of mistake multiple times per day. When I notice this I try to think of a way to avoid making that mistake at all.

Examples:

Some things I haven't tried yet that I suspect would be worthwhile:

See also The Checklist Manifesto.

Make low-level skills automatic

I wrote earlier that multi-tasking is just rapid context-switching. That's actually only true for skills which require conscious attention. It's possible to learn some kinds of skills so thoroughly that they no longer require attention. Good candidates are:

Any low-level skill that I can make automatic frees up attention and short-term memory for more important things.

A corollary to this is avoiding unreliable tools that break muscle memory.

Example: The final straw that made me stop using emacs was running into multiple bugs that each caused sporadic multi-second pauses during which my keystrokes would go to the wrong window or get queued up on a non-responsive window. Each time it would leave me confused as to what state things were now in and then after I fixed it I would have to take more time to remember what I was doing.

Reflect

Whenever I notice some process that is unreliable or that could be faster I make a quick note in a file called 'tools'. It's full of things like this:

zig pretty-print in gdb?
command-line version of magit to avoid emacs startup times
  try gitui
clipboard daemon with history
try tracy
fix focus hang/crash on big rg (need to limit input?)

Whenever I have a few spare hours I go through that file and try to fix one of the entries.

On a larger scale I try to write retrospectives in my work journal after each project.

Example: For materialize I spent many weeks fixing bugs in name resolution. In hindsight, if I'd started by spending a day looking at the spec and the postgres parser I would have realized that name resolution was a lot more complicated than I thought and I would have started out closer to a correct design.

Etc

It's easy to come up with more ideas that I haven't tried but which sound like they could work.

Examples: