Pythonium

Python, What else ?

When an optimization makes code harder to maintain

Optimizing code is a good thing, and it's part of a developer's daily work. A faster query, less memory usage, fewer API calls, or a process that goes from a few seconds to a few hundred milliseconds: on paper, it's hard to complain.

The problem is that an optimization can also make code much harder to understand.

And once it's in production, it's not always obvious whether the gain is really worth the additional complexity.

Faster code isn't necessarily better code

Let's imagine a fairly simple function that goes through a list of items and performs some processing on each one.

We might start by writing something very readable. Then, after looking at the profiling results, we realize that this function accounts for a significant part of the execution time. So we start optimizing.

We avoid certain function calls, cache results, group several operations together, change the data structure, or replace a loop with a more specialized operation.

The code becomes faster. But it also becomes harder to understand. That's not necessarily a problem if the gain is significant. If a process goes from ten seconds to a few milliseconds, that can easily justify a few extra lines of code.

But if we only gain a few percent at the cost of code that nobody dares to modify anymore, the situation is different.

Optimization can hide the intent of the code

This is probably one of the most common problems. Simple code often makes it immediately clear what it is trying to do. An optimization can introduce several additional steps that are only there to improve performance. And that makes the code much less readable, especially for colleagues who didn't write it.

Let's take a very simple example. Instead of going through a list several times to look for different pieces of information, we might decide to calculate everything in a single pass. On paper, that's a good optimization.

But the code then has to maintain several counters, several conditions, and sometimes several intermediate structures within the same loop. The result is more efficient, but the original intent may be less obvious.

Someone modifying this function two years later will first have to understand why all these things are being calculated at the same time. And if they remove something that seems unnecessary, they can easily remove an optimization they didn't even know existed.

Optimizations can sometimes have side effects

An optimization doesn't always affect performance alone. Adding a cache, for example, introduces a new concept into how the application works.

We now need to know when a value is cached, how long it remains valid, how it is invalidated, and what happens when it is no longer available.

The original code may have been slow but relatively predictable. The new code is faster, but it can now return stale data or behave differently when the cache is empty.

The performance gain therefore comes with a new problem to maintain. That's also one of the reasons why caching often creates more problems than it seems to.

An optimization can also increase the number of edge cases

Optimizations are often very effective for a specific case. The problem comes when the data changes.

A solution optimized to quickly process lists containing a few thousand items can be unnecessarily complex if the application ultimately only handles a few dozen.

Conversely, an optimization that works very well with normal data can behave much less efficiently with particularly large datasets.

We then end up adding conditions: if the list is small, we do this; if it's large, we do that; if the cache contains the value, we use the cache; otherwise, we perform the calculation; if the query exceeds a certain size, we use another method.

The code gradually becomes a collection of special cases, increasing its complexity and size along the way. And every new case then has to be tested, documented, and understood.

An optimization can become technical debt

This is probably the point that's easiest to forget. An optimization isn't necessarily free. It can require more code, more tests, more documentation, and more knowledge to understand how it works.

Every developer who works on the code afterward has to deal with this complexity. If the optimization saves a significant amount of resources, that cost can be perfectly acceptable.

But if it only saves a few milliseconds, it can become a form of technical debt. The problem is no longer performance, but the cost of maintaining the solution. If this optimized code increases the number of bugs, we lose in the end.

You need to know when to go back

An optimization can also become unnecessary. If it no longer provides any significant benefit, removing it can be a good decision. This can sometimes be psychologically difficult. Nobody likes deleting code that was written specifically to solve a performance problem.

But keeping an optimization simply because it was useful at some point isn't necessarily a good reason. Code should evolve with the needs of the application.

And what about beautiful code?

It's not only optimizations that can make code more complex than it needs to be. We can also be tempted to apply an architecture, a pattern, or a concept simply because it's elegant on paper and relevant most of the time.

Well-structured code, several abstractions, a perfectly organized architecture... all of this can give an impression of quality. But it's not necessarily what will make the code easiest to evolve in the real context of the application.

For example, a few months ago, I hesitated to split some code into several files. On paper, it was cleaner and prettier. But in my day-to-day work, I found myself jumping between several files, even though the code was still small enough to fit into a single file. In the end, this separation wasn't really relevant.

A solution can be technically very clean and yet introduce more constraints than benefits. Sometimes, a few simple functions do the job perfectly well where a more sophisticated architecture adds several layers that nobody actually needed.

Beautiful code is therefore a fairly subjective notion. And above all, we shouldn't confuse elegant code with code that is suited to the problem.

Optimization is still a trade-off

Optimizing isn't simply about making a program faster. It often means accepting more complexity in exchange for a performance gain.

This trade-off can be excellent when the gain is significant. It can be much less interesting when the difference is barely measurable.

The most important thing is therefore to measure before optimizing, and then keep checking that the optimization is still providing some benefit. Because code that saves a few milliseconds but takes several hours to understand isn't necessarily an optimization. Sometimes, it's simply a problem we've moved somewhere else.