Overbyte Blog

How Smart Programmers Write Stupid Code

Posted by on in Programming

This is article is based on the presentation I did at GCAP 2013. You can find the slides in Keynote and Powerpoint formats here. The slides include extensive presenter notes, so you should be able to understand what I was getting at even though the slides are purposely light on content.

I've spent a significant part of my professional career working with other people’s code. I've worked with code from big studios and small ones, from successful ones to struggling ones, with experienced devs and green. I’ve seen code that is beautiful, code that inspires as well as code that makes no sense, code that stinks, code that is impossible. I've seen code you wouldn't believe. 

tears_in_rain.jpg

But all the code I’ve seen, both good and bad, has one thing in common - it was written the way it is for a reason. Part of my job, and the job of any code maintainer, is to find that reason and understand why the code was written in that particular way. 

When confronted with new, complex code, there are 4 stages that a programmer goes through:

  • [horror] WTF is this?
  • [confusion] How did this ever work?
  • [blame] I hate you and I wish you would die.
  • [enlightenment] Ahh, I get it now.

The forth stage is the important one. Enlightenment. Comprehension. When you finally understand the code completely and it becomes your own. All too often if the function of some code isn't obvious, then it is often dismissed as being stupid - but this is rarely the case. The code in question isn't necessarily stupid, but it is obscure. Complex. Some of the smartest people I know are programmers, yet they can write some bloody awful code. Stupid coders don’t last long in this industry and their code doesn't last much longer than they do. So why do smart programmers write stupid code?

There are two ways of constructing a software design: 
One way is to make it so simple that there are obviously no deficiencies
and the other way is to make it so complicated that there are no obvious deficiencies.

 — C.A.R. Hoare, The 1980 ACM Turing Award Lecture

Time Constraints

Time constraints have a significant impact on the complexity of our codebases. There are two types of time constraints: the time available to write the code in the first place as well as the time available for the code to complete execution in. 

b2ap3_thumbnail_Dali-Clock.jpg

Time to get the code running

The first constraint is a very common one - game devs are time poor. We generally have only just enough time to get the code working in the first place - who has time to revisit their code just to clean it up? We've got a game to ship, right? Personally, I’ve seen some awful game engines ship great games - and conversely, great engines that have never shipped a game. So, back to my point; given a limit in time to develop the code, what is often produced isn't always the cleanest or simplest solution. When that code is revisited for bug fixing or some other modification, its opacity can mean that it won’t be touched - rather it will be worked around or removed completely as the new coder may not understand why or how it has been done.

Time for the code to run in.

The second constraint is one we've all seen too. A portion of code was recognised to be a bottleneck and so it has been optimised into something unrecognisable. In its optimised state it is difficult to debug, difficult to add to, difficult to understand. 

Both these forms of time constraint can result in complex, hard to maintain code. ‘Stupid’ code. Code that wasn't necessarily written by stupid people, but taken at face value is at best, obtuse.

Code Entropy

Code is like a thermodynamic system. Over time it tends to a state of disorder. In fact, given enough time it tends to a state of perfect internal disorder (which is a very apt description of most shipped games’ codebases). Disorder within a codebase manifests as complexity (and instability) which, if not actively resisted, increases over time. 

b2ap3_thumbnail_Code-Entropy.png

There are a couple of factors which contribute to this increase of complexity;

Problem Domain Evolution

The problem you end up solving is rarely the one you set out to solve.

Consider a programmer who sets out to solve a problem. She settles on an appropriate approach an initial solution is implemented. At a later date the initial problem is deemed to be incorrect or requires incremental modifications and so you she has to readdress the problem. The new direction is similar enough to the old one that it makes sense to simply modify the existing code - so she slowly twists the original solution to fit the new problem. It works, it’s a little ugly, but it works. If this happens enough times, the final problem can be significantly different to the original one and the initial design may not be as appropriate any more. Or even remotely appropriate. But it works. 

b2ap3_thumbnail_evolution.jpg

 

The issue here is the evolution of the code. The programmer has designed a clean system to solve the given problem and that problem has changed so she needed to modify, or dirty, the system. Edge cases and singularities can pop up which can dirty the implementation as well.

So what you have is an evolving system which gets more and more complicated as the problem domain changes. This is even more evident over longer periods of time leaving you with dead code and data. As new programmers are swapped in and out to iterate on the code things get progressively worse.

 Solution Evolution

Iterating without readdressing the initial design convolutes the eventual solution.

A programmer starts with a given problem. She designs a system to solve this problem and then implements that solution and tests it to see if it solves the problem. If it does, then the problem is solved, the code is checked in and beer is consumed in a celebratory manner. If not, then the implementation is revisited and iterated upon until a working solution has been achieved (possibly while everyone else is at the pub having beers). 

FlowChart.jpg

 

The issue here is that the initial design may not have been the most appropriate for this problem - the programmer learns more about the problem as she implements and iterates on it, but rarely revisits the initial approach to the problem. The more work that is put into a particular approach, the less likely the programmer is to change it. The ideal time to design the code for this problem is once she’s finished implementing and testing it.

What we end up with is sort of like DNA - there is a working solution there, but there is also a lot of cruft left in there from other iterations which has no effect on execution (except for when it does). It’s hard to tell what is part of the core solution and what is cruft - so what happens is nothing is touched when changes need to be made. New code is added to the periphery of the solution, often duplicating what is already in that original solution.

Complexity has momentum

The problem with complexity is that it breeds more complexity. The more complex a solution is, the more complex modifications to it become, either through necessity (the complex implementation requires complex changes) or convenience (it’s too hard to understand so its easier to tack something onto the existing solution or even to rewrite what is there). 

 

Controlling complexity is the essence of computer programming.
    — Brian Kernighan 

 

It takes a real effort to simplify a complex implementation - a coder needs to completely understand the existing solution before she can simplify it. The urge to rewrite a complex system is strong, but unless you fully understand the existing implementation you run the risk of making the same mistakes the original programmer(s) did.

 

Any intelligent fool can make things bigger, more complex, and more violent.
It takes a touch of genius — and a lot of courage — to move in the opposite direction.

 -Einstein

 

In closing

My recommendations are this; Treat existing code and previous coders with respect. In all likelihood it is the way it is for a valid reason. Once you understand why the code was written the way it was, you are then in a position to modify, simplify or optimise it - and when you do, leave the code in a state which is better than when you found it. Your future self with thank you.

Where possible, take the time to refactor your code once it is working. Simplify it, clean it, make it beautiful. Beautiful code is its own reward. 

 

0

I've been a professional game developer since 2000, specialising in the hard core, low level, highly technical programming that is required to produce games that keep getting bigger and better. I love writing well specified, high performance code and rebuilding existing systems to function at the highest levels of performance. I take pride in understanding how the hardware works at the lowest levels so that I can eke out the best performance at the higher levels.

Comments

  • John Sietsma
    John Sietsma Sunday, 24 November 2013

    Coders often get their sense of indentity from being smart. And ego can mean the slamming other people's code. I really enjoyed hearing you give a fuller picture.

    I remember Stroustrup being interviewed when Java was fresh. I can't find the source, but to paraphrase, when he was challenged about Java's simplicity as compared to C++, he said that by the time Java becomes useful it will no longer be simple. And he was right!

  • Tony Albrecht
    Tony Albrecht Thursday, 28 November 2013

    I'm glad you liked the article John. It's all to easy to criticise other people's code, yet all too hard to write code which is beyond criticism yourself.

  • Guest
    Aaron Winterhoff Wednesday, 27 November 2013

    Thanks, this is a great article. At the risk of generalising a few parts of your post, I feel there is a question that arises. The programmer often solves a needed problem in an efficient way, and then has to adjust it, leading to complexity. This bloat makes it harder to modify, which then compounds when more tweaks are needed.

    Is there a way to approach writing your code with the idea of flexibility and tweaks? It seems that in the pursuit of really efficient code, and not spending excess amounts of time on a single component, that this approach would be difficult to take as well.

    Do you have a methodology when it comes to designing systems, that takes into account where you might have to make changes + additions, without adding significant complexity or additional development time for something that mightn't get touched again?

  • Tony Albrecht
    Tony Albrecht Thursday, 28 November 2013

    Thanks Aaron. I find that just being aware of the fact that the code you are writing is very likely to change means that I'm more careful with what I implement and how I comment it. Keep it as simple as possible, solve just the problem you have - only solve the problem that you will have when you actually have it. If the system that you have to modify is already simple, then it should be relatively easy to modify, right?

    I think an ideal way to design a system is as a set of small, simple, independent parts that you can use independently. Sort of like unix with sort, find, cut, etc. They each do one thing very well, yet can be used together to do very complex things.

  • Guest
    Marak Wednesday, 11 December 2013

    Great read, as a new programmer its nice to see that its not just me coding like that (in reference to the increasing complex solutions to problems).

    Hell im working on a game in my free time atm and im currently on my 4th rebuild of the core mechanics, each time becoming a little cleaner and a lot faster.

Leave your comment

Guest Saturday, 27 May 2017

Serious. Game. Performance.