Skip to main content

Code Readability vs. Performance

· 9 min read
Nikita Lazarev-Zubov

Code Readability vs. Performance

Introduction

The debate over the readability versus the performance of code is nothing new. More than once, during a code review, I’ve witnessed emotional discussions about a particular piece not being readable enough. I would argue for making it more readable, while the author would in turn argue that this would make the code less performant.

Who’s right? What’s more important, readability or performance? And are they necessarily mutually exclusive? Let’s find out. But first, let’s start with these two fundamental questions: What does it mean for code to be readable and what does it mean for it to be performant?

What Is Readability?

Readable code is code that programmers can understand with ease. But why is this important? In the end, code is written for machines, no? But computers don’t read your code; they use machine code produced by compilers or interpreters from your source code. The main reader of your code is you. And you need it to be understandable in order to make it less error-prone and easier to debug.

Your colleagues also have to read your code. When development teams create or maintain software, they spend more time reading code than writing it. Your fellow programmers need to understand what is already written in order to extend it with new features and fix bugs: It’s not possible to fix code without knowing how it works.

How do you know if your code is readable? If your colleagues don’t spend the entire day trying to understand what you wrote and don’t ask you too many questions, you can rest assured your code is understandable. How can you achieve this? Code style helps a lot: meaningful variable names, consistent indentation, etc. Happily, we have linters and formatters, already included in IDEs or plugged-in, to help.

Another important thing is making code intuitive, and this is what takes experience. For instance, can you immediately get at what this Swift code does?:

    var result = maxColumnsCount
while result > 0 {
if width >= (minColumnWidth * result) {
return result
}
result -= 1
}


return 1

It’s code I once came across in a real project. Although it’s written using common code style, it took me a while to understand what exactly happens there.

This is another snippet that does exactly the same as the previous one but looks entirely different:

    max(min((width / minColumnWidth), maxColumnsCount), 1)

Again, there’ll likely be no complaints from linters, but I personally still find it very difficult to grasp.

What the above code does is calculate the number of table columns, having as input a maximum table width, a minimum column width, and a maximum number of columns. Here’s the final version that was used:

    let nominalColumnsCount = width / minColumnWidth
if (nominalColumnsCount == 0) {
return 1
}


return min(nominalColumnsCount, maxColumnsCount)

Readability Is Relative

Code readability depends a lot on the programming language, the ecosystem, and, of course, people. There’s no universal way of measuring readability because different development teams are used to different conventions. For example, in Swift, this is one way to count unique elements in an array, leaving only those that occur more than twice:

    array
.reduce(into: [:]) { $0[$1, default: 0] += 1 }
.filter { $0.value > 2 }

Some people consider this code perfectly readable, idiomatic, and clear. But others prefer another approach:

    var occurrences = [String : Int]()
for element in array {
occurrences[element, default: 0] += 1
}
for key in occurences.keys {
if (occurrences[key] ?? 0) < 3 {
occurences.removeValue(forKey: key)
}
}

So it’s not about picking the “right” style, but sticking to one style consistently.

What Is Performance?

Performance is also a notion that most people understand intuitively: It’s the ability for code to run efficiently and use fewer resources. The issue is that it’s not always clear what code is considered efficient.

As an example, consider a table view on a mobile device. The scrolling of table views looks smoothest when the device manages to render at least 60 frames per second. So, you wrote code for your table that you’re happy with and measured a scrolling frame rate on the least-powerful supported device. But now, you think you could make your code even faster. Is it worth the effort? The answer is usually no. Your code already meets requirements and, thus, is efficient enough. In this context, which code can you call performant? Technically, both, because they both meet requirements, but you should stop at your first attempt and don’t waste effort on further improvements.

This brings us to the point that there is no performant or non-performant code. There’s simply an acceptable level of performance, which makes performance also relative.

Readability vs. Performance

Let’s look at three ways of calculating the number of table columns, described above, one more time and put them into the table view context with its 60 fps.

I measured all three options on my device and found that the second one is the fastest. Does it mean we have a winner and should use it, compromising readability? Again, the answer is most probably no. The numbers I get roughly mean that the winning option must run about 1,800 times every second during the table scrolling in order to force my iPhone to skip a single frame. The performance gain is too insignificant to even consider it.

If the scrolling animation is laggy, there’s a good chance that something else is slowing it down. And from my experience, it’s not easy to guess exactly what that something is. For instance, one of the common reasons for unresponsiveness is networking calls, which can be easily moved to a background thread without affecting readability. Always profile your program before jumping to conclusions. Contemporary IDEs are rich in tools that help you find the piece of code slowing down your program.

Mindset Changed with Time

A long time ago, computers were very expensive, but their resources were very limited. That forced people to constantly make their programs faster. However, with time, hardware efficiency grew dramatically, and today we live in times of cheap RAM and powerful CPUs.

Gains in computational resources let us create bigger and more complicated programs. This required more readable programming languages to appear, the languages that aim to be closer to humans than machines.

At the same time, engineers haven’t become cheaper. So, next time you’re happy to gain a millisecond of performance time for a MacBook, also remember the hours spent on deriving a sophisticated algorithm and the time your colleagues will spend on understanding it.

Tools became smarter too. Compilers are able to perform tricky optimizations that often make poor and very apt code run at comparable speeds. Operating systems are also now smart enough to cache your data without your intervention.

All of these factors let us not think about code performance in the vast majority of cases. And if you think you should, start with profiling your program and then focus only on that specific piece of slow code that really needs optimization.

Is Performant Code Necessarily Unreadable?

If you organize your code nicely, I believe even heavily optimized code can still be perfectly readable.

Consider a classical example of multiplying an integer by two. You can use the straightforward approach by writing number * 2. Alternatively, you can use an operation of bit shifting, which is cheaper for CPUs: number &lt;< 1 (or number shl 1 in Kotlin). Given your compiler doesn’t perform this optimization automatically, the latter option might run twice as fast. It can save seconds of end-user time because multiplication by two is a relatively frequent operation. On the other hand, the shifting option can easily confuse other developers.

You can also wrap shifting with a function with a meaningful name, and the intent will stay clear. You can then mark the function as inlinable for the compiler and avoid the penalty for introducing a new method.

    inline fun doubleNumber(number: Int): Int = number shl 1

In more desperate situations, you can at least cover your sophisticated algorithms with comments, and it will be good enough in most cases.

Is Readable Code Unperformant?

Not necessarily. Writing clean, well-organized code will usually result in good-enough efficiency. That is the nature of today’s programming languages and compilers: They can read the intent from your source code and turn it into fast machine code.

Although sometimes you just need to walk that extra mile, e.g., choose wisely between reference and value types, restrict inheritance to avoid dynamic dispatch, plant the inline directive here and there, these measures don't make code less readable. In fact, they do the opposite: They make your source code more idiomatic and help explain why it is what it is.

Conclusion

When it comes to performance, the very first thing that comes to my mind is the renowned quote from Donald Knut: “We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.”

You design and code first, then optimize only if needed. Performing optimizations on a good design is always easier, and good design usually helps produce more performant software. This is especially true over the long term: It’s harder to spoil the program during its maintenance and extension if it’s clean and readable.

Another way of expressing the moral of this article is to cite Kent Beck’s “Make it work, make it right, make it fast.” I would recommend thinking twice before trying to make your code fast. Perhaps, it’s already fast enough.


Shipbook gives you the power to remotely gather, search and analyze your user logs and crashes in the cloud, on a per-user & session basis.