931 comments

[ 3.1 ms ] story [ 506 ms ] thread
Brilliant. Thanks.

tl;dr polymorphism, indirection, excessive function calls and branching create a worst-case for modern hardware.

Most C++-projects I dealt with are neither clean nor performant. I rather follow clean code to improve maintainability and get things done than to optimize for performance. It’s also easier to find bottlenecks in a well readable and testable code base than in a premature-optimized one. However it is true that the more abstractions and indirections used software gets slower. Also these examples are too basic to make a real-world suggestion: never assume sonething is slow in a large project because of indirection or something…always get a profiler involved and run tests with time requirements to identify and fix slow running parts of a program.
I've worked in projects where no one seemed to know SQL, where massive speed improvements were made by fixing very low hanging fruits like removing select * queries, adding naive indexes, removing N+1 queries etc.

Likewise, I've worked in code bases where performance had been dreadful, yet there were no obvious bottlenecks. Little by little, replacing iterators with loops, objects/closures with enum-backed structs/tables, early exits and so on accumulating to the point where speed ups ranged from 2X to 50X without changing algorithms (outside of fixing basic mistakes like not pre allocating vectors).

Always fun to see these videos. I highly recommend his `Performance Aware Programming` course linked in the description. It's concise and to the point, which is a nice break from his more casual videos/streams which tend to be long-winded/ranty.

Could you elaborate what you mean by "naive indexes"?
In general that means looking at the sql query plan for a slow query, and adding appropriate indexes when there are full table scans.
I assume "naive" means "simple, basic" here. As in "index this column. Period".
Not GP. But you can often anticipate where indexes have to go for common queries.

There might be some important colums like say a “status” or a “date”, which are fundamental to a lot of queries.

Or you have colums X and Y being used frequently or importantly in where clauses together, then that’s a candidate for composite indexes.

Stuff like that.

I think historically people had a lot of bad intuitions about how effective non-composite indexes are in databases. That if I have an index for A and an index for B I should be able to do A & B and get a quick answer.

There's been a lot more educational material on composite indexes and in particular partial indexes and so I'm not sure if someone with 3 years' experience today can accurately judge a conversation talking about ten years ago.

If some field is referenced in WHERE clause, add an index for it.

If there are a few fields referenced in a single WHERE add a single index that includes all of them.

If you have index that has a, b, c then it is as if you also had indexes a, b and a.

If condition in WHERE is = put this field at the beginning of an index. If it's < (or similar) put it at the end. You'll get best results if you have none or only one < in your query.

I can very much relate to this.

Just taking the little bit of time to think about what the computer needs to do and making a reasonable effort to not do unnecessary stuff goes a long way. That 2x-50x factor is in fact very familiar. That’s something loading in a second rather than in a minute, or something feeling snappy instead of slightly laggy.

And it matters much more than people say it does. The “premature optimisation...” quote has been grossly misused to a degree that it’s almost comical. It’s not a good excuse for being careless.

One thing I find frustrating in game development, is we often put off optimization until the end of the project when we know we are happy with the game. But that means _I_ have to live with a slow code base for 2 years.

It takes 19 seconds for the main menu to load when you push play on our current game in Unity. It's killing me.

Meanwhile in my lua side project, its less than a second.

Indeed. Having to deal with slow code (and slow compile times to some extent) for the majority of the project’s lifespan ruins iteration rate.
> I've worked in projects where no one seemed to know SQL, where massive speed improvements were made by fixing very low hanging fruits like removing select * queries, adding naive indexes, removing N+1 queries etc.

Can you recommend any SQL book with main focus on performance improvements like this?

Just know SQL, as noted these are really low hanging fruit. If you know SQL you should know about these.
I've learnt as I've needed it, so I'm afraid I don't have a single source to point at. Of the things that I've listed, a quick google search on each should give you enough info to be useful.

> Select * queries

Sometimes you only want two columns, but you ask for 5. Say you query a million rows, where you ask for but throw away 60% of all the data you get back.

> Naive indexes

As in, just slapping an index on a table that doesn't have one makes such a big difference that sometimes it's all you need.

> N+1 queries

This is more of a problem in ORMs, but any time you call the database N times instead of 1 time. A classic example is writing a for loop that asks for one row at a time, instead of asking for all rows once.

> adding naive indexes

You get 90% of the improvement just from that.

Indexes are the whole reason anyone even uses databases. And yet some backend guys think they are optional.

The performance difference is truly savage.

But I think there is a reason for the existence of "clean code" practices: it makes devs easier to replace. Plus it may create a market to try to optimize intrinsically slow programs!

It doesn't just make devs easier to replace. It makes it my job more pleasant (and that of my colleagues). But yes, you're right. It does also help onboard people.

Imagine working as a barista with a disorganised bar, a mat on the floor that keeps sliding and a corner is sticking up, and one bag of beans where half the side is decaf and the other is normal.

Now compare that to working in a more common sense coffee shop: everything is in its place, the mat isn't decrepit, and you have multiple bean bags.

In which one do you think it's easier to make coffee?

Huh? Coffee shops optimize for people not bumping into each other and having related items close together, and don't pretend to not know what kind of gear they have.. that's not a terrible analogy to the exact opposite argument.
I think the sentiment is that order and organisation is helpful in achieving goals and cultivating a good working environment as opposed to a big mess. Analogies, just like abstractions, are leaky.
Yeah, but this one leaks a smart-matter paint that self-assembles into a shape of text saying "the order and organization is not the goal, but a consequence of ruthlessly optimizing for performance above all".
I didn't try to say that, but maybe I confused my point with the analogy (like an earlier comment mentioned: they're imperfect).

I meant to say that organisation helps make work easier, including adding performance optimisations.

It seems like you may be assuming that Casey is arguing against writing clear code, which he is not. He is arguing that you should just write the simple thing and usually that is also the most clear, readable and "maintainable" code because it is easy to get an overview of. So what he is arguing for does not fit your coffee shop example, because of course no one should write unreadable code. The argument is that sometimes taking a step back from how you were taught to write clean code, could be simplified in a way that is _also_ performant by default.
This advice doesn’t really differ from the actual ‘source material’ though. It’s really arguing against the people that learned what “clean code” is from a blog post or a Tweet or (most likely of all) another YouTuber that tried to take a complex engineering topic that they don’t have the experience to understand, and shove it into a video-listicle full of DigitalOcean ads and forced facial expressions.

You see the same thing with microservices. Any of the reading material by the big / original proponents of microservices is actually quite good at giving you all the reasons why they probably aren’t for you. But that doesn’t stop the game of telephone that intercepts the message before it gets do most developers.

So I really just see this whole thing as someone saying “RTFM”, rather than it being any sort of derived nuanced take.

The sooner a professional software developer can get themselves off the treadmill of garbage trendy educational content, the better.

> people that learned what “clean code” is from a blog post or a Tweet or (most likely of all) another YouTuber

Or, you know, college. Though I can only speak of my local tech college, not full blown university. I don't bear them any ill will --- there's a hell of a lot to try to teach in two years --- but a lot of the things that were taught in my degree, were very dogmatic.

Unrelated to the content itself, am I the only one wondering if he has his t-shirt mirrored or if he's really skilled at writing right-to-left?

Content wise: his examples show such increases because they're extremely tight and CPU-bound loops. Not exactly surprising.

While there will be gains in in larger/more complex software by throwing away some maintainability practices (I don't like the term "clean code"), they will be dwarfed by the time actually spent on the operations themselves.

Just toss a 0.01ms I/O operation in those loops; it will throw the numbers off by a large margin, then one would just rather pick sanity over the speed gains without blinking.

That said, if a code path is hot and won't change anytime soon, by all means optimize away.

Edit: the upload seems to have been deleted.

its custom made mirrored tshirt, he is writing on one of those lightboards and then mirroring.
He created the t-shirts specially for StarCode Galaxy[1] which is a longer form class for C++ programming in the same veign as the current video, but with a much wider scope. (As far as I know SCG is not released yet).

As a small, amusing, s(n)ide note, Casey's rants about the slowness of the Windows terminal[2] that ended up in Microsoft releasing an improved version[3], were based him wanting to implement a TUI game as an exercise in SCG and the terminal being too slow.

[1] https://starcodegalaxy.com/ [2] https://news.ycombinator.com/item?id=31284419 [3] https://news.ycombinator.com/item?id=31372606

>Just toss a 0.01ms I/O operation in those loops; it will throw the numbers off by a large margin, then one would just rather pick sanity over the speed gains without blinking.

I mean, yes, if you do something completely fucking idiotic like put an IO operation inside a tight calculation loop, then all your speed gains will vanish. But I don't see how that refutes anything.

I said I/O, but it could be memory access. All he does fits in registers and maybe cache. Also, 0.01ms was the normal RAM latency when I started using computers...

Do you remember the last time you had to do a tight calculation loop, and not only that, but one that significantly impacted the total runtime? Personally I do, it was roughly 15 years ago writing a raytracer.

I can imagine that happening in game/3D dev, DSP, emulators, ML, and maybe some other types of software, but even in those cases one already has dedicated libraries and hardware to extract the performance from where it can be extracted.

I mean, Python is slow as an old dog, yet it gets most of the ML fun.

> All he does fits in registers and maybe cache.

Well yes, that's.. actually the point. Indirectly, anyway anyway.

> I mean, Python is slow as an old dog, yet it gets most of the ML fun.

Python driving some UI and logic, with a ton of optimized Fortran and C driving everything hot.

"Use subclasses over enums" must be some niche advice. I've never heard it. The youtuber seems to be referring to some specific example (he refers to specific advice from "them") so I guess there's some context in the other videos of the series.

re: the speedup from moving from subclassing to enums - Compiler isn't pulling its weight if it can't devirtualize in such a simple program.

re: the speedup from replacing the enum switch with a lookup table and common subexpression - Compiler isn't pulling its weight if it can't notice common subexpressions.

So both the premise and the results seem unconvincing to me.

Of course, he is the one with numbers and I just have an untested hypothesis, so don't believe me.

I think "them" is someone named Robert Cecil Martin, but I'm not sure if this example from the video appears in his book.

What compiler are you using that devirtualizes every class hierarchy? I suspect that Casey is using C++ so he may (unfortunately) have multiple translation units in his program.

>I suspect that Casey is using C++ so he may (unfortunately) have multiple translation units in his program.

Yes, the video uses C++.

Obviously if one compiles a library then the compiler has no way of knowing that other subclasses of `shape_base` do not exist. My point is that when compiling a binary as they are doing for their video, the compiler knows that there are no other subclasses that it needs to cater to.

It might require LTO explicitly, of course. At the very least godbolt doesn't devirtualize without LTO [1], but godbolt itself breaks if I enable LTO [2] and I CBA to test locally right now.

[1]: https://gcc.godbolt.org/z/498nKEzhK

[2]: https://gcc.godbolt.org/z/WqhP3fzxK

Correct me if I'm wrong. Even if the compiler devirtualizes the classes, you still have the memory cost of storing the vtable pointer in each of the object instances (8 bytes for each instance), which means you need to do more fetches from memory. Does CPU prefetching negate the cost of these additional memory lookups?
The enum to tell what shape an object is costs something, probably 4 bytes. Storing the 8 byte vtable constant isn't so bad considering.
looking at a value is faster than dereferencing a pointer—why is this controversial?
Even when the target is an app, said app might be loading DLLs at runtime which may contain other subclasses.

The fundamental problem here is that C++ doesn't have a good abstraction to represent visibility of public types, since any other translation unit - even across the DLL boundary! - can re-declare the type and then derive from it. The only way to constrain visibility is to use anonymous namespaces, and that only works if the type can be confined to a single unit (that C++ compilers seem to ignore the optimization opportunities here in practice perhaps indicates just how rare this actually is).

Martin Fowler's "Refactoring" also has "Replace Conditional with Polymorphism". I like the intent of the book but I don't follow it 100%. Another part of that book that tripped me is where he calls the same function with the same argument multiple times instead of saving the result in a variable for reuse.
The existence of a refactoring in the Fowler refactoring catalog is not normative advice that it should always be applied. Refactoring also has “Extract Function” and “Inline Function” as refactorings. They can’t both be right…

Refactorings are moves you can make. Choosing when to make them is up to you. In fact, Fowler provides guidance along with each refactoring suggesting when it might be applicable (i.e., not always)

A compiler can only devirtualize a virtual call if it knows the underlying object’s type at compile time, which it certainly won’t for a runtime populated array of base class pointers.

CSE doesn’t work across function boundaries unless those functions are inlined, which won’t happen with virtual functions due to the above.

See my reply to anonymoushn's comment.
Saying things akin to "your compiler is bad" because it doesn't optimize stuff like this is a cop out.

For one: Most compilers for most languages are bad by that metric, and interpreters don't even get to play. So this is not helpful for the vast majority of people. Waiting around for them becoming good is not a viable option.

Second, say the compiler would perform good in this scenario. Cool, lets go up a notch, or two, and it would start performing bad again, because there are limits to what it can do in a reasonable amount of time.

And if that limit were big that maybe wouldn't matter, but the limit is low, and so it does. Real programs are so much more complex than this example, that even if the compiler got 10 times better, it would still fail to optimize large parts of your real program.

For interactive applications debug builds also need to be fast.
Unfortunately, we live in the real world where compilers do not do the things that you suggest that they ought to.
The problem with the contemporary "clean code" concept is that the narrative that performance and efficiency don't matter has been pushed down the throat of all programmers.

Re-usability, OOP concepts or pure functional style, design patterns, TDD or XP methodologies are the only things that matter... And if you use them you will write "clean code". Even worse, the more concepts and abstractions you apply to your code the better programmer you are!

If you look at the history of programming and classic texts like "the art of programming", "sicp", "the elements of programming"... The concept of "beautiful code" appears a lot. This is an idea that has always existed in our culture. The main difference with the "clean code" cult is that "beautiful code" also used to mean fast and efficient code, efficient algorithms, low memory footprint... On top of the "clean code" concepts of easy to test and re-usable code, modularity... etc

If you have more than one person working on a codebase; clean code matters a lot.

A code base that can't be understood and maintained by the whole team, will degrade quickly.

Nobody is saying maintainable code isn't important but rather that clean vs fast is a false dichotomy.
Okay, so we should aim for clean AND fast code?
One can argue that simple code is both fast and clean. However, you can only measure how fast (or slow) your code is, so make it simple, aim for fast and hope it's clean (:
One can argue that extremes of fast and clean code will both result in something horrific.

What Casey is doing is showing how bad a hammer is at removing screws. I mean duh, you're removing screws with a hammer.

I don't understand what you are saying.

My comment was a jest (as suggested by the smiley).

What I take from Casey's post is that a simple non-pessimistic representation allows for efficient code. That is, using a table instead of a class hierarchy gives massive performance boost. Compared to a "clever" loop unrolling doesn't give that much of a boost.

So we need simpler representation. IMHO the table implementation is not less readable nor less flexible than the class hierarchy. But it is less common in the code I am used to, in other words, it's not a widely used pattern).

> a simple non-pessimistic representation

That's the insight, I think. "Clean Code" tells you to use maximally pessimistic representation for everything, because everything could be extended in some way in every direction. Meanwhile, in the real world, you likely have a good idea what directions of evolution are possible, and which of them are even useful.

Casey's example shows you that, if you design your code to make use of those assumptions, you'll get absurd performance benefits for little to none loss in readability (and perhaps even a gain!).

Some may ask, "what if you're wrong with your assumptions?". Well, you pay a price then. Worst case, you may need to rip out a module, rethink the theory behind it, and rewrite it from scratch - likely forgoing some of the performance benefits, too. Usually, the price will be much smaller. Either way, it's still better than being maximally pessimistic from the start, and writing software that never had a chance of ever becoming good or fast.

There are competeing in my opinion. Many techniques to make code fast will make it less readable.

Techniques like manual code unrolling/inlining, writing branchless code, compressing data to fit into a pointer, etc.

I agree .. fast doesn't take account of how verbose or understandable code will be.

As an extreme example, people choose not to write low level code for a good reason, even if it might be fast.

High level / interpreted code will be more understandable, but will automatically have an overhead in most cases.

> If you have more than one person working on a codebase; clean code matters a lot.

You can have fast and clean code, just not the Uncle Bob style of "clean code". Uncle Bob hijacked the meaning of cleanliness. It doesn't mean that code written like that is actually clean, in fact it's usually the opposite: Uncle Bob's clean code is NOT clean.

It's also worth noting that Bob's advice is for Java, which has relatively unusual performance-idiosyncrasies around polymorphism and function calls. Much of the advice becomes very poor in languages that aren't JVM-based.
> TDD or XP methodologies

> the more concepts and abstractions you apply to your code the better programmer you are!

These contradict each other. XP very explicitly opposes introducing (unnecessary) abstractions: YAGNI, DTSTTCPW, etc. And TDD is a good tool for enforcing that, as you only get to write code that you have a failing test case for.

> TDD is a good tool for enforcing that, as you only get to write code that you have a failing test case for.

TDD encourages the use of mocks and unit testing to increase code coverage. And unit testing is specially dangerous. You write a test, then program, so the test is helping you (the programmer). Selling the idea that the higher the test code coverage is the better and safer your code is. Not true at all. If your code doesn't have integration tests for example, you will never know how it actually runs. If you mock everything, you are not really "testing" anything but your internal logic. Unit testing and code coverage just checks that a code path has been run. But there are other tools like fuzzy testing or mutation testing... Do you randomize the memory at every test run? Do you make sure that the CPU cache is cold or hot depending on the test? Good testing is hard.

Most unit tests are written to ease the development. After finishing the development, they are safe to delete. Because they don't add any real value as I understand it. I understand that a test is a business contract of something that MUST work in a certain way. Unless the contract changes, the test must never be removed or changed. TDD and exhaustive unit testing make the maintenance process harder because you don't know if a test is useful or not.

If you follow TDD, most unit tests are re-written all the time. Because they were not written to test a business or critical contract, they were originally written to help some programmer write some internal logic.

> If you mock everything, you are not really "testing" anything but your internal logic.

That's the purpose of unit tests. They do not exclude the need to perform other kinds of test. Integration tests, contract tests, stress tests - all those will focus on different facets of a system.

> Most unit tests are written to ease the development. After finishing the development, they are safe to delete.

This is especially bad advice, unless no one will never touch that codebase ever again.

I saw old unit tests highlight bugs that would have been introduced by new code many times over the years.

> TDD and exhaustive unit testing make the maintenance process harder because you don't know if a test is useful or not.

Then, as a developer, remove unit tests that became useless.

Code coverage is a measurement. If you turn it into a goal, it will become useless. If you have "useless" unit tests, it tells me that some unit tests were written as padding to move code coverage up.

> TDD encourages the use of mocks and unit testing to increase code coverage.

No, it encourages reasonable decoupling, i.e. good design.

If you see yourself introducing mocks (I think you mean stubs, mocks are something more specific) everywhere, you are feeling the pressure, but avoiding the good design.

https://blog.metaobject.com/2014/05/why-i-don-mock.html

> Most unit tests are written to ease the development.

Yes, unit tests help significantly in development.

> After finishing the development, they are safe to delete.

Noooooooooooooooooooooooooooooooooooooooooooooooo!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

They are your guardrails against regressions.

> If you follow TDD, most unit tests are re-written all the time.

Nope.

> guardrails against regressions.

And of course that's important because it enables you to be courageous and refactor mercilessly. Which again is important because it enables you to Do the Simplest Thing That Could Possibly Work, and stick to YAGNI, because you know you can change your mind later.

> TDD and exhaustive unit testing make the maintenance process harder because you don't know if a test is useful or not.

TDD was later given the name BDD (Behaviour Driven Development) to emphasize that you are not testing, but actually documenting behaviour. What kind of behaviour are you documenting that isn't useful and why did you find it necessary to document in the first place?

> If you follow TDD, most unit tests are re-written all the time.

What for? If changing requirements see that your behaviour has changed to the extent that that your unit does something completely different, it's something brand new and should be treated as such. Barring exceptional circumstances, public interfaces should be considered stable for their entire lifetime and, at most, deprecated if they no longer serve a purpose.

The implementation beneath the interface may change over time, but TDD is explicit that you should not test implementation – it is not about testing – only that you should document the expected behaviour of any implementation that may carry out your desired behaviour.

> Selling the idea that the higher the test code coverage is the better and safer your code is.

I don't think someone that believes this has a good understanding of unit testing. You can easily get 100% coverage without testing anything at all!

Coverage is a great metric if it's predicated on high quality tests. Even then 100% coverage doesn't equal "safe". It means that a lot of effort has been put into understanding and testing internal behavior.

You still need higher order tests, arguably even more.

I generally don't mock in my TDD-style approach to writing tests. If you have a dependent class, e.g. writing nested serialization logic for JSON objects, you shouldn't mock the inner classes or the JSON serialization classes.

Well written unit tests serve to provide regression testing to prevent bugs reoccurring and to keep existing functionality (e.g. support for reading existing data) working.

TDD is used as a way to help write tests for the API surface and usage of your classes, functions, etc.. You should generally avoid testing internal state as that can change.

For example, if you are writing a set class, the logical place to start is with an empty set -- that's because it is easy to define the empty logic, defining accessor functions/properties like isEmpty, size, and contains. The next logical step is adding elements (two tests: add a single element, add multiple elements). Etc.

Later on, you can change the internal logic of the set from e.g. an array to a hash map. You will keep your existing tests as they document and test your API contract and external semantics. Likewise, if your hash set uses another class like an array, or a custom structure like a red-black tree, you shouldn't mock that class.

> Even worse, the more concepts and abstractions you apply to your code the better programmer you are!

I had an Android programmer, who was eager to write clean code following GOF patterns, OP and the rest of the fancy things senior developers usually do. Ended up Android team with 3 devs required 3x time to develop same feature compared to single iOS engineer.

Well they weren’t a good senior developer then. Part of the art is knowing when to use the patterns. An incredibly im protest differentiation as it’s so so easy for someone a bit green behind the ears to see this and think that any sort of architectural thinking is useless.
> Well they weren’t a good senior developer then. He wasn't. He was a mid grade dev, but it does not important, because even sr devs can fall in love with overcomplications.
Reminds me of Go. At the beginning you make only simple moves. As one learns the game more complicated patterns emerge. Watching master games the lines again are simple and clear - in hindsight.
It's often said that Design Patterns are workarounds for limitations in the expressiveness in a language: for example, the Singleton pattern is only useful in languages where you can't pass a reference to an interface implemented entirely by static methods - or the Visitor Pattern is the workaround for a language not supporting Double-Dispatch - method call chaining is a workaround for not having a pipe operator, and so on.

You said you're targeting Android, that implies you were using Java, which has its reputation for both a rigidly inflexible language-design team and its ecosystem having more design-patterns than a set of fabrics swatches - that's not a coincidence.

But for iOS, they'd be using Swift, right? Swift's designers clearly decided they didn't want to be like Java: take the best bits of C# and other well-designed languages and don't be afraid to iterate on the design, even if it means introducing breaking-changes - but the result is a highly-expressive language that, as you've demonstrated, allows just 1 Swift person to do the equivalent of 3 Java people. Swift is an actual pleasure to use, but using Java today makes me weary.

(To be clear: Java was a fantastic language when it was introduced, but it simply hasn't kept-up with the times to its own detriment, it feels like its falling behind more-and-more at time goes on - but that's going to be the fate of every programming language eventually, imo).

At least in the majority of places I worked at, people cared about readability and maintainability rather than just some abstract notion of "clean code".

And perhaps I was lucky, but typically readability and maintainability are orthogonal to performance and efficiency. Sometimes readable will have optimal performance, sometimes not. Then it becomes a matter of tradeoffs.

Same here. We had coding guidelines, not rules.

The first entry even stated that they were guidelines and if you have a valid reason to deviate: discuss it and you'll get an exception.

The discussion thing was mainly for new coders. We had a library part of the code that was used by many programs. Optimizing parts of it for their use case, could make it unusable for the others who used it.

Communication is key. If you discuss, before implementing it, why you're making certain design decisions then everything goes a lot smoother. If there are objections, keep in mind that the worst case isn't throwing away your design and starting over. The worst case is implementing it and screwing over your fellow coders.

Sounds like clean code used to mean things that were measurable.
> the narrative that performance and efficiency don't matter

That was the case during the 90s and the first decade of the 2000. Just wait for Moore's Law to kick in and in 18 months your code will get faster by an order of magnitude for free.

Moore's Law concerns IC transistor counts, not actual overall performance, and especially not single-threaded performance: a 40-core CPU isn't going to make Windows twice as fast as a 20-core CPU.

Single-threaded performance has long-since effectively plateaued: it's 2023 now and a desktop computer built 10 years ago (2013) can run Windows 11 just fine (ignoring the TPM thing) - but compare that to using a computer from 2003 in 2013 (where it'd run, but poorly), or a computer from 1993 in 2003 (which simply wouldn't work at all).

This is not to say that there won't be any significant performance gains to come, such as with rethinks in hardware (e.g. adding actual RAM into a desktop CPU package, non-volatile memory, etc) but I struggle to see how typical x86 MOV,CMP,JMP instructions could be executed sequentially any faster than they are right now.

That article doesn't contradict my post, in fact it's basically the same thing I'm saying: look at fig2 (the timeline graphic) and the paragraph preceding it: it shows that the gains in "serial" HPC performance gave-way to massively-parallel gains sometime around 2010.

The rest of the article is concerned with how software today is still written for those "serial" processors in-mind and fails to take advantage of parallel computing hardware - but this is hardly a new nor controversial statement.

You started your post saying that there's no correlation between transistors and performance. First graph shows that is wrong.

My argument was simple: 20 years ago nobody cared about performance because of the strong correlation between Moore Law and MFLOPS and performance in general.

Nowadays with multicore, individual CPU cores are becoming slower, not faster. Then we can't just wait 18 months for individual cores to get faster.

Today we need to write better software.

> You started your post saying that there's no correlation between transistors and performance.

No, that's not what I said: I'm saying that single-threaded performance is no-longer directly (let alone linearly) correlated with transistor count, and hasn't been for decades - but it's single-threaded performance that matters for most end-user applications on peoples' computers/smartphones/etc.

And Moore's Law makes no claims about performance increases either, only transistor density. It's literally the first paragraph of the Wikipedia article:

> Moore's law is the observation that the number of transistors in a dense integrated circuit (IC) doubles about every two years. Moore's law is an observation and projection of a historical trend.

The link to performance (any kind of performance: parallel or serial) was made incorrectly by another Intel executive, David House, but that assumption simply isn't true.

To summarize: *yes*: bleeding-edge IC transistor density generally doubles every 18-24 months, but this does not translate into any kind of performance-doubling in end-user applications. In fact, on the contrary, perceived "performance" (however you define it) outside of parallelizable programs, has demonstrably stagnated.

Strangely enough, even in this video, everyone notice how you're taken to make tradeoffs between coherence and cleanliness vs performance-oriented code in langs like C++, while the Clean Code book was exemplified in Java (so most of performance is shoved in JVM code) and SICP was in Scheme (which is interpreted, or transpiled to C where you can optimize, or has a VM/JIT underneath).

I vaguely smell the language of choice has something to do with that, and C++ would benefit more from data-oriented design than from literate OOP or functional programming patterns took from very very different programming ethoses (the programming language is an interface to something else)

One of the most insane things I hear repeated constantly is that "servers are cheaper than programmers", implying that runtime efficiency doesn't matter, only developer efficiency.

Which is all well and good, until you need to hire all the network engineers, systems administrators, devops people, security staff, datacenter operations managers, database sharding engineers, etc to manage the 10x more hardware and network surface area you have to throw at your slow codebase.

Go full Cloud and you won't need to hire most of these roles ever, except in niche ultra-high performance cases. These roles will be externalized at AWS or Azure, super-paid to work for you 24/7.
I think he's really underplaying the main selling point of clean code - the objective of writing clear maintainable, extendable code. His code was faster, but sometimes how it compares for adding new features or fixing bugs by people new to a code base is where you want to optimize.

Should performance be talked about more? Yes. Does this show valuable performance benifits? Also yes. Is performance where you want to start your focus? In my experience, often no.

I've made things faster by simplifying them down once I've found a solution. I've also made things slower in order to make them more extendable. If you treat clean code like a bible of unbreakable laws you're causing problems, if you treat performance as the be-all-end-all you're also causing problems, just in a different way.

It's given me something to think about, but I wish it was a more fair handed comparison showing the trade offs of each approach.

If someone is new to the codebase, would you rather they need to open a dozen files to see all of the different virtual functions that could occur at one call site, or open one file?
I see your point, but no one uses Windows notepad for coding anymore.

There are far worse crimes than having code structure that spans several files.

> There are far worse crimes than having code structure that spans several files.

Sure, but what's the advantage of having your code split over several files? Yes, you can jump between them with an IDE, but that's still a disruption, and it makes it harder to see common patterns that can help you simplify the code.

As demonstrated in the video, splitting up the switch into multiple classes only hurts readability.

You are only seeing a screenful of code at any time, no matter if they are in one file or multiple ones. I much prefer each “section” of code if you will having a specific name/location I can associate it with, than scrolling as a mad-man. Yeah, I know about markers/closable functions/opening multiple sections of the same file/etc, but at that point whose workflow is more complex?
> I see your point, but no one uses Windows notepad for coding anymore.

Did I miss an IDE that inlines all those things for you automatically? Because ones that only give you a "jump to definition", or maybe a one-at-a-time preview in a context popup, are not much better than Notepad++. You still don't get to see all the relevant things at the same time.

Most real world applications won’t have `print “red”` or the like under a specific implementation. Good luck inlining 6 1000 lines-implementations into a single switch statement.
The "project" vs. "external" vs. "system" library conceptual distinction exists for a reason. But of course you want presentation-level inlining to be semi-automated, much like autocomplete and jump to definition.
As tradeoffs go, I don't see opening a dozen files as a big hurdle. I'd take smaller single purpose files over one big file anyday.

Plus how often do you need to see all functions? If there's a problem in RectangleArea function, you go to that file, no need to look in CircleArea. If you're adding 'StarShapeArea' you only care that it matches the callers expectations, not worrying about the other function logic.

Honestly this itself is a massive oversimplification, and a bit of a strawman, and kinda indicates that you still aren’t seeing the nuance.

Yes, if something is understandable in a single file, that’s fine. But also appreciate that too many pieces in the same file can also be confusing and disorienting for people. You also have to consider all the cases in which someone would be opening that file.

You’re basically posing a situation which by its very description is an exception. If anyone is slavishly following these rules, they’re undoubtedly doing it wrong.

Knowing when to apply the patterns is the hard bit. And, in my experience working on codebases worthy of considering these things in the first place, there’s much more to consider than what you’re posing.

I didn't watch the video but I read the article and in the later part he shows how to extend every versions (polymorphic, enum and table-based) to "computing the sum of the corner-weighted areas".

This extension is easy enough with in the non-pessimistic case (table based).

> Main selling point of clean code - the objective of writing clear maintainable, extendable code

That's the alleged selling point, whether even that's true is arguable.

Since while performance can be clearly measured - whether the given "clear code" principles do in fact help with writing "maintainable and extendable code" is something that can be argued back and forth for a long time.

Really enjoyed watching some guy Breathlessly discover data oriented design https://en.wikipedia.org/wiki/Data-oriented_design

there's nothing novel in this video, really nothing to do with clean code. This is same sort of thing you see with pure python versus numpy

This guy is so dogmatic about it it hurts. I would argue that clean code is a spectrum from how flexible vs how rigid you want your abstractions to be. If your abstractions are too flexible for good performance, dial them back when you see the issue. If your abstractions are too rigid for your software to be extendable, then introduce indirection.

We can all write code that glues a very fixed set of things end to end and squeeze every last CPU cycle of performance out of it, but as we all know, software requirements change, and things like polymorphism allow for much better composition of functionality.

Is there any flexbility tradeoff at all here?
The shapes example is pretty contrived so I don't really have an opinion on it either way. But imagine you have something like a File interface and you have implementations of it e.g. DiskFile, NetworkFile, etc., and you anticipate other implementors. Why would you do anything other than have a polymorphic interface?
I don't know, I've never done anything where code needed to have runtime dispatch on the kind of file it has without also knowing anything about what kind of file it has.
Have you never used C++ iostreams? Or the Python file abstraction? Or Rust std::io::Read/std::io::Write? Or Node.js streams? Or DOM Web Streams? Or Ruby files? Or Haskell conduits? Or hell, fopencookie/funopen in C?

The abstraction is super common and allows you to connect streams to each other without worrying about the underlying mechanism, which 99% of the time I don't really think you want to worry about unless you're sure it's a performance overhead. And that's great, because I surely don't want to write specializations by hand for all the different combinations of streams I need to use if I don't have to.

I've used open(2) and the io_uring openat :)

I use generic readers and writers every day, but they don't have any runtime dispatch. The question was "Why would you do anything except vtables and runtime dispatch?" One answer is that my code that uses generic readers and writers and gets monomorphized gets to also be generic over async-ness.

Except in the kernel, of course.
There you go, actually: you can have your clean code cake and eat it too if you have the right language abstractions (like parametric polymorphism).
I think the shapes example is more of a dig at various game engines where you end up with a long trees of inheritance (physicsbody -> usercontrollable physics body -> renderable user controllable physics body etc..) as opposed to the recent trend of using something like an Entity Component System.

Also I think he isn't specifically against "clean code" but how the first tool used by various "clean code advocates" seems to be polymorphism via inheritance. I have seen this enough in lots of Java codebases and "Enterprise C++ codebases". "We need X." "Oh first I will create an Abstract Base class for X, then create X, so we can reuse X nicely elsewhere when we need it". It is still on the developers to understand that they may not even need it but for them it is "clean code".

I wish he would've gone a bit more into why people reach for polymorphism instead of a struct with a variant enum: because it's how OOP is taught, and how it easily maps to human understanding of most problem spaces. like you're making an RPG, so you make an Item class, and then you make Armor and Weapon subclasses, and then you make Helmet/Chestplate/Leggings/Shoes and Sword/Polearm/Axe/Mace/Staff subclasses, etc. etc., just because it fits your mental model of the problem, when in actuality, you could've totally avoided all of that complexity, and the ensuing boilerplate, just by sticking it all in one struct and calling it a day. this is not always the solution, but more often than not it is.
That does depend on how the abstraction is defined, of course. I once worked on optimising a 2D Canvas C++ class which had a nice top level virtual interface so you could replace it with a different implementation. It was also crazily slow because it defined:

  virtual void setPixel(int x, int y, int color) = 0;
and then implemented flood fill etc in terms of that.
This isn't necessarily bad if all the other operations are also virtual. The idea is that you can quickly get something working (e.g. when porting) just by implementing setPixel(), and then gradually fill in other primitives with properly hardware-optimized versions. And you do need a virtual setPixel() in any case because some API client might need that one call.
I'm sorry, why do you need a virtual setPixel?

You can still easily get something working just by implementing setPixel without virtual dispatch, the linker has no problem inlining that call at compile time.

If some arbitrary API needs it to be virtual it's easy to implement the virtual call in just that specific case, instead of burdening your entire system with a virtual call that'll always be static in practice.

For the same reason why you need a virtual drawLine etc. Because some code wants to render something, and doesn't want to care if it's rendering onto an on-screen surface, into a file etc.

Your entire system will not be burdened with virtual calls in places where you use concrete implementations (so long as they're final/sealed, anyway). The overhead is only there if you try to use the abstraction generically, but why would you do that in the case where the virtual call "will always be static in practice"?

Anything that yields data in chunks will not really be affected by runtime polymorphism much.

It's only a problem when it's done per datum.

Right, but the point is you can avoid polymorphism within a given implementation but still keep it at a higher level.

Heck, even codebases used in trading can use polymorphism for very high level interfaces (or async tasks) but hot code paths don't use it.

But is it true that "clean code" makes the adaptation to changing requirements easier? I saw a few testimonies saying otherwise.
I think it really truly depends. I think it's always good to do the minimal viable thing first instead of being an architecture astronaut, but if you've been asked for three (random ballpark number) different implementations for the same requirement it might be time to start adding some indirection.
The best idea in clean code is to stop coupling domain models to implementation details like databases/the web/etc. Once you grok that, then you're in a better position to work on eliminating unnecessary coupling within the model itself.

There's lots of ways to do this poorly and well. There's no process for it. That's a feature. I feel like a lot of the flak clean code gets boils down to, "I followed it dogmatically and look what it made me do!" It didn't make you do anything; it's trying to teach you aesthetics, not a process. Internalize the aesthetics and you won't need a rigid process.

Obviously when you do this you probably need more code than you'd normally write. That can be viewed as a maintenance burden in some situations, esp. when you don't have product market fit. Again, this shows that treating clean code like some process that always produces better code in every situation is extremely naive.

(comment deleted)
Casey is a bit of a hardcore crusader on the topic, but I'd hardly call dogmatic someone who can provide you evidence and measurements backing their thesis.

The tests he put together here are hardly something I'd call a straw-man argument, they seem like reasonable simplification of real-cases.

Evidence in a micro benchmark of a single page of code.

The focus on performance here ignores the fact that most programs are large systems of many things that interact with each other. That is where good design and abstractions and “clean code” can really help.

Like all things it is about finding a balance and applying the right techniques to the right parts of a larger system.

True, the example is simplistic, but that's just to make it fit within reasonable exposition. The author has in the past shown (elsewhere) how his techniques can actually make dramatic differences in more concrete examples (he rose to some Internet fame for building a performant shell that could actually handle larger outputs orders of magnitude better than most available alternatives).

To me the interesting point is the reminder that there is an innate tension between going fast and being "clean" (i.e. maintanable/understandable). And once you are aware of it you can make your decisions in an informed way. Too often this tension is forgotten/ignored/dogmatically put to the back ("performance doesn't matter over cleanliness" and the likes).

Mind you, I'm also of the camp that performance is very secondary to cleanliness in modern enterprises, but I appreciate a reminder of just how much we are sacrificing on this altar.

It was Windows Terminal he wrote something faster than. Windows terminal was doing a whole GPU draw call per character (at 2x standard terminal height, 160x25, it was as many draw calls as a AAA game).
No matter what design choices you argue for there are always ways to be an idiot about the implementation
Which is an excellent argument for why the WT team should have done some benchmarking, identified that they have a really dumb O(M*N) bottleneck in a critical part of their application (their rendering code), and optimized it.

It is not an excellent argument for why 'clean code' is not a better fit for the other 99% of their codebase.

I'd say that it really depends on what the code needs to do.

For example, if the code is to be distributed and extended as a third-party library, then the class hierarchy is probably a better fit to allow extensibility.

But if the purpose of the API is to compute the area of given shapes (as in the example), then it makes sense to make it efficient and there is no use to provide extensibility to the outside world.

The advocates of "clean code" that Casey mentions will go for extensibility no matter the use.

I find it very interesting to have numbers to weight what you are leaving when you go the extensibility route instead of the non-pessimistic route.

> The advocates of "clean code" that Casey mentions will go for extensibility no matter the use.

Unfortunately this is just some catchy stereotyping that probably doesn't match reality.

I don't know. Before reading Casey, I'd say most, if not all, of my APIs would look like the "clean code" style.

The process was "think about the model, design a class hierarchy that fits with the model, add the operations". Even if I don't think about extensibility, that's how I thought my code.

Why? Because when I think about performance, I used to think about algorithms and I/O optimizations (for example batching).

Now I'll look into this data-oriented programming-thing and see how that applies to my platform (embedded Java) :D

The code examples with the abstract / virtual methods for the most straightforward concepts are a clown show. Not cleanliness but madness.
(comment deleted)
I've seen at least one embodiment of the stereotype 3 months ago. It wasn't pretty, I was able to shrink his code by a factor of 5, and make it more flexible in the process. Among other mistakes he had some idea of flexibility, put part of the infrastructure in place, and then utterly failed to use it, such that when it came to test he required a monkey patching framework for C (Ceedling) so he could mock what he almost already mocked in the source code.

That's a fairly rare breed for sure, but the real problem was that nobody called him out. Well I did, but I'm no longer working there. I couldn't.

Unfortunately, everything in our profession is a tradeoff. Faster and maintainable are two of the many quality metrics you can optimize for that will be at odds at times. What the right balance is for a given piece of code depends on so much context. It's a hard balance to get right.
> they seem like reasonable simplification of real-cases.

Paraphrasing Russ Ackoff, doing the right thing and doing a thing right is the difference between wisdom or effectiveness and efficiency. What Casey is doing here may be efficient, but calculating a billion rectangles doesn't present a realistic or general use case.

"Clean Code" or any paradigm of the sort aims to make qualitative, not quantitative improvements to code. What you gain isn't performance but clarity when you build large systems, reduction in errors, reduce complexity, and so on. Nobody denies that you can make a program faster by manually squeezing performance out if it. But that isn't the only thing that matters, even if it's something you can easily benchmark.

Looking at a tiny code example tells you very little about the consequences of programming like this. If we program with disregard for the system in favour of performance of one of its parts, what does that mean three years down the line, in a codebase with millions of lines of code?`That's just one question.

Performance measurements are only one dimension of code quality. Having a laser focus on it disregards why you would want to sacrifice performance for a different dimension of code quality, such as extensibility for different requirements.

You should check if your code is in the hot path before optimizing, because the more you couple things together the harder it is to change it around. For instance, in Casey's example, if you wanted to add a polygon shape but you've optimized calculating area into multiplying height x width by a coefficient, that requires a significant refactor. If you are sure you don't need polygons, that's a perfectly fine optimization. But if you do, you need to start deoptimizing.

These examples are absolutely a strawman. He's imagining there's one specific access pattern that's executed thousands of times per second. In a realistic codebase you're accessing the data less often but in multiple different (often subtly so!) ways. Cache efficiency is everything for modern CPUs, so you can't "simplify" the access patterns without making your benchmarks unrepresentative.
How is it a strawman when it's literally taken from a Clean Code textbook?
His benchmarks aren't taken from the textbook. If you benchmarked the textbook's changes in a realistic context you'd get quite different results.
What defines a codebase as realistic, exactly? There are many, many programs out there doing various things in various ways.
I don't think rich abstracions necessarily contribute to 'clean code' in fact often the opposite.

Clean code means easy to read, maintain, not full of arbitrary things 'because performance'.

Spectrum or I believe more of a Venn Diagram.

There was a moment in grade school where I was sat down and it was explained to me that you don't have to take a test in order. You can skip around if you want to, and I ran so far with that notion that at 25 I probably should have written a book on how to take tests, while I could still remember most of it.

One of the few other "lightning bolt out of the blue" experiences I can recall was realizing that some code constructs are easier for both the human and the compiler to understand. You can by sympathetic to both instead of compromising. They both have a fundamental problem around how many concepts they can juggle at the exact same time. For any given commit message or PR you can adjust your reasoning for whichever stick the reviewer has up their butt about justifying code changes.

0 responsibility developers

you have Japan infrastructure, and you have Turkey infrastructure

6.1 quake in Japan = nothing destroyed

6.1 quake in Turkey = everything collapses

The engineers in Turkey probably didn't value performance and efficiency

It's the same for developers, you choose your camp wisely, otherwise people will complain at you if they can no longer bear your choice

You act like innocent, but your code choice translate to a cost (higher server bill for your company, higher energy bill for your customers/users, time wasted for everyone, depleting rare materials at a faster rate, growing tech junk)

Selfishness is high in the software industry

We are lucky it's not the same for the HW industry, but it's getting hard for them to hide your incompetence, as more things now run on a battery, and the battery tech is kinda struggling

Good thing is they get to sell more HW since the CPU is "becoming slower" lol

So we now got smartwatches that one need to recharge every damn day

> The engineers in Turkey probably didn't value performance and efficiency

Uh, no. It was corruption. They were standards, that worked, but people didn't do it, plain and simple.

Yeah, it's a bad and unnecessarily inflammatory (and arguably disrespectful) example. But the rest of the points GP makes are spot on.

A "blame systems over individuals" version would be that the industry is externalizing bad performance onto users, damaging environment, causing frustration, wasting lives, and occasionally even actually killing people (shitty ER/hospital software comes to mind) - because there's no good feedback mechanism to force software companies to internalize those costs.

> Yeah, it's a bad and unnecessarily inflammatory (and arguably disrespectful) example. But the rest of the points GP makes are spot on.

I agree, i apologies for the mistake, i can no longer edit the post unfortunately

I think part of the problem with supposed "clean" code is that it tends to be a matter of opinion. Is the polymorphic version cleaner than the switch statement version? I would argue the latter is actually easier to read. There's no real reason to think "clean" code is actually clean other than anecdotes and that someone wrote it in a book, but the performance is something that can be objectively measured.
The "Clean Code" that Casey is talking about is a book and a code philosophy that was explained in depth in talks and trainings and seminars, so I would disagree that it is a matter of opinion.
Yes... but I'm also seeing dogmatism from the opposing camps here in the comments section.

The reality is that how flexible your interfaces and abstractions are and their design has to be a part of your original design considerations when building something. It's a bad move to just hand wave away performance concerns because you religiously adhere to some design patterns. It's also a bad move to drop down to using intrinsics for everything from the get-go and thinking you know better than the compiler when it's a codepath that isn't even computationally expensive or a bottleneck a priori.

The original submitted link was a youtube video that's been deleted for some reason.

Probably a better link is the blog post because the author updated it with the new replacement video a few minutes ago as of this comment (around 09:12 UTC):

https://www.computerenhance.com/p/clean-code-horrible-perfor...

We eventually merged all the threads hither. Thanks!
He re-uploaded the video for some reason.
I wish software engineering cared a lot more that we have no way of measuring how clean code is. Much less any study that measures the tradeoffs of clean code and other concerns, like a real engineering discipline.
The funny thing is that the things that are not possible to measure will be undone all the time because people can't agree on how it should be. This means that there will be wasted time.

First we have to write the code this way. Next year we have to write it in the other way. Then it has to be done in the first way again.

It's so hard to prioritize things when you ask someone why something has to be done the way they say and they are not able to give a real answer. I can do my job when option A means faster program and option B means more memory usage but I can't do my job when option A means faster program and option B is just the way it "should" be done.

This happens because software engineering doesn’t yet know the difference between principles and morals.
It is also important to consider that better performance also increases your productivity as a developer. For example, you can use simpler algorithms, skip caching, and have faster iteration times. (If your code takes 1min to hit a bug, there are many debugging strategies you cannot use, compared to when it takes 1s. The same is true when you compare 1s and 10ms.)

In the end, it is all tradeoffs. If you have a rough mental model of how code is going to perform, you can make better decisions. Of course, part of this is determining whether it matters for the specific piece of code under consideration. Often it does not.

Yes. When I worked as a game engine programmer, two of the first things I did were to improve the speed of compiling and starting the games. When those two things are faster, all development will be faster and more fun.
This is definitely not something that should be overlooked. Choosing a more mathematically optimal algorithm might be 2-3x faster in theory (at the cost of more complexity). If you're executing that algorithm a lot, to the point where a 3x speedup is significant, well -- if you can restructure the code in a manner similar to that demonstrated in the article (avoiding costly vtable dispatches, indirection, etc) and achieve a 25x with the original simpler algorithm, then that's something worth taking into consideration. A 3x algorithmic improvement is only impressive if there isn't 25x potential speedup low-hanging fruit (from simply not writing your code in a moronic way in the first place).
What is demonstrated here is that if you understand well the different parts of some code, you can recombine them in more efficient ways.

This is something very good to have in mind, but it must be applied strategically. Avoiding "clean code" everywhere won't always provide huge performances win and will surely hurt maintainability.

This seems more like an argument against the object oriented model of C++ than anything else. Would have been more interesting if the performance was compared to languages like Rust.
I think it could be ok to have a link every once in a while that doesn't talk about rust.
We're talking about C++ and performance, no way nobody would mention rust :P
My key learning is the importance of balancing performance and code cleanliness instead of blindly adhering to clean code principles.
If you optimize for readability performance would suffer. If you optimize for performance readability will suffer.

Casey prizes performance over everything else.

In this particular case I find the code optimized for speed (the one using switch) to be also more readable and simpler than the code using virtual dispatch.

The problem with virtual calls in a big project is that there is no good way of knowing what is the target of the call, without some additional tooling like IDE. But in case of a switch/if, it is pretty obvious what the cases are.

I do agree. I've looked at code bases where the switch was replaced with virtuals and the like. It's kind of hard to navigate around the code when that happens. I think I'd usually rather have a completely separate path through the code rather than switch or virtual, making the decision at the highest level possible, usually the top-level caller.
Sure, but what happens, once you want to start supporting other shapes other than basics? Because clean code assumes code will be changed/maintained.

Then you get people writing their own horrible hacks.

Both clean code and performance oriented design have their extremes.

Clean Code has Spring with Proxy/Method/Factory monster... and hyper performance has the extreme in the story of Mel (i.e. read-and-weep only code).

> Sure, but what happens, once you want to start supporting other shapes other than basics? Because clean code assumes code will be changed/maintained.

You get to push back and ask, is it worth the developer time and predicted 1.5 - 5x perf drop across the board (depending on shape specifics)? In some cases, it might not be. In others, it might. But you get to ask the question. And more importantly, whatever the outcome, you're still left with software that's an order of magnitude faster than the "clean code" one.

Clean code "assumes code will be changed/maintained" in a maximally generic, unconstrained way. In a sense, it's the embodiment of "YAGNI" violation: it tries to make any possible change equally easy. At a huge cost to performance, and often readability. More performant code, written with the approach like Casey demonstrated, also assumes code will be changed/maintained - but constraints the directions of changes that are easy.

In the example from video, as long as you can fit your new shape to the same math as the other ones, the change is trivial and free. A more complex shape may force you to tweak the equation, taking little more time and incurring a performance penalty on all shapes. Even more complex shape may require you to rewrite the module, costing you a lot of time and possibly performance. But you can probably guess how likely the latter is going to be - you're not making an "abstract shape study tool", but rather a poly mesh renderer, or non-parametric CAD, or something else that's specific.

> Sure, but what happens, once you want to start supporting other shapes other than basics?

Sure, but what happens, once you want to start supporting more operations on the shapes?

Add more abstractions of course.
Indeed. If anything this demo shows how badly C++ polymorphism performs. It doesn't necessarly means that all OOP languages created equal. Although I have no data to prove anything, and frankly don't care b/c all these arguments about clean vs dirty code are meaningless in an absence of formally defined rules and metrics universally enforced by some authority that can revoke your sw dev license or something like that
> It doesn't necessarly means that all OOP languages created equal.

Exactly - unless you're trying very hard, you're unlikely to beat C++ polymorphism with your OOP code in a different language. Which makes Casey's argument that much stronger. C++ with its relatively unsophisticated OOP and minimal overhead on everything, is as fast as you're going to get, so it's good for showing just how slow that still is if you follow the Uncle Bob et al. Clean Code tradition.

> C++ with its relatively unsophisticated OOP and minimal overhead on everything, is as fast as you're going to get

No it isn't. If your C++ compiler isn't devirtualising at all (implied by the article) it'll get stomped on by anything doing inline caching [0] which will generate the switch-case code. The JVM does that for example.

[0] https://bibliography.selflanguage.org/_static/pics.pdf

I'm surprised I had to scroll down this far to find the obligatory Rust evangelist comment.
This example exists in such a vacuum and is so distant from real software tasks that I just have to shake my head at the "clean code is undoing 12 years of hardware evolution"
It's an example from the Clean Code book itself, though?
It is, but that's the catch. It would be exceedingly hard to provide understandable examples of these things in code that is complex enough to need the patterns.
I don't think there is a contradiction or surprising point here.

At least my understanding of the case for clean code is that developer time is a significantly more expensive resource than compute, therefore write code in a way which optimises for developers understanding and changing it, even at the expense of making it slower to run (within sensible limits etc etc).

Externalised cost on the environment will hopefully one day be addressed.
Not to mention the users. For all the bullshit the marketing departments of every company spew about valuing their customers, software companies don't really give a damn about the many person-hours of users' lives they waste to save a person-minute of dev time.
Everything is a balance though, and the tradeoff may not be linear. Where they've landed on it may be the only way to get the software to customers at all in a cost effective manner.
> developer time is a significantly more expensive resource than compute

Depends on the number of invocations of the program.

Not just that.

The “developer time is valuable” mantra is thrown left and right, disregarding how much of that valuable resource will be wasted down the line due to bad implementations.

If we optimize for developer time, let’s optimize across the software’s entire lifecycle, not just that first push of a MVP to production.

This is a good point but it gets complicated as you often don't know the software's entire lifecycle so you have to optimize for something slightly different.
Yep, I think you’re right.

We just need to do away with shoddy first implementations, while avoiding premature optimization (or pointless perfectionism).

Which is all good in theory. In practice, management needs to also acquiesce and stop with the impossible deadlines.

I mean, I’ve worked on more than one projects that were sold to clients before they were implemented, and I think I’m not unique in that :)

Yeah you're right, nobody ever said to me "use interfaces because the code it's faster".

On the other hand, when I studied dynamic dispatch and stuff like that, I don't think enough people told me "when you do that, you are making this tradeoff".

I feel like it's worth sharing this kind of knowledge in order to make better informed decisions (possibly based on numbers). There's no need to become an extremist in either direction.

The trade off has changed over time as memory access has become a bigger and bigger bottleneck. But caring about this is still "premature optimisation".

The claim of 20x program performance difference is overblown. Compilers can often remove virtual function calls, JITs can also do it at runtime. Virtual function calls in a tight loop are slow but most of your program isn't in a tight loop and few programs have compute as a bottleneck.

Measure your program, find the tight loops in your program and optimise that small part of your program.

I think this is hilarious when talking about someone like Muratori, who has had his hands in more games' code than most people on this site have ever even played. He is incredibly productive by comparison to all the people making excuses why their programs are slow bloated garbage fires.
The context for the author is game development. A game can't just spin up a few more servers in the cloud. The slower the code, the smaller the customer base. Some fields have more severe constraints, where developer time may be expensive, but compute is priceless.

(This is not to endorse writing code that is harder than necessary to understand, or failing to document the parts that are necessarily hard.)

this video is part of a course that is not at all specific to game development, in any way whatsoever—where do these excuses come from?
> developer time is a significantly more expensive resource than compute

This also presupposes that making a fast program is a lot more work. However poor performance is usually due to negligence rather than a lack of optimization effort. All you need to write reasonably fast code by default (without micro-optimizing) is:

1. a good knowledge of available algorithms 2. a good understanding of the problem

1. Is a one-time investment on the programmers part that benefits all future programs they write. There is no marginal cost to being familiar with what's available in <algorithm>. 2. Has a marginal cost, but it's probably a time saver anyway. Measure once, cut twice.

TL;DR:

"Game developer optimizes code for execution as opposed to readability that 'clean-code' people suggest".

There are few considerations:

- most code is not CPU bound so his claims that you are eroding progress because you are not optimizing for CPU efficiency is baseless

- writing readable code is more important than writing super optimal code (few exceptions: gaming is one)

- using enums vs OOP is not changing the readability at least to me

I think we can have fast and readable code without following the 'clean-code' principles and at the end it does not matter how much gain we have CPU cycle-wise.

I'm tired of every tool I install on my latest gen Intel CPU + 32GB RAM + NVMe drive machine being a complete slog.

To each their own, but I don't find Casey's performant version less readable, I don't see the need for so many abstractions.

“CLEAN” doesn’t care so much about readability, but rather testability. The video would have been far more compelling if Casey had spent more time showing how he would test his application without the test surface area blowing up exponentially as the feature space expands.
>I don't find Casey's performant version less readable

It does create implicit coupling. If you try to add a new shape you will run into the problem.

In the clean code version, your compiler will remind you to implement calculateArea

With his version you have to add a new `case` to every switch statement and hope you didn't miss one with a default case, because the compiler won't catch this one.

It's a crap way to code

people using clean code ideologies are being prematurely pessimistic and assuming they know much more about a problem than they actually do when they use these clean code techniques. "I don't know how many shapes I've been asked to do, so I'll assume the worst case scenario and make the code slower and harder than the simple naïve solution that would be hard to read(debatable) if we had one million shapes" is a terrible argument and it is why everything goes slow.

The correct way to deal with this, is refactoring to a more maintainable code once you know the amount of shapes will wildly change, As soon as we get too many shapes as the problem has changed. You can only pretend to know what is the best architecture for a problem when you have dealt with it several times.

Clean code apologists pretend their single time dealing with website backend is proof enough that clean code works and that it works for every problem and that it has to be the default approach and is the most readable for most problems. It is a total insanity for something that can't be measured with any tool.

Edit:

I fully understand that "premature optimization is wrong" but using these "guidelines" is premature optimization of scalability and maintainability. Somehow when the "premature optimization" is about things you people want that's somehow okay? pff

Also, I don't find clean code readable, it looks like complex, un-refactorable garbage to me 9 out of 10 times. No wonder why people are so fucking scared of rewriting a class and act is if it will take months to do so, this ideology makes impossible to actually play around with your code, you can't neither make it more readable or more performant, you are locked in with a sluggish collection of dozens of files even for the simplest of problems.

It doesn't have to be slow. There's a reason "Clean code" is being criticized everytime it's mentioned. It touts inheritance and polymorphism as a solution to everything like it's 2002. There's been enough "Inheritance considered harmful" articles to toss that aside

The take on switch statements is covered in "The Pragmatic programmer" as well, which coincidentally is much less criticized when it comes to books about clean code.

The way to fix the switch is getting rid of the class "shape" and making it an interface, then implementing the interface in each shape, as a non-virtual method. And then you don't let people inherit. They can compose instead.

Performance is unaffected, you get rid of the switch, the compiler catches your mistakes for you, and everyone's happy

> people using clean code ideologies are being prematurely pessimistic and assuming they know much more about a problem than they actually do when they use these clean code techniques.

No. The techniques are there to let you change your codebase as you learn more about the problem.

there are two ways in which I can understand your message:

a) "these techniques allow for an easy to refactor codebase". I profusely disagree, its easier to refactor a function with some ugly switch than a behavior disseminated over dozens of files.

b) "we are actually careful and not draconian about these techniques only when appropriate", which I don't agree either, as in every experience I had interacting with people that believe in clean code in meatspace, was an obsession with having things done their way, assertions about "code smells" which were literally just not doing whatever they wanted.

maybe there's a c) I'm not seeing. But seeing this thread on its own its already high evidence that these two notions are clearly there in the CC community.

"we are actually performant" becomes "well, actually we are readable" becomes "well, actually we are testable" becomes "well, actually humm... just shut it and write on our code style". Some posts on this thread even talk about people not using these guidelines being evil and having to be ejected. Just imagine how people not suck up on this ideology look at it.

I think if you dig into why any of these tools perform poorly, you probably won’t find it’s because they were implemented using “Clean Code”, but rather that it’s down to a combination of many different things. I can’t say anything concrete because I don’t know which applications you are referring to.

But IMO framing it as clean vs performant is a mistake

"Clean code", "Clean architecture", "Clean etc", they are all totally grotesque and the sign of incompetence.
There is no doubting Casey's chops when he talks about performance, but as someone who has spent many hours watching (and enjoying!) his videos, as he stares puzzled at compiler errors, scrolls up and down endlessly at code he no longer remembers writing, and then - when it finally does compile - immediately has to dig into the debugger to work out something else that's gone wrong, I suspect the real answer to programmer happiness is somewhere in the middle.
If you're talking about Handmade Hero, the real answer to programmer happiness is not using a language you despise and refusing to leverage the features of, not refusing to use libraries in that language or frameworks, not re-implementing everything from first principles, and to actually have your game designed first (not designing while you code.)
Casey is a bad example of a game designer and he'll be the first to admit it. However, it is worth noting that Jonathan Blow very much does design while he codes and recommends the practice. He also generally abstains from library dependencies and implements a lot of thing himself.

Of course, part of the point of Handmade Hero is to show that you can totally reimplement everything from first principles. Libraries are not magical black boxes, they're code written by human beings like you or me, and you can understand what they're doing.

For instance, he wrote his own PNG decoder[0] live on stream, with hardly any prior knowledge of the spec, even though I'm confident that under normal circumstances he'd just use stb_image. I'm sure he did this just to show how you'd go about doing that sort of thing.

[0] He only implemented the parts necessary to load a non-progressive 24bit color image, but that still involved writing his own DEFLATE implementation.

Is Blow even a good example to look at? He's released 2 games in 18 years which definitely had phenomenal game play but are not technically complex even for the the time.
Yes, he is a good example. His games are as highly regarded as they are because he takes the time to really work through everything about them and ensure they're the product he wants. If you watch his streams you'll see that he is constantly experimenting with things, both gameplay-wise and in the engine... and now in his own compiler for his own programming language.

Jon is not making the by-the-numbers annual entry in the Call of FIFA series here.

But also, as a nit pick, Jon isn't just programming all the time, he's running a business and he is very involved in the indie game community and a founding member of indie fund. And when he is programming it isn't always for his own games. Here is a link to the credits page for him at MobyGames:

https://www.mobygames.com/person/188969/jonathan-blow/credit...

Where in addition to nebulous "Special Thanks" credits, you will note programming and QA credits on several non-Thekla games.

Look I'm a huge fan of his games! And I really can't understate how influential his game design nor the work he does for the indie community. However, I think a lot of folks tend to assume his software engineering skills are great because his game design is excellent. I don't think that's an earned position. Releasing 2 games in 18 years that are not pushing the technical envelope does not scream software engineering expertise. You say he has more credits but only one of those is programming since Braid. On the other hand, on a software engineering level those paint by numbers Modern Warfare and FIFA games are both more technically impressive and are designed for fast iteration.

Moreover he's pushing a particular paradigm and view point that is in many ways the opposite of clean code. He pushes for still doing very low level design with minimal abstractions. But even at the time Braid could have been written in python SDL wrappers and probably had similar performance, and the witness could have used unity. If clean code is about maintenance and time to market, the Blow paradigm hasn't proven that its needed or fixes the holes in clean code. This is not to say clean code is perfect just that Blow hasn't cracked the nut either and I don't know why people act like he is the final word, or honestly even a respected voice, in game software engineering. On the other hand, if Blow wanted to talk about managing indie studios or game design my ears would prick up instantly.

> I don't think that's an earned position.

I disagree.

> You say he has more credits but only one of those is programming since Braid.

He did start a company after that you know. A successful one that makes money and employs people to make art. I don't imagine that running a business takes no time from his life.

> But even at the time Braid could have been written in python SDL wrappers and probably had similar performance

Braid did a lot more than you give it credit for. Here's a GDC talk about the rewind system in which he explains some of the hurdles he had to deal with: https://www.youtube.com/watch?v=8dinUbg2h70&t=5s Pay particular attention to the discussion of the background particles and how to get that to work within the RAM constraints.

> On the other hand, on a software engineering level those paint by numbers Modern Warfare and FIFA games are both more technically impressive and are designed for fast iteration

Hardly anything about these games change from release to release. They're not exploring new gameplay problem spaces, they're not doing anything super interesting or surprising on a technical level either and I don't get why you think they are. Of course if you keep using essentially the same engine and know exactly what you're trying to make, making another like a goddamned factory is going to happen quicker than if you're trying to make something unique and meaningful.

> If clean code is about maintenance and time to market, the Blow paradigm hasn't proven that its needed or fixes the holes in clean code.

I have heard "maintenance and scaling" as an excuse for poorly performing software for a long time now, yet what I'm not seeing is software that has features added on quick schedules and without bugs. So at best I'd say that it isn't accomplishing what it is supposed to and, at the same time, it is wasting our time and resources by producing slow software to boot.

Doing things without libraries (except the stuff built into Windows XP?) is the whole point of the project.
But the results of living in a contrived environment is that there are no guarantees that at the end you will understand 'normal' environments better or just be over-trained on your fictitious one.
The problem is people get hooked into Casey's misanthropic philosophies and take his rants as gospel, and wind up believing Handmade Hero represents the way game development should be done, that libraries and frameworks can't be trusted, that C++ shouldn't be used at all, and you should implement as much from scratch as possible, when none of that is true.
> and to actually have your game designed first (not designing while you code.)

tell me you've never made a game without telling me you've never made a game

hahahaahahaha, that's accurate af
When working with a larger code base, there will always be parts that you don't remember writing and you'll inevitably have to read the code to understand it. That's just part of the job/task, regardless of the style it's written in.
In shared code particularly with a culture of refactoring, there's no guarantee that the function call you see is doing what you remember it doing a year ago.

When I was coming up I got gifted a bunch of modules at several jobs because the original writer couldn't be arsed to keep up with the many incremental changes I'd been making. They had a mentality that code was meant to be memorized instead of explored, and I was just beginning to understand code exploration from a writer's perspective. So they were both on the wrong side of history and the wrong side of me. Fuck it, if you want it so much, kid, it's yours now. Good luck.

Why were you making incremental changes?
"Make the change easy, then make the easy change" hadn't even been coined as a phrase yet when I discovered the utility of that behavior. When I read 'Refactoring (Fowler)' it was more like psychotherapy than a roadmap to better software. "So that's why I am like this."

When we get unstuck on a problem it's usually due to finding a new perspective. Sometimes those come as epiphanies, but while miracles do happen, planning on them leads to disappointment. Sometimes you just have to do the work. Finding new perspectives 'the hard way' involves looking at the problem from different angles, and if explaining it to someone else doesn't work, then often enough just organizing a block of code will help you stumble on that new perspective. And if that also fails, at least the code is in better shape now.

Not long after I figured out how to articulate that, my writer friend figured out the same thing about creative writing, so I took it as a sign I was on the right track.

I do know that the first time I was doing that, it was for performance reasons. I was on a project that was so slow you could see the pixels painting. My first month on that project I was doing optimizations by saying "1 Mississippi" out loud. The second month I used a timer app. I was three months in before I even needed to print(end - start).

> there's no guarantee that the function call you see is doing what you remember it doing a year ago.

TDD provides those guarantees. If someone changes the behaviour of the function you will soon know about it.

That's significant because Robert 'Clean' Martin sells clean code as a solution to some of the problems that TDD creates. If you reject TDD, clean code has no relevance to your codebase. As Casey does not seem to practice TDD, it is not clear why he though clean code would apply to his work?

It doesn't. TDD is about writing new code. It doesn't say anything about existing tests being sacrosanct, or pinning tests sticking around forever. I can extract code from a function and write tests for it. I probably know that there's still code that checks for user names but I can't guarantee that this code is being called from function X anymore, or whether it's before or after calling function Y. Those are the sorts of things people try to memorize about code. "What are the knock-on effects of this function" doesn't always survive refactoring. Particularly when the refactoring is because we have a new customer who doesn't want Y to happen at all. So now X->Y is no longer an invariant of the system.
> TDD is about writing new code.

TDD is about documenting behaviour. Which is why it was later given the name Behaviour Driven Development (BDD), to dispel the myths that it is about testing. It is true that you need to document behaviour before writing code, else how would you know what to write? Even outside of TDD you need to document the behaviour some way before you can know what needs to be written.

A function's behaviour should have no reason to change after its behaviour is documented. You can change the implementation beneath to your hearts content, but the behaviour should be static. If someone attempts to change the behaviour, you are going to know about it. If you are not alerted to this, your infrastructure needs improvement.

> A function's behaviour should have no reason to change after its behaviour is documented.

That's only true with spherical cows. That something happens is a requirement. When it happens is often only as specific as 'before' or 'after' but tests often dictate that they happen 'between', which is not an actual requirement, it's an accident of implementation. It was 'easy' to put it here.

Nowhere is it written that behavior in a system is strictly additive.

Systems are full of XY problems. When you recognize that, and start addressing that problem, you sprout a lot of tests for the Y solution and block delete tests for the X solution. That behavior doesn't exist in the system anymore because it's answering the wrong question. Functional parity tests can be copied, or written in parallel. But the old tests disappear with the old code (when the feature toggle goes away).

Leaving the code for X around is at best a footgun for new devs, and at worse a sign of hoarding behavior of an intensity that requires therapy.

You're espousing a process whereby you've nailed one foot to the deck, preferring form over function. Whether you believe what you're saying or not I can't say, but it's restrictive and harmful.

> Nowhere is it written that behavior in a system is strictly additive.

For a unit of the same identity to suddenly start doing something different is plain nonsensical, never mind the technical challenges that come with breaking behaviour that should scare anyone away from trying. Logically, a unit is additive until the unit are no longer used, at which point it can be eliminated.

> But the old tests disappear with the old code (when the feature toggle goes away).

Absolutely, but static analysis can easily determine that the tests being removed correspond with units being removed. If (TDD) tests are removed and the unit code isn't, something has gone wrong and your infrastructure should make this known.

You should reread “Refactoring”.

Refactors compose. In three months you can completely rearchitect a module without breaking it at any point in the process. That’s the promise of refactoring.

Functions don’t have an identity. There is no such thing. I don’t know who taught you that but they have broken you in the process. Renaming things is a refactoring. We don’t check the entire commit history to make sure that function name has never existed. Only that it hasn’t existed recently. There’s no identity.

One of the reasons to refactor is that the function has been lying about its responsibilities. So you extract steps out of it, create a new call path that fixes the discrepancy, migrate the call sites, delete the incorrect function, and then, if the function name was really good, you might wait a while and rename the new function to the old name. Each step makes sense if you’ve followed the entire process. If you haven’t been following along at all then you have absolutely no idea how things got here until you read the git history thoroughly, which some people can’t do, and others won’t do if they expect the code to be static.

> You should reread “Refactoring”.

With respect, you should reread the comment chain. You're clearly just repeating what has already been said.

Also, to clarify, I'm talking about cumulative changes. If I'm working with someone on a feature then we both see all of the changes as they occur. If I'm off dealing with some long initiative, I may not look at that code for 3 months and so I miss all of the intermediate states that made perfect sense at the time.

Like visiting a friend who did their own house remodel. Their spouse saw all the steps, all you saw was before and after, and so the fact that the bathroom door is missing is confusing. The bathroom still exists, but now it's the master bath.

You seem to ignore that when the unit changes, the tests do too. If you come back a year later, foo.bar.baz(quux) might have been refactored and lazily so. The tests were also updated and still pass. You may jump into the code only to realize that someone no-op'd everything and never removed call sites. TDD is primarily a design tool, not a lock-into-implementation tool.
Someone has put some notion of function identity in his head and I don’t know what school taught that notion but it needs to burn.

IBM’s Visual Age tried to behave that way and it didn’t end well. Eclipse dropped that conceit when it forked.

> Someone has put some notion of function identity in his head

The only comment that talks about function identity is this one[1], and it is written by you. "His head" refers to your own?

I used the word identity earlier in that thread, but I definitely wasn't referring to functions. The word "function" isn't even found in the comment.

[1] https://news.ycombinator.com/item?id=34983709

Of course any code requires some refresher at time, but the difficulty and time required to figure it out again is a spectrum that goes all the way down to the seventh circle of hell.
> There is no doubting Casey's chops when he talks about performance,

Remember that everyone has their blind spots!

I follow Casey on twitter, and a couple years ago there was a weird thread where he had hung his browser for 4-5 seconds by running some JS to assign CSS rules to ~50K div elements. And Casey was a million percent confident that the hang was due to JS being slow, and had nothing to do with CSS or DOM rendering.

Because enterprise Java programmers never stare puzzled at anything.
Much of this is very compiler dependent. For example, Java's compiler is generally able to perform more aggressive optimisations, and even virtual calls are often, and even usually, inlined (so if at a particular call-site only one shape is encountered, there won't even be a branch, just straight inline code, and if there are only two or three shapes, the call would compile to a branch; only if there are more, i.e. a "megamorphic" call site will a vtable indirection actually take place). There is no general way of concluding that a virtual call is more or less costly than a branch, but the best approximation is "about the same."

Having said that, even Java now encourages programmers to use algebraic data types when "programming in the small", and OOP/encapsulation at module boundaries: https://www.infoq.com/articles/data-oriented-programming-jav... though not for performance reasons. My point being is that the "best practice" recommendations for mainstream language does change.

> and even virtual calls are often, and even usually, inlined.

Last time I checked it could not inline megamorphic call sites, evn if implementations were trivial (returning constants). At the same time I saw C++ compilers able to replace an analogue switch that dispatched to constants with a simple array lookup, with no branching at all.

If I'm not mistaken, C2 inlines up to three targets, but of course, as a JIT, it inlines more aggressively than an AOT compiler, as it does not require a soundness proof.
Most of those Clean code rules are BS.

1. Prefer polymorphism to “if/else” and “switch” - if anything, that makes code less readable, as it hides the dispatch targets. Switch/if is much more direct and explicit. And traditional OOP polymorphism like in C++ or Java makes the code extensible in one particular dimension (types) at the expense of making it non-extensible in another dimension (operations), so there is no net win or loss in that area as well. It is just a different tool, but not better/worse.

2. Code should not know about the internals of objects it’s working with – again, that depends. Hiding the internals behind an interface is good if the complexity of the interface is way lower than the complexity of the internals. In that case the abstraction reduces the cognitive load, because you don't have to learn the internal implementation. However, the total complexity of the system modelled like that is larger, and if you introduce too many indirection levels in too many places, or if the complexity of the interfaces/abstractions is not much smaller than the complexity they hide, then the project soon becomes an overengineered mess like FizzBuzz Enterprise.

3. Functions should be small – that's quite subjective, and also depends on the complexity of the functions. A flat (not nested) function can be large without causing issues. Also going another extreme is not good either – thousands of one-liners can be also extremely hard to read.

4. Functions should do one thing – "one thing" is not well defined; and functions have fractal nature - they appear do more things the more closely you inspect them. This rule can be used to justify splitting any function.

5. “DRY” - Don’t Repeat Yourself – this one is pretty good, as long as one doesn't do DRY by just matching accidentally similar code (e.g. in tests).

I think if someone takes any of these typical guidelines - clean, SOLID, REST etc - and mindlessly applies it they’re likely to end up with a few parts of their applications which look weird, or perform poorly or end up being worse somehow. This is because there are inevitably going to be situations where the guidelines don’t fit well - they’re not necessarily hard and fast rules after all.

Any time you have these common rules of thumb in any part of your life you need to evaluate whether or not they are appropriate. But just because they’re not infallibly universal, doesn’t mean they’re wrong, it just means life throws complex situations at us sometimes, and that we need to be pragmatic and flexible

In his small example he already added unforeseen couplings that could get out of hands if it was a big codebase. If you follow the principle "switch statements over [X]", try to add a new shape down the line and see how quickly you run into problems.

In the clean code version, your compiler will remind you to implement calculateArea, calculateNumberOfVertices, calculateWhatever, and so on and so forth.

With his version you have to add a new `case` to every switch statement and hope you didn't miss one with a default case, because the compiler won't catch it.

You don't need virtual functions and polymorphism to remove his switch statements. Just compose over inheritance.

> With his version you have to add a new `case` to every switch statement and hope you didn't miss one with a default case, because the compiler won't catch it.

The compiler not catching it is a limitation of the language he uses, not the limitation of the general concept of switch / pattern matching. Scala, Haskell, Rust do catch those.

> If you follow the principle "switch statements over [X]", try to add a new shape down the line and see how quickly you run into problems.

And who said you'd ever need to add a new shape? Maybe you will need to add a new operation? Try to add a new operation `calculateWhatever` and see how many places of the code you need to chnage instead of just adding one new function with a switch.

Often you really don't know in which direction the code will evolve. Most often you can't guess the coming change, so don't make the code more complex now in order to make it simpler in the future (which may never come).

>The compiler not catching it is a limitation of the language he uses, not the limitation of the general concept of switch / pattern matching. Scala, Haskell, Rust do catch those.

You're thinking of defaultless switch statements. Rust and Typescript catch those issues as long as you don't add a default. I'd already thought of it when I typed it

>And who said you'd ever need to add a new shape?

ô_o

>Maybe you will need to add a new operation? Try to add a new operation `calculateWhatever` and see how many places of the code you need to chnage instead of just adding one new function with a switch.

You do realize that your "one function with a switch" does have to cover every case exactly like the clean code version, it's not magic you're just fitting it all into one switch/case, and you'll probably end up extracting those case: blocks into separate functions anyway.

And on top of that you're not safe from a colleague adding a useless "default" case at the end ouf ot paranoia and making your compiler not catch a future problem when a new shape gets added.

You are not safe from a colleague adding a dozen classes to add two numbers if you are enabling the "clean" crowd either. When I was at uni I was obsessed with these clean ideas and how they would allow big teams to work. after years of working I have seen these ideas fail to actually make teams work in an harmonious manner, worst hit I have experienced against these ideologies was finally working in a place without them and seeing how a lot of the alleged "advantages" of "clean code" could be achieved by other simpler means and how much in the way these ideologies are of actually being productive.

Productivity is achieved in spite of clean, not thanks to it.

>You are not safe from a colleague adding a dozen classes to add two numbers

The difference is, adding "default" cases to complete switch statements happens all the time, you've probably witnessed it, I know I have, whereas colleagues adding a dozen classes to add two numbers doesn't.

You're also never safe from meteorites crashing your building, let's talk about real antipatterns

> You're thinking of defaultless switch statements. Rust and Typescript catch those issues as long as you don't add a default. I'd already thought of it when I typed it

But the same problem exists with interfaces and virtual dispatch! If you provide a default implementation at the interface / abstract class level, then the compiler won't tell you forgot to implement that method, because it would see the default one exists.

Default implementations at the interface level, wow, apparently it's a thing in Java. I don't think it's in C++.
> he already added unforeseen couplings that could get out of hands if it was a big codebase

And one should be able to fix things that get out of hand when they start getting out of hand. Architecture should be based on current facts, not on our fantasies about the future.

For some reason we're always making this weird assumption that future engineers working on a problem are going to be less capable than us. So we decouple things in advance for them. Those future idiots won't know how to architect for scale, but we, today, with our limited domain expertise, know better.

The reality is that in most cases we are those future engineers. And, unsurprisingly, "future we" tend to know more, not less.

> Prefer polymorphism to “if/else” and “switch” - if anything, that makes code less readable, as it hides the dispatch targets.

Worse, I think, is that it hides the condition, which can be arbitrarily far away in space and time.

Certainly dynamic dispatch can be very useful, and the abstraction can be clearer than the alternatives. A rule of thumb is to consider whether you'd do it if you were writing in C, using explicit tables of function pointers. If that would be clearer than conditional statements, do it.

(In case it isn't obvious, I'm talking about languages in the C++/Java vein here.)