34

While responding to this question, I began to wonder why so many developers believe a good design should not account for performance because doing so would affect readability and/or maintainability.

I believe that a good design also takes performance into consideration at the time it is written, and that a good developer with a good design can write an efficient program without adversely affecting readability or maintainability.

While I acknowledge that there are extreme cases, Why do many developers insist an efficient program/design will result in poor readability and/or poor maintainability, and consequently that performance should not be a design consideration?

  • 9
    It would be nearly impossible to reason about it in a large scale, but for small pieces of code it is quite obvious. Just compare the readable and the efficient versions of, say, quicksort. – SK-logic Sep 27 '11 at 11:52
  • 7
    Mu. You should start by supporting your statement that many developers insist that efficiency leads to unmaintainability. – Peter Taylor Sep 27 '11 at 11:57
  • 2
    SK-logic: In my opinion that's one of the best parts of all the stackexchange sites, since one gets to question the obvious, which can be healthy every now and then. What might be obvious to you might not be obvious to someone else, and vice versa. :) Sharing is caring. – Andreas Johansson Sep 27 '11 at 12:07
  • 2
    @Justin, no. That thread seems to me to presuppose a situation in which there's a forced choice between efficient code or maintainable code. The questioner doesn't say how frequently he finds himself in that situation, and the answerers don't seem to claim to be in that situation frequently. – Peter Taylor Sep 27 '11 at 13:41
  • 2
    -1 for the question. When I read it I thought this is a straw man to evict the only true answer: "Because they don't use python." – Ingo Sep 27 '11 at 14:49

14 Answers 14

38

I think such views are usually reactions to attempts at premature (micro-)optimization, which is still prevalent, and usually does way more harm than good. When one tries to counter such views, it is easy to fall into - or at least look like - the other extreme.

It is nevertheless true that with the enormous development of hardware resources in recent decades, for most of the programs written today, performance ceased to be a major limiting factor. Of course, one should take into account expected and achievable performance during design phase, in order to identify the cases when performance may be(come) a major issue. And then it is indeed important to design for performance from the beginning. However, overall simplicity, readability and maintainability is still more important. As others noted, performance optimized code is more complex, harder to read and maintain, and more bug-prone than the simplest working solution. Thus any effort spent on optimization must be proven - not just believed - to bring real benefits, while degrading the long term maintainability of the program as little as possible. So a good design isolates the complex, performance intensive parts from the rest of the code, which is kept as simple and clean as possible.

  • 8
    "When one tries to counter such views, it is easy to fall into - or at least look like - the other extreme" I have issues all the time with people thinking I hold the opposite view when I'm merely balancing out the pros with the cons. Not just in programming, in everything. – jhocking Sep 27 '11 at 15:20
  • 1
    I'm so sick of everyone discussing about this that I get angry and take extremes.. – Thomas Bonini Sep 27 '11 at 17:27
  • There have been several good responses, but I think yours made the best attempt at detailing the origins of this mentality. Thanks to everybody involved! – justin Sep 30 '11 at 15:24
  • My answer... most developers are bad at their jobs – TheCatWhisperer Dec 12 '17 at 21:20
38

Coming at your question from the side of a developer who works on high-performance code, there are several things to consider in design.

  • Do not prematurely pessimize. When you have the choice between two designs that are equal in complexity, choose the one that has the best performance characteristics. One of the famous C++ examples is the prevalence of post-increment of counters (or iterators) in loops. This is a totally unnecessary premature pessimization that MAY not cost you anything, but it MIGHT, so don't do it.
  • In many cases you have no business to go anywhere near micro-optimization yet. Algorithmic optimizations are a lower-hanging fruit and are nearly always a lot easier to understand than really low-level optimizations.
  • If and ONLY if performance is absolutely critical, you get down and dirty. Actually, you isolate the code as much as you can first, and THEN you get down and dirty. And it gets really dirty in there, with caching schemes, lazy evaluation, memory layout optimization for caching, blocks of inline intrinsics or assembly, layer-upon-layer of templates, etc. You test and document like crazy here, you know it's going to hurt if you have to do any maintenance in this code, but you have to because performance is absolutely critical. Edit: By the way, I'm not saying this code cannot be beautiful, and it should be made as beautiful as it can be, but it's still going to be very complex and often convoluted compared to less optimized code.

Get it right, get it beautiful, get it fast. In that order.

  • I like the rule of thumb: 'get it beautiful, get it fast. In that order'. I am going to start using that. – Martin York Sep 27 '11 at 14:45
  • Exactly. And isolate the code in the third point, as much as possible. Because when you move to difference hardware, even something as small as a processor with a different cache size, these things may change. – KeithB Sep 27 '11 at 15:45
  • @KeithB - you make a good point, I will add it to my answer. – Joris Timmermans Sep 27 '11 at 16:00
  • +1: "Get it right, get it beautiful, get it fast. In that order." Very nice summary, with which I agree 90%. Sometimes I can only fix certain bugs (get it right) once I get it beautiful (and more understandable). – Giorgio Sep 27 '11 at 17:14
  • +1 for "Do not prematurely pessimize". The advice to avoid premature optimization is not permission to wantonly employ boneheaded algorithms. If you're writing Java, and you have a collection you will be calling contains on a lot, use a HashSet, not an ArrayList. The performance may not matter, but there is no reason not to. Exploit congruences between good design and performance - if processing some collection, try to do everything in a single pass, which will probably be both more readable, and faster (probably). – Tom Anderson Sep 27 '11 at 18:11
16

If I can presume to "borrow" @greengit's nice diagram, and make a small addition:

|
P
E
R
F
O  *               X <- a program as first written
R   * 
M    *
A      *
N        *
C          *  *   *  *  *
E
|
O -- R E A D A B I L I T Y --

We've all been "taught" that there are tradeoff curves. Also, we have all assumed we are such optimal programmers that any given program we write is so tight it is on the curve. If a program is on the curve, any improvement in one dimension necessarily incurs a cost in the other dimension.

In my experience, programs only get near any curve by being tuned, tweaked, hammered, waxed, and in general turned into "code golf". Most programs have plenty of room for improvement in all dimensions. Here's what I mean.

  • Personally I think there is another end to the curve where it goes up again on the right hand side (as long as you move far enough to the right (which probably means re-thinking your algorithm)). – Martin York Sep 27 '11 at 14:49
  • 2
    +1 for "Most programs have plenty of room for improvement in all dimensions." – Steven Sep 27 '11 at 15:01
5

Precisely because highly performing software components are generally orders of magnitude more complex than other software components (all other things being equal).

Even then it is not as clear cut, if performance metrics are a critically important requirement then it is imperative that the design have complexity to accomodate such requirements. The danger is a developer who wastes a sprint on a relatively simple feature trying to squeeze a few extra milliseconds out of his component.

Regardless, complexity of design has a direct correlation with the ability of a developer to quickly learn and become familiar with such a design, and further modifications to functionality in a complex component can result in bugs that might not be caught by unit tests. Complex designs have many more facets and possible test cases to consider making the goal of 100% unit test coverage even more of a pipe dream.

With that being said it should be noted that a poorly performing software component could perform poorly just because it was foolishly written and unnecessarily complex based on the ignorance of the original author, (making 8 database calls to build a single entity when just one would do, completely unnecessary code that results in a single code path regardless, etc...) These cases are more a matter of improving code quality and performance increases happening as a consequence of the refactor and NOT the intended consequence necessarily.

Assuming a well designed component however, it will always be less complex than a similarly well designed component tuned for performance (all other things being equal).

3

It is not so much that those things cannot coexist. The problem is that everyone's code is slow, unreadable, and unmaintainable on the first iteration. The rest of the time is spent working on improving whatever is most important. If that is performance, then go for it. Don't write spitefully awful code, but if it just has to be X fast, then make it X fast. I believe that performance and cleanliness are basically uncorrelated. Performant code does not cause ugly code. However, If you spend your time tuning every bit of code to be fast, guess what you did not spend your time doing? Making your code clean and maintainable.

2
    |
    P
    E
    R
    F
    O  *
    R   * 
    M    *
    A      *
    N        *
    C          *  *   *  *  *
    E
    |
    O -- R E A D A B I L I T Y --

As you can see...

  • Sacrificing readability can increase performance -- but only so much. After a certain point, you have to resort to "real" means like better algorithms and hardware.
  • Also, losing performance at the cost of readability can happen only to some extent. After that, you can make your program as much readable as you want without affecting performance. For example adding more helpful comments doesn't toll performance.

So, performance and readability are but modestly related -- and in most cases, there's no real big incentives preferring the former over latter. And I am talking here about high level languages.

1

In my opinion performance should be a consideration when it's an actual problem (or e.g. a requirement). Not doing so tends to lead to microoptimizations, which might lead to more obfuscated code just to save a few microseconds here and there, which in turn leads to less maintainable and less readable code. Instead one should focus on the real bottlenecks of the system, if needed, and put emphasis on performance there.

1

The point is not readability should always trump efficiency. If you know from the get go that your algorithm needs to be highly efficient, then it will be one of the factors you use to develop it.

The thing is most uses cases don't need blinding fast code. In many cases IO or user interaction causes much more delay then your algorithm execution causes. The point is that you should not go out of your way to make some thing more efficient if you don't know it is the bottle neck.

Optimizing code for performance often makes it more complicated because it generally involves doing things in a clever way, instead of the most intuitive. More complicated code is harder to maintain and harder for other developers to pick-up (both are costs that must be considered). At the same time, compilers are very good at optimizing common cases. It is possible that your attempt to improve a common case means that the compiler does not recognize the pattern anymore and thus can not help you make your code fast. It should be noted that this does not mean write whatever you want without concern to performance. You should not be doing anything that is clearly inefficient.

The point is to not worry about little things that might make things better. Use a profiler and see that 1) what you have now is an issue and 2) what you changed it to was an improvement.

1

I think most programmers get that gut feeling simply because most of the time, performance code is code based on a lot more informations (about the context, hardware knowledge, global architecture) than any other code in applications. Most code will only express some solutions to specific problems that are encapsulated in some abstractions in a modular way (like functions) and that means limiting the knowledge of the context to only what enter that encapsulation (like function parameters).

When you write for high performance, after you fix any algorithmic optilizations, you get into details that requires far more knowledge about the context. That might naturally overwhelm any programmer that don't feel focused enough for the task.

1

Because the cost of global warming (from those extra CPU cycles scaled by hundreds of millions of PCs plus massive data center facilities) and mediocre battery life (on user's mobile devices), as required to run their poorly optimized code, rarely shows up on most programmer's performance or peer reviews.

It's an economic negative externality, similar to a form of ignored pollution. So the cost/benefit ratio of thinking about performance at all is mentally skewed from reality.

Hardware designers have been working hard adding power save and clock scaling features to the latest CPUs. It's up to programmers to let the hardware take advantage of these capabilities more often, by not chewing up every CPU clock cycle available.

ADDED: Back in ancient times, the cost of the one computer was millions, so optimizing CPU time was very important. Then the cost of developing and maintaining the code became greater than the cost of the computers, so optimization fell way out of favor compared with programmer productivity. Now, however, another cost is becoming greater than the cost of computers, the cost of powering and cooling all those data centers is now becoming greater than the cost of all the processors inside.

  • Apart from the question if PCs contributed to global warming, even if it were real: It is a fallacy, that more energy efficiency leads to less energy demand. Almost the opposite is true, as can be seen from the first day a PC appeared on the market. Before that, some hundreds or thousends Mainframe (each one virtually equipped with their own power plant) used much less energy than today, where 1 CPU minute computes much more than then at a fraction of the cost and energy demand. Yet, the total energy demand for computing is higher than before. – Ingo Sep 27 '11 at 14:56
1

I think it's hard to achieve all three. Two I think can be feasible. For example, I think it's possible to achieve efficiency and readability in some cases, but maintainability might be hard with micro-tuned code. The most efficient code on the planet will generally lack both maintainability and readability as is probably obvious to most, unless you're the kind that can understand the hand SoA-vectorized, multithreaded SIMD code that Intel writes with inlined assembly, or the most cutting-edge algorithms used in the industry with 40-page mathematical papers published only 2 months ago and 12 libraries worth of code for one incredibly complex data structure.

Micro-Efficiency

One thing I'd suggest that might be contrary to popular opinion is that the smartest algorithmic code is often more difficult to maintain than the most micro-tuned straightforward algorithm. This idea that scalability improvements yield more bang for the buck over micro-tuned code (ex: cache-friendly access patterns, multithreading, SIMD, etc.) is something I'd challenge, at least having worked in an industry filled with extremely complex data structures and algorithms (the visual FX industry), especially in areas like mesh processing, because the bang might be big but the buck is extremely expensive when you introduce new algorithms and data structures no one has ever heard of before since they're brand new. Further, I've managed to beat such algorithms and data structures with more straightforward code relying on general computer science and computer architecture knowledge, not cutting-edge industrial algorithms published by mathematical wizards, that "theoretically" didn't scale quite as well (ex: linearithmic vs. linear) but were micro-tuned, and my version required only about 1/100th of the code the scalable solution required while being faster for larger inputs (relatively faster the larger the inputs became, making my version even more scalable in practice even though it was less scalable theoretically).

So this idea that algorithmic optimizations always trump, say, optimizations related to memory-access patterns is always something I didn't quite agree with. Of course if you're using a bubble sort, no amount of micro-optimization can help you there... but within reason, I don't think it's always so clear-cut. And arguably algorithmic optimizations are more difficult to maintain than micro-optimizations. I'd find it much easier to maintain, say, Intel's Embree which takes a classic and straightforward BVH algorithm and just micro-tunes the crap out of it than Dreamwork's OpenVDB code for cutting-edge ways of algorithmically accelerating fluid simulation. So in my industry at least, I'd like to see more people familiar with computer architecture micro-optimizing more, as Intel has when they stepped into the scene, as opposed to coming up with thousands and thousands of new algorithms and data structures. With effective micro-optimizations, people could potentially find fewer and fewer reasons to invent new algorithms.

I worked in a legacy codebase before where almost every single user operation had its own unique data structure and algorithm behind it (adding up to hundreds of exotic data structures). And most of them had very skewed performance characteristics, being very narrowly applicable. It would have been so much easier if the system could revolve around a couple dozen more widely-applicable data structures, and I think that could have been the case if they were micro-optimized much better. I mention this case because micro-optimization can potentially improve maintainability tremendously in such a case if it means the difference between hundreds of micro-pessimized data structures that can't even be used safely for strict read-only purposes which involve cache misses left and right vs. just dozens of micro-optimized data structures that perform so much more efficiently all around.

Functional Languages

Meanwhile some of the most maintainable code I've ever encountered was reasonably efficient but extremely hard to read, since they were written in functional languages. In general readability and uber maintainability are conflicting ideas in my opinion.

It is really hard to make code readable, and maintainable, and efficient all at once. Typically you have to compromise a bit in one of those three, if not two, like compromising readability for maintainability, or compromising maintainability for efficiency. It's usually maintainability that suffers when you seek a lot of the other two.

Readability vs. Maintainability

Now as said, I believe readability and maintainability are not harmonious concepts. After all, the most readable code for most of us mortals maps very intuitively to human thought patterns, and human thoughts patterns are inherently error-prone: "If this happens, do this. If that happens, do that. Otherwise do this. Oops, I forgot something! If these systems interact with each other, this should happen so that this system can do this... oh wait, what about that system when this event is triggered?" I forgot the exact quote but someone once said that if Rome was built like software, it would only take a bird landing on a wall to bring it toppling down. Such is the case with most software. It's more fragile than we often care to think. A few lines of seemingly innocuous code here and there could bring it to a halt to the point of making us reconsider the entire design, and high-level languages which aim to be as readable as possible are no exceptions to such human design errors.

Pure functional languages are about as close to invulnerable to this as one can feasibly get (not even close to invulnerable, but relatively much closer than most). And that's partially because they don't map intuitively to human thought. They're not readable. They force thinking patterns upon us which make us have to solve problems with as few special cases as possible using the minimum amount of knowledge possible and without causing any side effects. They're extremely orthogonal, they allow the code to often be changed and changed without surprises so epic that we have to rethink the design on a drawing board, even to the point of changing our minds about the overall design, without rewriting everything. It doesn't seem to get easier to maintain than that... but the code is still very hard to read, and almost necessarily so to be so maintainable (so easy to change without problems).

  • 1
    "Micro-Efficiency" is sort of like saying "There ain't no such thing as O(1) memory access" – Caleth Dec 13 '17 at 10:42
0

One problem is that finite developer time means that whatever you seek to optimise takes away from spending time on the other issues.

There's a rather good experiment done on this referenced in Meyer's Code Complete. Different groups of developers were asked to optimise for speed, memory usage, readability, robustness and so forth. It was found that their projects scored high in whatever they were asked to optimise in, but lower in all the other qualities.

  • Obviously you can devote more time but eventually you begin questioning why developers would take time off programming emacs to express love for their children, and at that point you're basically Sheldon from the Big Bang Theory – deworde Sep 27 '11 at 22:09
0

Because experienced programmers have learned that it's true.

We've worked with code that is lean and mean and doesn't have performance issues.

We've worked on a lot of code that, to address performance issues is VERY complex.

One immediate example that comes to mind is that my last project included 8,192 manually sharded SQL tables. This was needed because of performance issues. The setup to select from 1 table is a lot simpler than to select from and maintain 8,192 shards.

0

There are also some famous pieces of highly optimised code that will bend most peoples brains that support the case that highly optimised code is difficult to read and understand.

Here's the most famous I think. Taken from Quake III Arena and attributed to John Carmak, although I think there's been several iterations of this function and it wasn't originally created by him (isn't Wikipedia great?).

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.