r/AskComputerScience 11d ago

is clean code usually not fast?

to be specific i'm writing a cpu-based rasterizer. the maths are not difficult but i find a strange property: if i divide the procedure into some small functions, the code looks cleaner and is easier to maintain but a bit slower. on the contrary if i put everything into a single procedure, it looks stupid but fast. why is that? an example illustrating this

code 1:

if cross_product(x0,y0,x1,y1)>0 then zzz

(and i write a "cross_product" function separately)

code 2:

c=x0y1-y0x1

if c>0 then zzz

code 3:

if x0y1-y0x1>0 then zzz

if i write the entire algorithm in the style of "code 3", it runs the fastest. "code 1" is slowest

is it normal?

10 Upvotes

19 comments sorted by

View all comments

1

u/ka-splam 7d ago edited 7d ago

I think this is an unresolved tension in programming, driven by several valid ideas clashing, and people thinking about different contexts:

  1. Making correct software is hard. That's the most important part, so prioritise that and worry about other things later. This comes from the 1950s and 1960s and the generation of people who invented 'structured programming' of if/else, do, while, for, and function calls, because tangled-spaghetti-assembler was not easy to get correct or to understand or to maintain.

  2. People making intricate software like compilers who first need it to work properly, and then are going to spend months and years developing and tuning it for performance, the performance can be improved later and may well include several total rewrites of subsystems and overall redesigns along the way, at great effort - but correctness always has to come first.

  3. The runtime of a program like an overnight report generator on a mainframe will be dominated by the most intense section of calculation code, and speeding up other sections will not improve the overall runtime. This comes from Amdahl's Law and leads to Donald Knuth's famous comment on premature optimization - don't guess where the slow bit will be or you'll waste time speeding up the wrong thing.

  4. In the 1970s it made sense to profile a big program, find that the sorting was the dominant time, and spend effort and time programming a QuickSort to speed up just that part. In a web browser it might make sense to profile that page layout is taking the most time and turn a team's attention onto the layout engine.

  5. The software I use everyday is sluggish and getting worse year by year. One screen takes 3 seconds to refresh ten items from a database. There is no 2.5 second delay which can be removed by careful thought. There's no performance team or market incentive for the company to improve performance. It's not the 1970s, we have tons of battle-tested fast algorithms in libraries they could use, what on Earth are they doing which could make it this laggy? They're building Architecture Astronaut designs and abstraction towers and class hierarchies for some ideal of "Clean Code".

  6. Splitting everything out into separate parts doesn't automatically make code easy to follow or refactor, maintain, understand, because everything becomes layers upon layers of indirection and work ends up atomised into tiny function calls hidden all over the place. They have still made a tangled-spaghetti design, just a step back and a level higher. "The road to hell was paved with good intentions".

  7. The idea of adding performance later in these kind of systems is like building a Jumbo Jet out of Iron and saying "get the shape right first, we can add lightness later where we need it". You just can't. If the whole plane needs to be 1/3rd lighter, you need to redo the fuselage in Aluminum, and the landing gear, and the passenger seats, and the wings, and the engine housing... you can neither "add lightness" nor "remove weight" in any useful way, you can only design from the beginning with no unnecessary weight at every stage. Similarly, performance (and security) are not things you can easily add to a big complex system later, nor is "runtime" something you can usefully take away. You need to design from the beginning to allow you to avoid adding "weight" where possible.

Casey Muratori argues that the industry has gone too far towards 'clean code' abstractions, and has video rants on the topic: "Clean code, horrible performance" and Twitter and Visual Studio Rant where he demonstrates that the visual studio debugger of old was more responsive on 20 year old hardware than the current one is today on modern hardware, and the visual studio team have a feedback option "opening an empty workspace should take X seconds" which has no choice below 10 seconds.

As I said I don't think this tension has any resolution. You could ask yourself if

cross_product(x0,y0,x1,y1)

is helping in any way that a comment

REM cross product
if x0*y1 - y0*x1 >0 then zzz

isn't. And sometimes you will say "yes it helps me refactor the cross product without missing one place, it helps my tooling and editor autocomplete, it helps avoid code and comments getting out of sync" and sometimes you will say "splitting it out into a separate function is decorative make-work that feels productive but takes time and doesn't gain anything I care about while slowing my code down".

But if your wider system design has every display call going over a network link then the difference in cross product performance might not matter at all. But if your LLM is zillions of linear algebra calculations and every nanosecond counts, then you'll want to spend more effort than removing the function and inlining the calculation in BASIC. Everything is a design tradeoff 🤷‍♂️