In the game development industry (and maybe in software in general) there lives a very convenient idea that many people use to justify extra layers of logic and abstraction. It goes something like this: «I want the very process of making games to be simpler, more pleasant, and more productive, and I'm willing to sacrifice 10% of performance for that» — Tim Sweeney.
The argument is a good one: for the small price of pretty abstractions, patterns, and language features, it promises to let a programmer produce "bad" code faster — bad in the sense of slow, but code that does the same useful work as the alternative good-but-unreadable, hard-to-follow version. For the business this is more profitable, though in my experience the business couldn't care less about the number of lines of code written by some particular John, Ivan, or Vapur, its complexity, or the presence of abstractions in it — over there they have completely different metrics of "goodness", even at the expense of the quality of the end result.
Let me clarify right away that by "bad code" I don't mean what the programmer sees in their editor. There, in fact, everything can be beautiful — neat indentation, telling names, abstractions, and documentation. But I'm talking about the real code that actually runs on the player's CPU or GPU, and that turns into framerate drops, into half a minute of level loading, and into fans spinning up into orbit.
This article was born after yet another fit of clean-code-mania at one particular studio, when someone once again spotted and dragged in for everyone the well-known Martin book about the virtues of that very clean-code-ology. But "Clean Code" — the one with a capital letter and print runs in the thousands — is not «clean code» in the everyday sense at all, because Martin invented this brand, this label, and spent several years nurturing it at conferences, tying his name to the concept. It would be more correct to call it "the Mr. Martin style" or "the Uncle Bob style", and then half the arguments about the importance of the book fall away on their own, because you're no longer arguing about abstract "cleanliness" but about a specific set of techniques from a specific author. That discussion, on the technical side, never produced any clear results, because by the third lap of the debate people fell back on rhetoric and equivocation, where «clean» (good, decent) started turning into «Clean» (by the handbook), and back again.
Not all "yogurts" are equally healthy
Here a small correction to the article is in order, on why you shouldn't rush to use the examples from the red book in your own project. When Martin wrote Clean Code (2008), he had already spent many years not only in development but also in consulting teams through Object Mentor, which taught extreme programming, object-oriented approaches, and agile ideas. So the book grew out of experience working with large business systems and teams, where the main problem was usually the difficulty of changing and maintaining code, not the cost of every microsecond-long stretch of execution.
You have to understand that Clean Code is not a book about optimizing program execution, and its main domain is the maintainability of large systems, where the main cost often comes not from an extra function call or a cache miss but from the difficulty of changing the code years later. For game engines and realtime systems these ideas become an extra layer of abstraction and can land not only in the cost of maintenance but also in the cost of every development iteration and every frame.
Let's start with the fact that the main interface through which a programmer writes code is very narrow and inconvenient — you have to type letters, lots of letters, a great many letters. Just to describe a small action, like printing HelloWorld to the console. The interface through which we write code is tightly bound to psychology, to the ability to reason about and express computational effects, and it doesn't suit everyone, and most of the existing interfaces are equally inconvenient for a human — for example because writing on paper, drawing, or speaking is more natural than expressing a thought through a limited set of letters, plus signs, and ampersands.
But that's if you look at it from the human's side, because there's another side where things are also bad, and any interface is equally inefficiently implemented on hardware, simply because it's an interface. And the inefficiency of the hardware creeps back into the code.
Let's take something completely down-to-earth: iterating over an array of objects, say, and trying to sum up some field. The «clean» version almost always looks innocent — a vector of shared_ptr, and a virtual method, a convenient hierarchy, or a graph of types — but the problem doesn't begin where the code becomes clean. The problem begins where abstraction becomes the goal rather than the tool.
// "convenient", but each object is a separate allocation
for (Entity* e : scene.getAllEntities())
e->getTransform()->update(dt); // virtual call + virtual call
// + a jump across the heap
It looks pretty, it's easy to read, easy to test, but... But each object lies in its own region of memory, the cache misses, the virtual call keeps the compiler from inlining and, at the same time, from vectorizing the loop, and if there are ten different types in the hierarchy, then the BPU (Branch Prediction Unit) doesn't help much either.
The second version — the one without hierarchies and interfaces — just holds an array of structs (or, better still, an array of separate fields, structure of arrays) and a flat loop that the compiler unrolls and vectorizes on its own, without hints. This version is "dirtier" in Martin's sense, harder for a newcomer to read, and fits worse into the pretty class hierarchy from the textbook, but it's easier to profile, because there's nothing in it to hide behind, easier to parallelize, because the data lies close together, and it's many times faster on the hardware. This isn't a sacrifice for the sake of speed, it's simply a different way of thinking about data — not as objects with behavior, but as arrays of data that need to be run quickly through the processor, which is what all of this was anyway, until we tried to prettify and "clean up" the whole thing.
// tight pass over the array: nice to read and nice to the cache
for (size_t i = 0; i < count; ++i)
positions[i] += velocities[i] * dt; // SoA, predictable access
And so when we get down to measuring fps, measuring memory usage, or profiling on real hardware, the number of serious «Clean Code» proponents drops by half, and all the claims that we're trading CPU cycles for programmer productivity remain just claims.
But let's suppose we really do find ourselves in a situation where we have to trade performance for the speed of writing code. Let's even assume we have tools that are known for a fact to do exactly that — but even in this case, ideal for them, choosing the programmer's comfort is almost always unwise. The main mistake that breaks the whole «speed of writing at the expense of speed of execution» argument is building the wrong ecosystem from the very start.
I wrote you, and I'll run you too
The implicit model this all rests on looks like this: we have one producer (who writes the code) and one consumer (who runs it). The producer suffers over the keyboard, the consumer enjoys the result. Beautiful! Except in game development that's not the case at all.
In reality the producer, i.e. the programmer, runs the code first and most often: during development, debugging, profiling, and playtests, and only after countless runs does the build reach the player. A developer's game loop looks roughly like this:
DEVELOPER ITERATION
edit code / asset
compilation (bad architecture -> +2 minutes on an incremental build)
launch editor / cooking assets
click through to the needed scene
look at the result for 10 seconds
-> notice something's off
-> repeat N times
And this is exactly where bad execution time hits the author's own productivity, and part of the «gain» from writing the code "cleaner" is immediately eaten up by waiting. While the project builds, while the level loads, while you get to the bug at the tenth minute of gameplay because there's no quicksave at that point and the load takes half a minute, while... while... while...
To keep justifying approaches that speed up the generation of bad code, you have to prove that the worsening of the iteration loop doesn't outweigh the gain in writing speed. And yet code is written once, but run, debugged, and tweaked hundreds and thousands of times, and every extra second of compilation and every editor that dropped to 20 fps get multiplied by that number. Think about compiling your own project: you added <iostream> somewhere, it added almost a whole !second! to your compilation, you build this code 60 times a day, a minute of time just went nowhere, and that's just one header file, but there are hundreds of them in a project, and each with its own side effect. That second is critical (I'll explain below), but nobody counts it until the time starts tipping over into minutes, tens of minutes, or hours. Hours of waiting.
Slowdowns spread
The examples and problems in Clean Code are first and foremost about applied business software — accounting systems, business logic, applications where the main difficulty is changing requirements and keeping the codebase understandable. These are important problems too, but realtime development adds one more layer on top of them, where the code must not only be understandable to a human — it must fit into a strict budget of processor time, memory, and team iterations.
I happen to make an engine, and to make an engine well and to keep the designer from cursing about a level that loads for five minutes, I need a profiler, a debugger, a version control system, and an editor. The profiler is made for me by another programmer, and for him to make a profiler he needs... an engine, on which he'll test that the profiler catches anything at all.
And here, instead of abstract «producer» and «client», there's me, writing the engine, and to my right sits some John-or-Ivan writing a tool without which I can't write the engine properly. And there are a great many such connections everywhere inside a studio: gameplay programmers depend on tools, tools depend on build engineers, they depend on the renderer that the render programmers are working on, who need the gameplay programmers' tools to test with, and the whole industry as a whole sits on someone's libs, engines, libraries, and SDKs.
In this context, if my software is slower than it could be, then it slows down not only me. It slows down everyone who depends on me, cuts the number of their iterations, and I in turn depend on their software. I started out just slightly worsening my engine, and the effect begins to creep across the network of dependencies and, since the graph is cyclic, it came back to me. I dropped my own productivity through three handshakes.
Slowdowns accumulate
And this is still the optimistic picture, because the real graph of a game development ecosystem is a huge cyclic dependency through independent participants: studios, middleware, engines, plugins, asset stores, toolchains, platform SDKs. Everyone depends on everyone.
Bad performance introduced into any node of this network spreads across the whole network, because "depending on software" in our swamp literally means "running it". The running time of a dependency enters, one way or another, into the cost of launching a level, and through it into the cost of the work. Sometimes directly in runtime, through a laggy physics library and a couple of milliseconds per frame; sometimes in developer overhead, when a laggy editor eats up an hour a day from everyone on a hundred-person team.
A figure from a real, active project: an artist on average makes 40 asset generations a day — not drawing from scratch, but changing something and updating/checking it in the game; I took only the asset import, without launching the editor and viewing it on an actual level.
fast asset import: 0.2 s -> slow: 8 s
× 40 asset generations per day
× 40 artists
= +4 hours of waiting per day
Either the program is blocked or the human is, but there's a price in both cases, it's just that in the second case it's harder to see in the profiler, because it hides in cups of coffee and smoked cigarettes. And the longer the dependency chain, the higher the chance that the effect will multiply, duplicate, and come back around, and the smaller the chance that anyone will even figure out where the root of the problem is. So there you have the real costs of applying clean code: you could have hired another designer or artist, but that money went to paying the interest on the project's "tech debt".
«Why is the editor lagging?» — well, because a plugin pokes a manager that under the hood allocates strings on every tick, because that was the "cleaner" way to write it three years ago in a library that nobody has opened since, and this, by the way, perfectly explains the state of modern games and tools.
Despite the monstrous growth of hardware power, most projects launch, load, and iterate noticeably worse than their counterparts from ten or fifteen years ago, which ran on machines tens of times weaker. Why? Because degradation in such a network doesn't stop at a single edge of the graph, it gets multiplied at every junction.
Slowdowns slow down everyone
In his book Martin divided time into «domains»: nanoseconds for systems programmers, microseconds for application developers, and seconds for users — meaning that losing a couple of milliseconds makes no difference to the latter. Maybe that works for the bloody enterprise and business systems, but in reality the time domain is a single one. Lost nanoseconds add up into microseconds, those into milliseconds, those into a dropped frame, that into an unplayable scene, that into a release slip.
16.6 ms per frame @ 60 fps
- 4.0 ms render
- 3.0 ms physics
- 2.0 ms animation
- 5.6 ms gameplay logic
--------
= 2.0 ms headroom
// the "beautiful" ECS wrapper layer added 0.0008 ms per entity
// × 30 000 entities = 2.4 ms -> headroom in the red -> frame stalled
Take rendering at 60 fps: that's just 16 milliseconds per frame, and into that budget you need to fit physics, animation, AI, rendering, and sound all at once. If somewhere in the bowels of the particle system there's a «clean» abstraction with a virtual call for every particle, then with ten thousand particles on the scene that's already half a millisecond, three percent of the entire frame budget, spent not on particles but on the beauty of the code. One such class won't crash the game, but in a real project there are hundreds of such "clean" solutions, and the drop piles up not from one place but from a thousand small ones, each of which on its own is justified by readability.
Victims of clean code
Trading software efficiency for programmer efficiency can seem like a profitable deal, as long as someone else, far away from your studio, is paying for it. Methodologies that push the beauty of code in the editor as the highest value bring along with them a noticeable degradation in the quality of games and tools, and here's the irony that shows up: also a drop in the productivity of the very developers for whose sake the whole thing was supposedly started.
I started with Sweeney's quote about 10% of performance for the programmer's convenience, but the trouble is that game development today, all over the place, turns out to be harder, more tedious, and less productive precisely because the system has learned to justify mediocrity with pretty words. Clean code first devours speed for the sake of the developer's convenience, and then takes away both the project's speed and the convenience of development.
So the next time someone brings you a red book about something clean and noble, throw a profiler at the bearer of the gift — maybe they'll come to their senses. Code, of course, shouldn't be dirty, but cleanliness for cleanliness's sake turns into just another kind of technical debt, and ideas, however beautiful they may be, still have to pass the test of real hardware, build time, and the amount of swearing from the designers.
P.S. Here they also rail against clean code and Here
P.P.S. I also started a site on github pages, where I've gathered some of the articles related to development and games (https://dalerank.github.io/). The articles are translated into English and partially into Italian, for which special thanks to my colleagues and our localization department.
← All articles