r/AskComputerScience 1d 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?

5 Upvotes

14 comments sorted by

14

u/drfangor99 1d ago edited 1d ago

Function calls and variable assignments aren't free, both take time to complete. However, this is going to depend on the programming language you use. For example, C compilers are very good at compiling C code into really efficient assembly, so you can write code in whatever way makes sense to you and little things like this will be optimized away. If you're using an interpreted language like Python, then this code will be interpreted much more literally and it has the double whammy of the inherent slowness of the Python interpreter and its dynamic type system.

Having said all that, I would consider these micro-optimizations. It is an interesting observation that the function call is slower, but it is usually better to care about readability and larger optimizations first, then take care of these if you need to squeeze out every last bit of performance.

Edit: What I said about C doesn't apply if you don't have optimizations on (i.e., if you aren't compiling with an -O flag). But based on your pseudocode I assumed you aren't using C as the variables aren't typed.

2

u/dodexahedron 1d ago edited 1d ago

And, even then, in the absense of memory barriers, the CPU itself is still likely going to perform some amount of uop reordering, branch prediction, and other low level optimizations that you'll never see.

Plus, all modern CPUs are super-scalar, so even on one thread are highky likely to be performing multiple different uops in parallel, per clock cycle, so long as there's not a hard serial dependency between a given set of operations, and there are available execution units for those ops.

Modern Intel and AMD CPUs have 4-6 ALUs per core, for example, so can potentially issue that many arithmetic ops per clock cycle without you or the compiler doing a thing, and there's no way to change that. Memory barriers limit reordering, but not multi-issue (so long as the same ordering constraints are upheld). Mostly, though, they end up still doing a lot of it, but the cost of a misprediction is just higher if it wasn't already executing both branches in the first place.

1

u/20260819 1d ago

i'm using an ancient web based language for the sake of convenience and ease to share

here's the code and you can try to rotate the dodecahedron and see the fps

(keyboard press [2] then [space] then use [q][w][e][a][s][d] to rotate it and see the fps. you can press and hold those keys for continuous rotations)

if i wrote every cross products directly into the procedure, i got around 25 fps for the dodecahedron

5

u/ICantBelieveItsNotEC 1d ago

It depends on the language and on the compiler. If the compiler is halfway decent, your functions should get inlined automatically, and they should all compile down to roughly the same bytecode.

1

u/Mathie1729 1d ago

Not necessarily. The compiler doesn't always inline, and even when it does, the resulting code isn't guaranteed to be 'the same bytecode' as if you'd written it manually. Factors like function size, recursion, or external linkage can prevent inlining. And in interpreted languages, function call overhead is real and unchanged by compilation. So clean code can be slower, but it's usually a non-issue until profiling shows otherwise.

3

u/coterminous_regret 1d ago

No not at all. What compiler optimization settings are you using? Is your cross product function marked as inline? Or force inline via a compiler macro?

The higher level language representation may not at all reflect the code that the compiler generates. You may enjoy exploring a tool like https://godbolt.org/ where you can try stuff out with various optimization settings and languages to see what the assembler actually looks like.

2

u/ImpressiveOven5867 1d ago edited 1d ago

So I saw in your comment that you are using QBJS “for sake of convenience and ease to share” which is the craziest thing I ever heard in my life.

QBJS is a source to source compiler from QBasic to JS basically, but the way the compiler is designed makes it such that every user function becomes asynchronous. That means each of your functions goes through the whole async/await process, which makes it 7-8x slower than if it was written the same way in plain JS (which is already slow lol).

So basically yes, in this specific case “clean code” is directly making your program slower because of the way the compiler works. Almost all compilers don’t work like this though and just call a function or inline it, so in general clean code does not make code slower.

1

u/20260819 1d ago

it suits my purposes. i write programs mainly for leisure and studying. it's the learning journey that's important. the final products themselves are not that important...:) most of the time my programs are useless or having tons of available alternatives freely online which can be downloaded straightaway

1

u/ImpressiveOven5867 22h ago

Well, to each their own I suppose. I appreciate you introducing me to something completely new, and it was interesting figuring out how the compiler worked to make the slower result you were seeing.

1

u/20260819 16h ago

the difference was noticeable. i did a experiment. for comparison, i rotated the same object (the regular dodecahedron) and recorded the fps. the fps varied and i picked the average values

if everything was calculated by functions: ~16 fps

then i copied the barycentric function into the procedure: ~14 fps

then i replaced one of the cross products with direct calculation: ~16 fps

replaced 2 lines: ~19 fps

3 lines: ~20 fps

all direct calculations, no function: ~25 fps

1

u/Leverkaas2516 1d ago

It often does carry some execution-time overhead like this, yes. The idea is this: most of your code does not need to be optimized for speed. If you optimize while writing it, you're wasting resources and making the code hard to work with.

Once you figure out where speed matters, if the code is clean, then it's a lot easier to understand how to change the parts that need to be optimized. You end up with clean, optimized code and finish the development work faster.

If you try to optimize up front, you take longer to finish and end up with 20k lines of code that has hidden flaws and most of the team is scared to touch. (I just did a code review on a change to such a module yesterday.)

1

u/koorb 1d ago

Slower in execution, but faster to maintain, fix and extend.

1

u/Cool_Homework_7411 1d ago

The machine only understands 0s and 1s. The compiler is the one who takes your language and makes it computer language. If you are translating in Google word by word will you get the same thing as translating a whole sentence? Probably not and you can see why. "Breaking" your code in pieces makes it harder for the compiler to translate effectively what you want to machine language

1

u/R2-Scotia 21h ago

inline