10 ms·
"Clean" Code, Horrible Performance (2023)
- pragmatic 1mo agoWhy did Uncle Boob ever have any credence or credibility? What did Bob ever ship that gives him any gravitas or authority in this area?
- MonstraG 1mo ago(2023)
- inigyou 1mo agoStill true today.
- WesolyKubeczek 1mo agoNobody argues with that. But it's helpful to know right from the title that it's the original Casey's work and not something newer.
- unscaled 1mo agoI don't think it was true even in 2023. This sounds like tackling the problems of C++ in the early 2000s. 1. Casey Muratori also that DRY shouldn't doesn't have to result in non-performant code. 2. Smaller functions, functions that do one-thing: Modern compiler can inline those. There are some edge cases where inlining may make less efficient use of states and loops but I don't think that's a main problem nowadays. I also wouldn't say the extreme version of this idea (very small functions) is still popular. The strongest proponent of this was Uncle Bob, and the last time I've heard him speak about code, he said he now lets the LLM write everything and he only reviews the module hierarchy and maybe the modules' public interfaces. 3. Polymorphism instead of ifs and switches was a big fad in the late 1990s until the late 2000s and had some holdouts in the 2010s. It was only ever popular in the Enterprise Java and C++ world (and maybe in Enterprise Smalltalk, never hard). Overuse of runtime polymorphism widely considered bad form in newer static languages like Go and Rust and in most dynamic languages there was always a tacit understanding of "use mostly conditions, add polymorphism if you need extensibility". In functional languages (or languages heavily influenced by functional programming like Rust, Swift and Kotlin[1]), the classic approach for the type of scenario in this example is to use a sum type, and run a safe exhaustive match/switch on all the variants. 4. Hiding internals: The sum type example is telling of modern best-practices. Sum type fields are generally made public. Some languages (e.g. Rust and most pure functional languages) do not support private fields in sum types at all! Other languages (e.g. Kotlin) but immutable, so it's easy to maintain invariants without hiding information. Sometimes we do want to hide the type details and wrap it with public-facing type (this is a common pattern with internal error enums in Rust for example). Even in this case, there is no impact since we do not use runtime polymorphism or indirection (that would be Box<T> in Rust). Due to compiler optimizations, hiding internals has marginal performance cost (if any) unless you require runtime polymorphism to achieve it. But why should you? I feel like the performance costs lamented in this article mostly have to do with runtime polymorphism in static languages. And I fully agree here: runtime polymorphism is something that should be avoided when you don't need it[2]. But that's the thing: if you're looking at modern static language codebases, runtime polymorphism is not as hyped as it used to be in the past. Some languages still require heavy use of runtime polymorphism (Go is a good example of this), but other languages more often rely on static polymorphism (Rust) or compile time duck-typing (Zig and you could argue C++ template meta-programming used to do that, albeit quite awkwardly). Even with all the issues you get with polymorphism, I don't think it's the main cause of slow application performance. It be very much the culprit in tight loops inside games, but if you look at the performance issues plaguing everyday apps, I think the two major culprits are endless layers of abstraction (the most quintessential example is basically every sluggish Electron app out there) and blocking the user on slow actions (like network loads). --- [1] Even Java had sealed record types for a while now, and I'm sure will see Enterprise frameworks encouraging them in 20 years, when the rest of the world has moved on to spacefaring super-intelligent LLMs. But Enterprise frameworks also don't encourage you to write DRY code or keep your functions short. [2] But do keep in mind that in Java it could be almost zero-cost in many cases. The JIT will monomorphize or bimorphize your classes if you always use the same class at the same callsite. The pointer indirection is not an extra cost, since every non-primitive that doesn't undergo Scalar Replacement[3] lives on the heap, and has a pointer. [3] https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacement/ https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacemen...
- inigyou 1mo agoThis reads like contrarianism to me, like you have to oppose the article because you just do (maybe you dislike Casey). There's plenty of code written the way Casey disagrees with.
- Johanx64 1mo agoI wish we were at the level where some doofus has red too many "Gang of Four" "Design Patterns OOP" bullshit books and gone to town. Because that would be way better than what we have now. Whenever I run a thing and it's unbearabily super duper slow, when you look at the process lists the thing will have spawned bunch of chromium instances - on top of probably making bunch of internet connections. Delegating some of the work that can easily done on my PC to "cloud" instead. What we have now is way worse - it's electron and webshit technologies on desktop. Like you couldn't make software of worse quality even if you tried. The performance way worse than PCs of 1990s. It's almost like using software that's running from a floppy disk. And now this trash is probably getting generated with LLMs.
- deleted 1mo ago[deleted]
- aw1621107 1mo agoRelated: HN post for original article on 2023-02-28 (https://news.ycombinator.com/item?id=34966137 https://news.ycombinator.com/item?id=34966137), 739 points, 914 comments Discussion between Casey (author of this article) and Uncle Bob (author of _Clean Code_, whose programming patterns Casey is critiquing), posted on HN on 2023-03-11 (https://news.ycombinator.com/item?id=35105528 https://news.ycombinator.com/item?id=35105528), 223 points, 213 comments "Horrible Code, Clean Performance", a "homage" to Casey's original article, posted on HN on 2023-04-19 (https://news.ycombinator.com/item?id=35596069 https://news.ycombinator.com/item?id=35596069), 121 points, 114 comments
- flossly 1mo agoI'd say Clean Code is teaching many bad-practices. Too many to be recommended.
- pixlmint 1mo agoeh, when I read it as a newbie it was really helpful. still had to make my own experiences and judgments, but overall I think reading it made me a better programmer
- flossly 1mo agoat bast it makes to better at "Clean(TM) OOP code". programming in general is waaaaaay bigger than what the book covers.
- Jtsummers 1mo ago> programming in general is waaaaaay bigger than what the book covers. It's way bigger than any book covers. Clean Code has some useful things, but if anyone actually reads chapter 1 they'd see that Martin even addresses the idea that you should not just read Clean Code and use it alone, or even entirely. It's a collection of one person's judgements (some good, some bad), just like all the other books like it.
- pixlmint 1mo agoyou realize reading books isn't a zero-sum game right? I can still read more books, it didn't end with Clean Code
- flossly 1mo agoand hence i scope it's message to "clean OOP code"; simply to show that it's flawed message does not even pertain to code in general.
- general1465 1mo ago> Functions should be small + Functions should do one thing This is often a trap for performance. Sure, it looks nice on a screen but calling a function to return a variable is usually epic waste of performance unless compiler will save you by inlining the function into your code or architecture you are using has a magic instruction for that (call vs fcall - which compiler has to recognize and use) which is just fancy "goto there, mov r1 <- *var, goto back"
- jgwil2 1mo agoSee also the more in-depth followup "Simple Code, High Performance (https://www.youtube.com/watch?v=Ge3aKEmZcqY https://www.youtube.com/watch?v=Ge3aKEmZcqY)
- lunar_mycroft 1mo agoActually, that video predates the one on clean code.
- jgwil2 1mo agoI stand corrected. Still I'd recommend it as a followup for anyone intrigued by this post as it shows a real world, non-trivial example of removing abstractions in order to improve performance.
- Aurornis 1mo agoI consider Clean Code to be in the category of books/styles that is helpful for early developers who need some structure, but harmful to late-stage developers who adopt it as dogma. On a long enough career path, eventually you will run into one Clean Code zealot who carries an air of superiority and nit picks every PR over things like a function having more than an arbitrary number of lines in it instead of reviewing the actual code. This is the point where most people come to hate Clean Code.
- tarcon 1mo agoYou mean people come to hate code reviews. If you don't use those rules, you'll argue about something else in the code reviews. Likely something even more ambigous that wasn't explicitly written down for everyone as a baseline.
- locknitpicker 1mo ago[flagged]
- lionkor 1mo agoThere are way better metrics of function complexity, like how many branching points, how many loops, or even just how many levels of indentation. TDD, OOP, Clean Code, etc are an attempt to solve very real problems. They are then applied as dogma to places where these problems are not evident. That's the issue. Of course these rules have their place, but always with a caveat and never applied over all possible places where they might fit. Very often, a better solution exists, as well.
- ryanbrunner 1mo agoFortunately we are humans, and professionally trained humans at that, and we can judge readability and comprehensibility of methods through better measures than whether it crosses a boundary of number of lines. There is absolutely a place for PR reviews, and I don't think the person you were replying to was against that, just that PR reviews would be better by actually judging things like readability directly rather than relying on measures that estimate those qualities. I can think of many times arbitrary rules like linting or Clean Code-esque standards resulted in a "solution" of making my code less readable.
- jayd16 1mo agoOk now add a Path shape that has to calculate the area of a polygon with arbitrary complexity. Consider how the workload is now dominated by the core task of actually calculating the area, reducing the impact of struct usage. Consider the diffs required to make this change. It's not like Clean Code should be taken as gospel but this micro-benchmark is not a realistic example of what CC is trying to solve.
- lunar_mycroft 1mo agoIn that case, you'd branch into a separate function/block that runs the calculation. Sure, it's slower than a simple array index to find a coefficient, but you're only incurring that cost when you actually need it and it's still much faster than using polymorphism everywhere instead.
- coldbrewed 1mo agoThe problem in both of these cases is to how prioritize the complexity of the domain vs. the cognitive overhead of the implementation vs. the computational complexity. If the domain is complex and best represented by modeling the domain, model the domain. If the domain is simple and the the complexity is low, make it simple. If the computational complexity is high and the domain is complex, then all solutions will be bad so minimize the suck in the best way that you know how. Occam's razor applies to all domains. Don't use confusing implementations until there are no good options left.
- josephg 1mo agoI think if you push your craft, most things become this sort of tradeoff between approaches. More performance at the cost of more code complexity and a harder to use API. Deep testing improves correctness but locks you in to your existing design choices and makes refactoring harder. But most code is still nowhere near the Pareto frontier. Lots of code can be improved on one or multiple axes without sacrificing anything. For example, making functions pure when you can often results in easier to read code, better readability and better performance (with other changes). This is my main gripe with “clean code”. His examples are full of hidden side effects and latent performance problems. He over relies on classes, inner mutation, virtual functions and tiny functions spread out everywhere. It’s a pity, but he doesn’t seem to know how to actually practice what he preaches.
- taybin 1mo agoYes, a toy problem only needs a simple implementation. This is a straw man. And I don't even like Robert Martin's Clean Code, but the author is not addressing where this style actually provides benefits. When you're updating 23 if-statements because you had to add support for some new business workflow, you'll wish you had a conceptual entity that encapsulated the operations on the type of workflows so you just had to implement them in one place.
- deleted 1mo ago[deleted]
- HeavyStorm 1mo agoThank you! I always see this stupid conversation about performance and nobody seems to get this.
- rbanffy 1mo agoIt's much easier to optimise an easy to understand program than it is to debug a highly optimised one.
- jackling 1mo ago> stupid conversation about performance The article is titled "'Clean' Code, Horrible Performance", that's the argument being made. Why is it a stupid conversation? If you think the trade-offs are necessary, then fine, argue that. But that doesn't change the objective measures that the author did to demonstrate the thesis of article.
- jackling 1mo agoIt's a problem chosen by the author of Clean Code. How is it a strawman? The author of the article is directly refuting the style of the problem/solution that the original author chose, and arguably demonstrated a better approach. That is not a strawman.
- wduquette 1mo agoUsing `switch` is not a better approach if the design allows for outsiders to add their own shapes at a later time. Using `switch` probably is a better approach if the range is shapes is fixed and new shapes can't be added, especially if the language's `switch` statement requires that all valid cases be included.
- meerita 1mo ago"Code Complete" by Steve McConnell is a good option for those who want to improve their development practices.
- bluGill 1mo agoI stopped reading as soon as I saw the shape class. This example (along with the proverbial animal) has done a lot of harm to OOP and programming. You need base classes (which are not always the right answer, but when they are) to be based on the abstract concept you need to model not something real that is easy to understand when someone isn't an expert in your domain.
- mrkeen 1mo agoMaybe give shape another try. https://www.youtube.com/watch?v=zHiWqnTWsn4 https://www.youtube.com/watch?v=zHiWqnTWsn4 1:00:00 - Open/closed principle and 1:13:52 - Liskov substitution principle. Both are given in terms of Shape, but it's to paint the picture that things are more complicated than you thought, even with something that should have been as simple as shapes. (As opposed to "shapes are easy, just model the world like that and it will be easy too")
- bluGill 1mo agoI won't have time to watch that anytime soon, but your summary makes my point. The real world is complex and shape as a quick skim shows this article used is just a bad example. Worse they were using this bad example to try to make a general point but the example used was to simple to generalize like that.
- glitchc 1mo agoI'm not sure I follow the thrust of the article. The author starts off with talking about clean code, but then compares OO with procedural code. It's not the same thing, and of course we've always known that OO abstractions carry a performance penalty. Even the founders of OO (Alan Kay et al.) acknowledged the memory and compute impact, but thought it was a worthwhile tradeoff for clean abstractions in complex code-bases. Back then computers were far less performant than they are today, so the first languages (e.g. SmallTalk) had to be compiled into a bytecode VM that ran on a Xerox PARC. Other efforts included hardcoding some of the constructs into the ISA.
- jackling 1mo agoHe states at the start of the article the tenets of clean code he's arguing against, not just the general OOP of it. Shows how ignoring a certain tenet leads to increase performance, that's the thrust of the article. He routinely in the article goes back to the tenets he's arguing against.
- scelerat 1mo agoHow much of the performance differences come down to language or compiler choice in these examples? Would I see the same kinds of performance gains or losses avoiding or using certain patterns in Go or Rust or Java? Are they the same examples as in C++? What about dynamic languages like ruby or python or javascript?
- nylonstrung 1mo agoI believe in Rust there would be almost no performance hit due to the compiler using monomorphizing everything via the "zero-cost abstraction" we love to brag about
- devmor 1mo agoPerformance vs. Maintainability is the infinite debate, and it’s a mind numbing one because in the vast majority of professional roles you will have the opportunity to prefer neither.
- jeltz 1mo agoAnd following Clean Code gives you neither. The book is written by someone with a very limited experience and the advice is either basic and obvious or harmful. People should just stop reading that book.
- wduquette 1mo agoMake it work, then make it "clean" (that is, readable and maintainable); then make it fast, and only if measurement indicates that it matters.
- narnarpapadaddy 1mo agoI think performance generally trades along a different axis: open-world vs closed-world assumptions. There are many cases where closed-world assumptions may confer performance benefits, such as tree-shaking, whole program optimization, and using switch statements rather than a class hierarchy. Whereas designing for extensibility necessarily precludes some of those choices (though it doesn’t necessarily require OOP, for example registering a handler in a table). In other words, it’s easier to optimize a problem that is fixed and well-understood, versus one flexible and unknown. Take that ideas to the extreme and end up at ASIC bitcoin miners.
- usr_222 1mo agoThe only reason why your code is slow or bad - because you created it in such a way, not due Clean Code. I cannot stop being surprised by how ridiculously short-sighted developers are - and how you continue to believe in golden hammers and silver bullets. You want to build a car, so you take the “Clean Code” hammer and try to build one with it. Then you say, “Hmm, I built a car using the Clean Code hammer, but it cannot even reach 100 km/h. Therefore, Clean Code is bullshit.” This is ridiculous. The same applies to blind followers of Clean Code and SOLID who build systems without any high-level understanding of the system they are trying to create. The result is almost always an unreadable, unmaintainable pile of shit. In fact, they are all in the same boat. All of these principles are just that: principles. They are not specifications to be implemented. Moreover, they are LOW-LEVEL principles. So, they cannot be “bad,” “good,” “slow,” or “fast”. Your code is bad or slow - not the programming principles. Until you understand what you are trying to build and how it should work, you cannot decide whether Clean Code, SOLID, GoF patterns, or any other principles are appropriate. Once you have a solid architectural backbone that satisfies the required system characteristics, you can apply the principles that help you implement that design in the simplest and most effective way. And each principle has its own trade-off with other principles! --- too much DRY -> dead coupling (all these “cores” and “libraries” that team leads cobble together at night and proudly turning a distributed system into monolith) --- too loose coupling -> excessive fragmentation -> low cohesion and broken incapsulation --- excessive SRP -> low cohesion and so on and so on. So it is not Clean Code bad - you just not understand what Clean Code and other principles are.
- jeffnash 1mo agoIt seems like the main takeaway is that many textbook OO paradigms aren't the most optimized representations of the code. In this case, the cost is dynamic dispatch and pointer-chasing. This is a function of the Shape abstraction, but not the abstraction itself. But the argument is you're trading some of that performance optimization for maintainability. None of this is exactly news. And while I'm here ranting: I never understood why shapes are the canonical OOP example. Shapes are a closed set of types (yes I'm sure GPT-324 invented a new one) with an open set of operations. There's always going to be one more thing you need to do with those shapes, but you'll never be adding new shapes down the road unless you are still in Kindergarten. OOP is useful for the exact opposite case, where there is a relatively fixed set of operations and you routinely introduce a new subtype that needs to perform all or most of those operations. I've noticed that most courses that introduce the concept of OOP do so in a way that (perhaps unintentionally) emphasizes the false notion that everything should have an 'x-is-a-y' taxonomy before actually asking the question if that is appropriate. Putting the Cart extends Vehicle before the Horse extends Animal.
- leecommamichael 1mo ago> In this case, the cost is dynamic dispatch and pointer-chasing. To sharpen your statement, the cost is missing the CPU caches, which is often caused by failing to pool allocations and reading indirectly. > But the argument is you're trading some of that performance optimization for maintainability. Right, but exactly how much? I would argue "very OOP" design styles neuter your ability to optimize the system, and sometimes necessitate that you are kept at arms-length from the system, only capable of "customizing" it via more abstract API layers. I do believe certain OOP practices can make maintaining software easier, but I also believe we have not figured out how to retain control over the computer in the face of these abstractions. As an example, Clean Coders advocate for "separation of responsibilities" and often speak in terms like "ownership" or what a function/class "knows about" or "should have to know about." When different classes are given different data-fields in the pursuit of making it clearer (what should exist in that scope,) you are creating a constraint which is virally spread through the codebase which runs counter to what the CPU wants. The CPU wants an array, but you can't have an array because the FileManagerFile can't "know about" the FileManagerFileCache, and the FileManagerFileCache can't known about the FileCache, so now each FileManager "owns" its own cache, which is an entirely separate heap allocation.
- ErroneousBosh 1mo agoThis is just bloody stupid. If you care about performance, you don't use OOP, you don't use if/else, you don't use switch{case}, what you do is you write the hot parts in assembler. If you aren't writing it in assembler, you're writing slow code. But that code is still not optimised until you've implemented it in an ASIC.
- zabzonk 1mo ago> If you aren't writing it in assembler, you're writing slow code. Depends in part in how good you are at writing assembler.
- ErroneousBosh 1mo agoTrue, with modern processors there is a hell of a lot of "it has to be this way round for the pipeline to flow" that the compiler does for you. But you're still throwing away so much time on things like bounds-checking memory accesses that never need it.
- tcfhgj 1mo ago> But you're still throwing away so much time on things like bounds-checking memory accesses that never need it. Are you? C++ doesn't check bounds by default, and Rust only checks in certain situations and you could opt out if you wanted to instead of switching to asm
- ErroneousBosh 1mo agoC++ is tremendously bloated though, and wastes hundreds of instructions with stuff you shouldn't need to care about.
- zabzonk 1mo ago> stuff you shouldn't need to care about Such as? I don't believe any C++ compilers are producing extra machine code just for the fun of it.
- teddyh 1mo agoPlease note that this criticism is from 2023, but the “Clean Code” book has a second edition from 2025, extensively revised to account for the many misconceptions which new programmers might have gotten from the old edition, such as interpreting rules too strictly, etc.
- pragmatic 1mo agoAre the examples any better? What a muddled bunch of gibberish.
- cratermoon 1mo agoThis is Muratori showing he's a solo programmer who has only ever worked on relative small, simple software that runs on a single machine.
- throw16180339 1mo agoHe worked on the Granny animation system at Rad Game Tools. It's shipped in over 5200 games[1] and targets all the major game dev platforms. [1] https://www.radgametools.com/granny/customers.html https://www.radgametools.com/granny/customers.html
- ferroman 1mo agoClean Code wasn't trying to solve performance issues. It tries to solve issue with expensive code maintenance.
- bellgrove 1mo agoI think the author comes from a very specific perspective; RAD tools, as I understand it, generally has only one, or a very few, software engineers per product. The way I would write code on a personal project is very different than the way I’d write code in an environment with changing team members, interns, guest commits, etc. Also, In real-time simulations (ie games) often then way you write code can be the bottleneck. In web services the bottlenecks are more often network calls, database model, etc.
- relug 1mo agoits a bigger problem of class based abstractions...cpu thinks in terms of arrays and lanes and indexes thats literally what a pointer is...when you try and abstract that away it in the wrong way that compiler cant understand, it creates overhead. but i think this is overall for all patterns in programming classrs are just so low level people take it for dogma and are appalled that something so standard is anti pattern
- angusik 1mo ago[flagged]
- damienmeur 1mo agoIt really depends on what you are building, coding is always about trade-offs, and sometimes (not always) you have to choose between maintainability/readability and performance. If you are writing code for embedded devices where every cpu cycle counts, I would indeed trade a bit of the maintenance for some cpu cycle. If I am writing a huge web app that has to be maintained years by a large team of devs, I would prefer a more simple/maintanable code over a fast one (+ in such scenarios the real bottlenecks are often your I/O, not the raw CPU perf). This is for the same reason you usually write code that needs to be fast in low-level programming lng like C and huge web app in Node.js or Java.
- Alfredo66 1mo ago[flagged]