If you came here for some friendly media criticism then, boy howdy, is this absolutely fucking not the post for you.
I’m a software developer by trade, and also the kind of person who watches technical talks on YouTube in my own time, so the dread beast Algorithm will occasionally send a developer talk my way. Often these are part of a conference, but sometimes they’re created specifically for YouTube. A lot of these are actually quite good! For example, this talk by Ben Deane, which introduced me to some features of C++ 17 that I didn’t know much about, blew my mind a little with how they can be applied, and actually gave me a whole new way of thinking about maintainability problems!
Some of them are less good. For instance, there’s this guy, Coach Joe, who’s been recommended to me a few times, who makes videos about coding pitfalls and how to avoid them. These are a bit frustrating; often there’s a very real truth at the heart of his advice (that returning null from a function is problematic), but then he over-extends it out into a hard rule (NEVER return Null from a function) or suggests solutions that won’t help in a lot of cases (if you throw an exception instead of returning null, you’ve now recreated the calling code’s null check in the form of exception handling!). I don’t hate the man, but his work is annoying.
Well, today YouTube decided to ruin by morning by serving up a talk that not only did I disagree with, but that I found absolutely enraging. I was actually, genuinely angry. Not at how wrong this man was, but how sanctimonious he was in his wrongness.
This is that talk:
The inflammatory title raised an eyebrow, but hey, maybe this guy (Casey Muratori) knows something I don’t. Maybe he has something interesting to share besides the incredibly obvious fact that there’s a tradeoff between a piece code’s human-readability and its performance.
Reader, he did not have anything interesting to share.
I’ll spare you needing to watch all twenty-two minutes of a man spouting his own ignorance by summarizing. He identifies five central tenets of ‘Clean Code’, which are apparently passed down to rookie developers as gospel:
- Polymorphism is good. If/Switch statements are bad
- You should never know the internals of an object you’re working on
- Functions should be small
- Functions should do one thing
- Don’t repeat code if you can avoid it
He then writes a piece of toy software; an abstract class called Shape, which has a virtual function for computing the shape’s area, and then a set of concrete classes (Square, Rectangle, Triangle, etc) which inherit from this and implement the area function. He chose this example because, in his words, this is the canonical example Clean Code advocates use when demonstrating their principles. He then runs this code over and over to get a measurement of how long it takes to run. Then he rewrites it in such a way that it breaks the first four rules of Clean Code (he says the last one is fine, actually) in order to be more optimized. Then he runs his test again after breaking each rule, and brags about how much faster his version is. Finally, he concludes by saying that since he’s demonstrated that following the rules of Clean Code makes your code AT LEAST!!! fifteen times slower, you should never write Clean Code.
So, there’s a couple obvious problems with this right away, before we ever get into specifics.
The first and perhaps most obvious one is that no human has ever claimed that you should write Clean Code because it will make your code run faster. That’s…not a thing. Clean Code is about making code that is easier for humans to read and easier for humans to maintain. Using speed as the point of comparison is straight-up committing a category error. It’s like saying one plus one equals fish. So right away, the whole premise of this talk is just orthogonal to its subject matter.
In fact, it’s the opposite; while many Clean Code principles have no performance cost associated with them, there is, in terms of design, a tradeoff to be made between how easy it is for humans to read your code, and how optimally it will perform in the processor. This is because humans don’t think like a computer! The more plain something is to a human reader, the more abstraction has been applied to it to move it away from the instructions that will actually run on the machine. To make it something that humans can keep in their brain, concessions need to be made. This is not something that any Clean Code advocate that I know of denies.
The second is that this guy’s idea of what Clean Code entails is…pretty bizarre. That first rule in particular stands out; this is the first time I’ve ever heard of Clean Code as demanding maximum polymorphism at all times. Again, that’s just not a thing. But hey, I don’t know every single Clean Code guy out there. Maybe I’m missing something. So, let’s have a look around!
To the extent that you can call Clean Code a movement, it originates with a book published in 2008: Clean Code: A Handbook of Agile Software Craftsmanship, by Robert Martin. This is part of a series of books by Martin (affectionately known in the community as Uncle Bob) detailing advice and guidelines for software development, from code structure to team organization, based on his decades of practical experience. I’ve read…most of this book, I never actually finished it and I can’t find my copy. This is an extremely influential book; so much so that if you search Wikipedia for ‘Clean Code’, it will redirect to its page on Robert Martin.
So, what does Uncle Bob have to say about polymorphism? Well, he does have a chapter on Object Oriented Programming (OOP), of which polymorphism is a feature. But only one chapter, and it’s about halfway through the book. Most of the guidelines laid out in the Clean Code book have nothing to do with polymorphism at all, actually; they apply equally to object-oriented code as it does to procedural code. To a certain extent OOP does pervade the book through its coding examples, but that’s not because OOP is intrinsic to the concept, it’s because the examples are written in fucking Java, which was a very popular language at the time, and good fucking luck writing Java code of any complexity at all without at least dabbling in OOP.
So, this piece of advice doesn’t originate with Uncle Bob. But let’s let the Google machine take us further afield, in case there have been more recent developments. I stick ‘Clean Code’ into Google, scroll past all the links trying to sell me Uncle Bob’s book, and I’ve picked out a selection of blog posts on Clean Code, each of them laying out its principles:
- A Few Principles of Clean Code
- Clean Code
- Introduction to Clean Code and Software Design Principles
- Clean code: what makes programming code clean
- 10 Tips for Writing Clean Code
- Clean Code Explained – A Practical Introduction to Clean Coding for Beginners
If you will, give those a quick perusal. See if you can spot anything that suggests using polymorphism religiously, or in place of conditional statements. You won’t.
The absolute funniest thing I found during this search was this Udemy course someone was selling for $120 Canadian about writing clean code. I’m not spending that kind of money to peruse it, but quickly looking over the table of contents, you can see a video in the introductory chapter called ‘Functional, OOP, Procedural: The Course Concepts Always Apply’. And then if you scroll down to the chapter on Objects, Classes, and Data Containers, there’s a short video titled ‘Important: This is NOT an OOP or “Patterns & Principles” Course!’.
So, maybe there’s some sort of Clean Code advocates out there who are really jazzed up about polymorphism. But if there are, I can’t find them, and the guys trying to get your dollars to teach you Clean Code seem eager to disavow it. It’s almost like Casey doesn’t know what the fuck he’s talking about. The generous interpretation is that he’s conflating advocacy of Object-Oriented Programming in general with Clean Code? Which is not a totally crazy mistake to make, the sequel to the Clean Code book is Uncle Bob’s guide to Object-oriented design, and People who like their code nice and clean will tend to be drawn towards Object-Oriented designs, which have an attractive elegance to them. But I mean, if you’re going to make a twenty-minute YouTube video raving about the evils of something, I think it behooves you to have at least a basic idea of what that thing actually is, you know?
But wait, there’s more! He calls his toy program, with the Shapes and the inherited Rectangles and Triangles, the ‘canonical example’ of Clean Code using polymorphism. He says this at multiple points in the video, actually. This is the example he’s using, because it’s the example Clean Code advocates use. He’s very clear about that. Except…what the fuck are you talking about, Casey? Nobody advocates for this design, either in the Object Oriented world or among Clean Code enthusiasts. This isn’t the canonical example of either; this is the example you use when introducing a student to the concept of inheritance for the first time. It’s a simple example of an Is-A relationship that’s easy for a neophyte to get their heads around. In fact, there’s been a lot of ink spilled about how this kind of design is kind of bad, for extensibility reasons, and how you should in general prefer Composition methods over the Inheritance Casey uses here to achieve polymorphism.
So, Casey is assessing Clean Code by a metric that doesn’t make sense, and also seems (to be generous) deeply confused about what Clean Code is. So right away, before you even get into the actual meat of the talk, you can already tell that it’s completely worthless. But let’s be good sports! Let’s assess Casey’s claims on their own terms, ignoring the fact that we already know that those terms are complete and utter nonsense.
And…well, that’s really all there is to the video, actually. Casey takes his toy, and then changes it to be less readable and more difficult to maintain (and I’ll get into a bit of detail about that in a moment, don’t you worry) but also run faster, and then shows that his version runs faster. With all the changes in, fifteen times faster, actually! And that’s basically the whole argument. Look, it’s fast! Don’t write clean code!
There are only actually two times when Casey addresses the idea of readability or maintainability in any way. The first is when he’s talking about how he got the idea for his lookup table for the area coefficients for the different kinds of shapes:
This is actually one of the reasons that — unlike “clean” code advocates — I think switch statements are great! They make this kind of pattern very easy to see. When your code is organized by operation, rather than by type, it’s straightforward to observe and pull out common patterns. By contrast, if you were to look back at the class version, you would probably never notice this kind of pattern because not only is there a lot more boilerplate in the way, but “clean” code advocates recommend putting each class in a separate file, making it even less likely you’ll ever notice something like this.
This is, to be gentle to it, complete and total fucking gibberish. These are the ravings of a madman. Before the advent of the internet you needed to go to an opium den to hear sincerely-expressed thoughts that were half this garbled.
To begin with, putting every class into its own file is not one of the tenets of Clean Code. Uncle Bob’s book is totally silent on the question of how your code should be organized in files, as far as I can tell. And yes, it is true that in OOP, it’s common practice to give each class its own file. But that’s because most classes in OOP are a full module of code unto themselves. It’s actually very common to group a bunch of minimally distinct classes like his Shapes, which are more of a module collectively, into one file.
But also! If they had split these up and made it more difficult for him to spot this pattern, they would actually have been doing him a favor, because they would have been preventing him from spotting this completely fake pattern. It is true that the shapes he’s implemented here, the square, the rectangle, the triangle, and the circle, do have similar-looking area formula: you take the width, multiply it by the height, and then multiply that by some number. So what he does is, he stores the width and the height in the Shape objects, as well as the type of shape it is, and then has an array of that extra coefficient. When the Area function is called, it looks up which coefficient to use based on the Shape’s type, and then multiplies that by the Shape’s Height and Width, and returns the results. Easy-peasy.
Observant readers may actually have already noticed a problem here. For all four types of shapes, the two values to be multiplied together are called Height and Width. This is a little dicey with the Triangle; the generally used nomenclature for describing the dimensions of a triangle is Base and Height, not Width and Height, after all. But, whatever, there’s a fairly obvious mapping from Base to Width, this isn’t a real concern. The actual problem is the Circle. The area of a circle is of course pi times the radius of the circle squared. So what we’ve done here is map the radius onto both the width and the height, and then pi is the coefficient. Everything gets multiplied together, hunky-dory. Except! The radius of a circle does not mean the same thing as the width or height of a rectangle! It is in fact half of that. So, just for circles, we’re storing half of the value you would expect in each of those variables, based on its name.
Can we solve this by storing the diameter in the Width and Height instead? Sure, we just need to make the coefficient pi over four instead of just pi. But this doesn’t actually solve the readability problem we’ve introduced. Either way, we’ve introduced a landmine for future maintainers of this code. In the code as Casey’s written it, you’ve got variables that aren’t actually holding the data the variable name says they are; in my version, you’ve got this inexplicable pi over four, which doesn’t correspond to the actual math, and requires a great deal more code interpretation to make sense of.
‘Future maintainers’ is important to bear in mind here; it’s likely that the next person who has to touch your code will be someone laying eyes on it for the first time. And even if you are the one who works on it next, you’ll probably have forgotten the details by that time and need to refresh yourself. That’s why Clean Code is desirable; it makes reading and understanding the code so you can work on it as painless a process as possible.
Now, is this particular example actually very difficult to grasp? No, it would only take a bit of thought to piece together the original intent. But that’s because this is an incredibly simple example that’s based on basic mathematical formulas we had drilled into us during childhood! Most of the problems that you’ll face in real-world coding are not so lucky. Making your intent legible in the code is incredibly important!
But actually this is only the problem that’s sitting there in the code as it’s already written. The bigger problem comes when your boss walks into the room and says: “Hey, you know how you’ve got your shapes? Well, marketing wants us to be able to do trapezoids as well.” The pattern Casey detected and based his code around? Completely fake. Only applied by coincidence because of the specific shapes he chose for his implementation. And now his solution doesn’t work at all for the new Shape, and he either needs to carve out another weird exception, add another readability cul-de-sac, or throw out his existing solution altogether.
These are not insurmountable problems in the example he’s provided. But again, this is an incredibly simple example! It’s less than a hundred lines of code! It can all fit on the screen at the same time! This is actually a fairly shocking number of maintainability problems for a program of this size and complexity!
But don’t worry. He’s got an explanation as to why he’s written this absolutely dogshit code. You see, it turns out that maintainability is a totally fake concern in the face of something like performance!
The “clean” code rules were developed because someone thought they would produce more maintainable codebases. Even if that were true, you’d have to ask, “At what cost?”
It simply cannot be the case that we’re willing to give up a decade or more of hardware performance just to make programmers’ lives a little bit easier. Our job is to write programs that run well on the hardware that we are given. If this is how bad these rules cause software to perform, they simply aren’t acceptable.
You know what, Casey? You’re right. We should be developing the fastest programs we possibly can. And that’s why I have to ask you, Casey: why the fuck did you write this in C++? Why any C-based language at all? All programming should be done in Assembly, so that it can be optimized to the greatest extent possible. The compiler does its best, of course, but it can’t get every possible optimization since it doesn’t actually know what’s important or not for the program. In fact, fuck assembly, let’s code directly in machine code. Just staring at a hex editor all day, that’s the way to do things. That way we can write the fastest code possible! Sure, it’ll be tedious, and agonizingly slow, and almost impossible to read or reason about, but that doesn’t matter! Our job is to write programs that run well on the hardware we’re given, and any loss of performance is unacceptable. Right?
It’s a completely brain-dead way of thinking about software development. Yes, there is some code where performance is very important. And sometimes, to get the performance you need, you have to write ugly code! These are facts of life that are not in dispute by anyone, anywhere. But there is a lot more code where that level of performance doesn’t fucking matter even a little. Who gives a shit about how efficient we are with our clock cycles if, for example, the actual bottleneck is waiting for some data to come in from disk?
And for that vast majority of our code bases, how easy to maintain and change the code is is miles more important than how fast it runs, because that’s the metric that determines how quickly your team can do things. If you’ve written the fastest most performant code in the world, but you can’t implement new features your customers are asking for or fix the bugs they encounter, you’ve fucked up! You have made a bad program and will probably lose their business to someone who wasn’t so fixated on performance! But hey, you’ve got your pride, right?
But again, let’s cede ground to Casey for the purposes of argument. Let’s talk about performance as the be-all and end-all of coding. Let’s totally ignore questions of maintainability and extensibility. Let’s just talk about performance.
Casey is very adamant that the dramatic numbers he’s throwing out are representative of real-world cases:
As you can see, these results [for a version with a second operation] are even worse for the “clean” code. The switch statement version, which was previously only 1.5x faster, is now nearly 2x faster, and the lookup table version is nearly 15x faster.
This demonstrates the even deeper problem with “clean” code: the more complex you make the problem, the more these ideas harm your performance. When you try to scale up “clean” techniques to real objects with many properties, you will suffer these pervasive performance penalties everywhere in your code.
The more you use the “clean” code methodology, the less a compiler is able to see what you’re doing. Everything is in separate translation units, behind virtual function calls, etc. No matter how smart the compiler is, there’s very little it can do with that kind of code.
And to make matters worse, there’s not much you can do with that kind of code either! As I showed before, simple things like pulling values out into a table and removing switch statements are simple to achieve if your codebase is architected around it’s functions. If instead it is architected around it’s types, it is much more difficult — perhaps even impossible without extensive rewrites.
That all sounds very convincing. He added more operations, and the differential got even worse, by a multiplicative factor! Just imagine how much worse it must be in a real system that isn’t just a dumb toy!
Reader, please imagine me leaning in very closely, as I say: Casey. Casey. Casey, you dumb motherfucker. Your code isn’t doing any fucking work! In both the original area function and in the second operation you added, all you’re actually doing is loading in a couple values, multiplying them together, and then returning the result. That’s so small that it’s basically negligible. Meaning: a very disproportionate amount of processor time is being spent on the act of loading up the function being called and then returning to the calling function; the exact operations that are most impacted by the real performance hits inheritance (which, again, is not synonymous with Clean Code, no matter how many times Casey insists that it is!) and similar indirections cause. So, yeah, the performance difference looks massive!
I’m not sure if Casey is deliberately setting his thumb on the scale here, or if he’s just too fucking stupid to realize that that’s what he’s done, but either way, his little toy and his dramatic figures don’t actually show anything meaningful! If the actual function did work that took a measurable fraction of a second, would anybody be able to notice that inheritance added twenty clock cycles to the process? Of course they fucking wouldn’t!
But don’t worry, reader. Casey’s got a real reason for wanting you to write unreadable garbage in search of pointless performance. He’s going to give your lives real meaning:
So out of the five clean code things that actually affect code structure, I would say you have one you might want to think about and four you definitely shouldn’t. Why? Because as you may have noticed, software is extremely slow these days. It performs very, very poorly compared to how fast modern hardware can actually do the things we need our software to do.
If you ask why software is slow, there are several answers. Which one is most dominant depends on the specific development environment and coding methodology.
But for a certain segment of the computing industry, the answer to “why is software so slow” is in large part “because of ‘clean’ code”. The ideas underlying the “clean” code methodology are almost all horrible for performance, and you shouldn’t do them.
This was the point in the talk where I fucking hit the roof. What kind of blinkered, head-assed, cockamamie horse piss is this?
So, first of all: I actually don’t know that software is ‘extremely slow’ these days. I’m not even sure how you would quantify that on a societal scale. And hey, maybe it is slow, whatever that means, and I just haven’t noticed! But if Casey has reason to think it, he isn’t citing his sources either.
But also, hey, let’s assume that software as a whole is slow compared to modern hardware capabilities. Does it follow that it’s because of ‘Clean Code’ practices (which, again, I have to keep stressing, Casey has not remotely accurately described here)? Or is there another, glaringly obvious, explanation?
Throughout his pointless little exercise, Casey keeps trying to illustrate how dreadful these performance losses are by calling back to the days of yore:
So by violating the first rule of clean code — which is one of its central tenants — we are able to drop from 35 cycles per shape to 24 cycles per shape, impling that code following that rule number is 1.5x slower than code that doesn’t. To put that in in hardware terms, it would be like taking an iPhone 14 Pro Max and reducing it to an iPhone 11 Pro Max. It’s three or four years of hardware evolution erased because somebody said to use polymorphism instead of switch statements.
10x is so large a performance increase, it’s not even possible to put it in iPhone terms because iPhone benchmarks don’t go back far enough. If I went all the way back to the iPhone 6, which is the oldest phone still showing up on modern benchmarks, it’s only about three times slower than the iPhone 14 Pro Max. So we can’t even use phones anymore to describe this difference.
If we were to look at single-thread desktop performance, a 10x speed improvement is like going from the average CPU mark today all the way back to the average CPU mark from 2010! The first two rules of the “clean code” concept wipe out 12 years of hardware evolution, all by themselves.
So we’ve gone from a 10x speed difference to a 15x speed difference just be adding one more property to our shapes. That’s like pushing 2023 hardware all the way back to 2008! Instead of erasing 12 years, we’re erasing 14 years just by adding one new parameter to our definition of the problem.
This framing sucks shit!
First of all, it’s totally backwards. The human timescale hasn’t adjusted alongside the increase in processing power, and not all operations have had to scale up as well. If in 2001 a piece of code needed to be an unmaintainable mess to get acceptable performance, and today that same operation can be performed in a similar amount of time, with code that’s clean and readable and extensible, that’s not leaving clock cycles on the table, that’s taking advantage of the increased processing ceiling.
But also! It’s funny that Casey hearkens back to the days of the oughties, because here’s a thing about that: if you were to take a single-threaded application, like Casey’s little shapes toy, and run it on a modern PC, and then compare it with the same program running on a computer from 2008, you probably wouldn’t see much of a performance difference. Actually, the old computer might run it faster!
Why is that? Well, it’s because we stopped making faster processing units around that time. Actually, it’s more accurate to say that we stopped being able to make faster processing units, thanks to our old friend The Laws of Physics. Every operation performed by a CPU generates a little bit of heat, and we were hitting the point where it wasn’t possible for cooling systems that were practical for consumer use to carry the heat away from the CPU fast enough. If we kept making CPUs faster, they would literally start melting themselves when under load.
So, instead, we started putting more processing units into computers! Multiple cores, to carry out multiple operations (or ‘threads’) in parallel. All sharing the same memory bus, so there was no need to worry about keeping that in sync. And in this way, we’ve continued to expand the calculative power at our fingertips! But here’s the thing: in order to take advantage of this extra power, your code needs to actually be doing multiple things at a time. It has to be written to parallelize its operations, to keep all of those processing cores occupied.
And here’s my hypothesis about why code is so slow these days, on the assumption that it actually is: writing good threaded code is fucking hard. Not all operations are easily parallelizable; concurrent code is prone to race conditions and other nasty bugs that are absolute brain-melters to diagnose and fix even under the best conditions; and it’s all just really difficult to reason about in a way that single-threaded code is not. And because doing threading safely requires the use of locks, it also introduces whole new categories of performance killers. Again, who gives a shit about whether an operation is going to take 10 or 30 clock cycles if it then needs to wait a full second for another thread to finish what it’s doing before it can proceed? Not to mention, let’s meet everyone’s good friend, Mr. Deadlock.
But, no, it couldn’t be that. It’s gotta be that Clean Code.
And of course, the fact that all of this is really hard, and difficult to reason about, and inherently laced with the nastiest classes of bugs, all of that is before you get to whether the code is clear, legible, and easy to understand. Imagine debugging an incredibly tight race condition but you also need to keep double-checking your understanding of what the code is supposed to do, because the guy who wrote it let Casey Muratori talk him into making it an unreadable, but performant, mess. This is an argument for Clean Code standards (the real ones, not the ones that exist in Casey’s head), not against!
Casey clearly knows a great deal about the intricacies of processor architectures, and how to make your code run efficiently through it. But the fact that he’s still talking about processor performance in this linear sense, as if there was just one number that kept going up forever and made your code go fast, suggests a hideously blinkered understanding of every other area of software development.
I want to reiterate: there is always a tradeoff between readability and performance. This honestly has very little to do with OOP or Clean Code or whatever, and much more to do with the fact that it’s almost impossible for human beings to think like a computer at any kind of speed. The more abstractions we pile onto the code, the easier it is for us to understand, but each of those abstractions comes at a cost in terms of performance. And it is absolutely true that there is some code that must be blazingly fast, must use all available processing resources, cannot be allowed to be unoptimal. And that code will generally, by necessity, be absolutely filthy, unreadable, hideous, ugly.
But most code does not need to be written like that! Except in Casey’s world, where every wasted clock cycle is a crime.