"Clean" Code, Horrible Performance (2023)

(computerenhance.com)

79 points | by FrojoS 7 hours ago

13 comments

  • Aurornis 1 hour ago
    I 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.

    • cavoirom 46 minutes ago
      I learned Clean Code at the beginning of my career, but I don't actually get it. Recently I know about "testable code" from Justin Searls. I found the "testable code" concept is more useful because we can monitor the effectiveness of the concept and I can see the actual benefits in my projects.
    • MrBuddyCasino 49 minutes ago
      Well said. It is not without merit, but tends to attract the tedious killjoys and midwits.

      The bureaucrats who above all value process over outcome.

    • HeavyStorm 51 minutes ago
      [flagged]
    • locknitpicker 57 minutes ago
      > 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.

      As much as it pains you, that's exactly the feedback that should be provided to developers such as yourself, specially if you do not understand why it matters.

      Unmaintainable code is prevented at the PR stage. The likes of you need this feedback because you aren't mindful to the problems it reflects.

      The problem with a long function is not if it has N or N+1 lines of code, it's that length code with many branching conditions is prone to be untestable and introduce non trivial bugs. Once you start to refactor, not only is it easier to parse but harder to break. This fact is known for decades now.

      Your comment is really not about clean code, it reads like frustration for having team members point out the quality issues in the code you're delivering and your unwillingness or inability to understand why it's a problem and why you should correct your approach.

      There is a class of developers who are very vocal against established practices, such as OO, clean code, TDD, Etc. but ultimately when their complains are hold to scrutiny it's evident that the issue doesn't lie with OO, clean code, TDD, etc.

      • rbanffy 41 minutes ago
        > The problem with a long function is not if it has N or N+1 lines of code, it's that length code with many branching conditions is prone to be untestable and introduce non trivial bugs. Once you start to refactor, not only is it easier to parse but harder to break. This fact is known for decades now.

        A function with too many lines is, most likely, doing more than one thing. Functions should do one thing, be easy to test (with few or no external dependencies whenever possible), be deterministic (unless required not to be), and so on. Excessive mocking is another code smell I look for - it often betrays poorly designed functions that can't be easily tested.

        • fellowniusmonk 26 minutes ago
          I once saw a professional software engineer try to refactor the spaghetti code in a complex bioinfomactics project written in python.

          It was a complete failure. That branch was abandoned and development continued off the spaghetti.

          There is a reason bioinformatics has its own set of viz charts that only they use.

          That's my anecdote anyway, it led me to the conclusion that sometimes things are continuous spaghetti and other than some small organizational changes, attempts to exhaustively discretize the code are a fools errand.

          The biggest benefits most projects like that are likely to see are performance and debugging improvements accomplished by factoring out recursion.

          Mapping to terrain is always the real effort in my opinion.

          • rbanffy 6 minutes ago
            > It was a complete failure. That branch was abandoned and development continued off the spaghetti.

            It was considered too hard, most likely because the present state of the code already degenerated beyond recovery. It might be difficult, but it's never impossible.

            > Mapping to terrain is always the real effort in my opinion.

            Yes. The domain might be complex, and it might be possible that there are no simple ways to work within that domain. Irreducible complexity is, after all, a thing.

      • ryanbrunner 41 minutes ago
        Fortunately 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.

        • rbanffy 30 minutes ago
          > Fortunately 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.

          It's very hard to make a function you need to scroll back and forth to understand readable. Break it into smaller ideas that are more easily reasoned about. We love to think we are too clever, but we are not and we always need to keep an eye on cognitive load - having epifanies when you finally understand how something works is a great feeling, but relying on epifanies coming to you when you are trying to figure out how something works because it's not working now, is a terrible practice.

      • lionkor 45 minutes ago
        There 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.

      • the__alchemist 3 minutes ago
        It sounds like we have opposite programming styles!
      • kelseyfrog 16 minutes ago
        Clean code nitpickers mistake the map for the territory. The rules are the map, maintainability is the territory. The map is a model of the territory, but the territory always contains more detail, both zones of maintainability not covered by the rules and zones of unmaintainability covered by the rules. The rules are heuristics, and like all heuristics, they have false positives and false negatives. Being a mature developer means knowing the limits of tools including processes, style, and standards. When they nit to the rules and not to the goal, it doesn't contribute, it distracts.
    • tarcon 1 hour ago
      You 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.

    • tcfhgj 22 minutes ago
      So they hate clean code because they don't actually know clean code
      • tranceylc 12 minutes ago
        Even the author of clean code would fall under someone who can’t program
        • tcfhgj 4 minutes ago
          what?

          I am talking about knowing the idea of clean code, which doesn't include dogmatically limiting #locs in functions to an arbitrary number.

          So if someone hates clean code for someone doing that, it's just dumb.

  • taybin 1 hour ago
    Yes, 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.
    • alerighi 21 minutes ago
      Indeed. The problem is trying to apply the principles of "Clean Code" or more generally of OOP everywhere. There are surely cases where having an interface as an abstraction and multiple implementations makes sense. There are case where it doesn't, and if you take as a dogma "everything shall be the implementation of an interface" you get more complex code and performance penalty for nothing.

      There is nothing wrong with having procedural code with a switch case, as there is nothing wrong in having global variables, in having even goto, depends on how you use it.

      • tcfhgj 15 minutes ago
        The principle of clean code includes KISS, therefore complex code for nothing isn't clean code
    • HeavyStorm 52 minutes ago
      Thank you! I always see this stupid conversation about performance and nobody seems to get this.
      • rbanffy 23 minutes ago
        It's much easier to optimise an easy to understand program than it is to debug a highly optimised one.
      • jackling 14 minutes 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 23 minutes ago
      It'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.
  • scelerat 10 minutes ago
    How 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?

  • jayd16 1 hour ago
    Ok 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 1 hour ago
      In 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 56 minutes ago
        The 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.

  • aw1621107 2 hours ago
    Related:

    HN post for original article on 2023-02-28 (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), 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), 121 points, 114 comments

  • glitchc 28 minutes ago
    I'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.

  • devmor 2 minutes ago
    Performance 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.
  • flossly 2 hours ago
    I'd say Clean Code is teaching many bad-practices. Too many to be recommended.
    • pixlmint 1 hour ago
      eh, 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 1 hour ago
        at bast it makes to better at "Clean(TM) OOP code".

        programming in general is waaaaaay bigger than what the book covers.

        • Jtsummers 1 hour 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.

    • general1465 1 hour 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"

      • bluGill 51 minutes ago
        If your compilers is any good it will inline, and do a better job than you of figuring out what should be inlined. For that matter function calls are generally fast so long as the objects you copy as part of the function call are not slow to copy (which they can be). There are exceptions to the above, but in general small functions are not a problem.

        What is a problem is large functions. I have seen functions that were over 60,000 lines long (and few comments or other excess space takers). I will take a 5 lines max rule for functions (this is nearly straw man levels of short!) over that. Functions that are 50 lines long start to get annoying to read but are not a problem. Even 100 lines functions I can handle. However the extreme of long functions is much worse than the extreme of short.

        • tialaramex 10 minutes ago
          Manual inlining, like manual loop unrolling wants an explanation, why did you do this, why not let the compiler do it? If I see it with no explanation I am going to assume you don't know what you're doing.
        • rbanffy 17 minutes ago
          > I have seen functions that were over 60,000 lines long

          That can't possibly be from a serious person.

          • Jtsummers 14 minutes ago
            I've seen 10k+ SLOC functions written in C, and 20k SLOC functions written in Fortran, so it wouldn't surprise me if people created ones as big as bluGill describes.

            The C was almost always written by EEs who learned that function calls were expensive and so they minimized their use of them (this was their stated rationale, not me guessing). What amused me was that every time I tackled one of those things I'd reduce the line count by 70-90%, and usually at least double performance, by using a bunch of small functions to encapsulate the repeated logic. Compilers inline well, and have for quite some time.

            • rbanffy 5 minutes ago
              > I've seen 10k+ SLOC functions written in C, and 20k SLOC functions written in Fortran,

              That, spoken by Rutger Hauer.

      • rbanffy 18 minutes ago
        > unless compiler will save you by inlining the function into your code or architecture

        That's precisely what a compiler should do. Your code should be easy to read and understand. Let the compiler inline calls and unroll loops (until the I1/L2/L3 cache starts becoming a problem, that is)

  • meerita 59 minutes ago
    "Code Complete" by Steve McConnell is a good option for those who want to improve their development practices.
  • bluGill 46 minutes ago
    I 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.
  • MonstraG 6 hours ago
    (2023)
    • inigyou 4 hours ago
      Still true today.
      • unscaled 1 hour ago
        I 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-replacemen...

        • inigyou 1 hour ago
          This 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.
      • WesolyKubeczek 2 hours ago
        Nobody 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.
      • Johanx64 53 minutes ago
        I 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.

  • jgwil2 2 hours ago
    See also the more in-depth followup "Simple Code, High Performance (https://www.youtube.com/watch?v=Ge3aKEmZcqY)
    • lunar_mycroft 1 hour ago
      Actually, that video predates the one on clean code.