Comment by bunderbunder
15 hours ago
I’ve had code review pushback against reorganizing data structures to be more cache-friendly on the grounds that it was “premature optimization” even though I pre-emotively justified the decision with benchmark results indicating that the performance difference was large and business relevant, and addressed a real performance bottleneck that I observed during testing.
This was on a team and at a time where Clean Code was very popular. I somewhat wonder if the problem is that, in the 2010s, many people became convinced that certain aesthetic standards were both objectively, provably correct, and more important than anything else.
A lot of my poor opinion on developer opinions dates back to a couple projects where Product and Marketing were begging us for substantial performance improvements and the teams pushed back saying this was as fast as you could expect. I got 2x in the first six months on both projects, and delivered 25% per quarter on one for almost 2 years (that maths to around 8x) before I started to scrape the bottom of my barrel.
It’s predominantly a way to shut down thinking, which is why it’s so often misquoted. Knuth’s version is more nuanced. Theirs is not at all.
As a compromise I got really good at doing optimizations that also improved readability. Things like hoisting and better variable naming combined together can be mistaken for just code cleanup.
I also discovered that keyhole optimizations are more palatable. Which is to say, move the slow bit yo a separate function with a single responsibility. Then do the optimization in a separate commit. Then the other devs don’t have to look at your changes and if they turn out to have a bug, we can always revert the change later. They almost never do, but having the option seems to grease the wheels.
My general rule of thumb is that if you see you can 2x it easily, you can actually get somewhere around 10x with effort, since the presence of that easy 2x means nobody really spent any time on performance.
i got over 800 this way. O(n^2) everywhere and n+1 queries
3 replies →
Nitpick but 25% improvement over 8 quarters is 1.25^8, almost a runtime in 1/6th of the time.
25% faster is 1.33x work done in the same time. What you want is 0.75^7 (almost 2 years) gives you 13.3% of the original time, which is 1/7.5. Only it was more like 25-30% so I rounded up.
1 reply →
That’s just pattern matching. Most people don’t do this level of prep so they assume you didn’t either. There’s usually no ill intent, and people are maybe a little embarrassed when you point it out, but if you are tactful it’s fine.
All types of architecture have advantages and disadvantages. Clean code might not be very cache-friendly, but it does a pretty good job of making sure you don't have to hunt in too many places to update things and It also makes it pretty simple to split things up so that you can write unit test for most everything.
This is not the right choice for everything, just as using Python is not the right choice for everything. The question to ask is: what are we trying to do in the first place? Then take it from there.
You did that by making sure the speed-up was business relevant. I'm mostly putting this out to force people to think about whether that also applies to their specific thing.
"Clean" code for modelling links between objects would be to store pointers within them. But that means dangling pointers when deleting objects, so you need to either update manually everywhere (easy to forget somewhere) or use shared/weak pointers (extra bookkeeping and allocations).
"Unclean" code for this is putting objects in a table, giving objects a generation (that increments whenever you create/delete one) and storing pairs of (index, generation) numbers everywhere you need a link. Link is live only if the generations matches and since only odd generations are live objects, default construction means no link. Lifted straight from 2000s video game tricks and with modern C++ it's basically all encapsulated away.
I've made that refactoring in a codebase, not because of speed but because of maintenance and memory fragmentation concerns. Sometimes, the clean solution really isn't the right one and it's really sad that compsci only ever teaches classical data structures, like we're still coding in the 1970s on computers with no caches that don't suffer from pointer chasing.
> objectively
I see what you did there
Processors I mostly write firmware for have little cache at all.
I think cache friendly code also tends to be reason about friendly and debugger friendly. None of that is remotely premature optimization.
> I think cache friendly code also tends to be reason about friendly and debugger friendly.
Not sure what examples you have in mind, but the very first things I think of when I hear cache friendly code are cache-blocking and cache-oblivious algorithms, which are quite the opposite of that. See also B-trees vs. binary search trees, etc.
I usually start with simpler things like paying attention to locality of reference and adjusting data so that it takes up less space in memory.
Even CPUs like the ARM cores have caches, usually 16K, occasionally 32K, and even larger L2 caches (512K).
Before AI, nobody has been responsible for more psychic damage against software developers than Bob Martin.
From what I understand, it is mostly the very rigid and prescriptive attitude which the average reader gets from his book, which, if you listen to him talk, was not his intent. He even published a new 2025 edition of “Clean Code” to try and fix this, by making the text less self-assured. So while he may or may not be responsible, he did not intend it.
I liked the book, so did my colleagues, and we even had a meetup to discuss the book on how to improve the legacy codebase we had inherited.
think you mean "psychological damage". Can you give some examples of what you mean?
I’m not who you were replying to, but there was a thread here a couple of years ago in which many people chimed in with examples. I added my two cents here, but the whole discussion is much more worth reading than my gripes: https://news.ycombinator.com/item?id=43166836
Psychic is a damage type in D&D (and probably other RPGs too), so that is most likely a reference.
1 reply →
[flagged]
2 replies →
There's not enough context for me (or other commenters) to really evaluate this, but every change has risk, and some projects aren't in a position to be accepting that risk.
A "real performance bottleneck that I observed during testing" is a little bit unclear whether it occurred outside of test conditions, or whether speeding up that particular bit improves anything anyone cares about.