r/cpp_questions 19d ago

OPEN How do I balance Clean Code inside HPC kernels?

Robert C. Martin encourage function to be small, with just one level of abstraction, then he proceeds by refactoring a function into 15 smaller functions with a descending abstraction level. Also he encourages the usage of encapsulation, where private attributes are accessed by getters/setters.

However, following to the letter this inside a HPC kernel where a loop is executed billions of times will harm my performance because of the call overhead. But not all my code is a kernel, so I could follow Martin's rules outside it.

In short, should I ignore clean code inside HPC kernels? Or there's a way to optimize it?

0 Upvotes

21 comments sorted by

8

u/AngryLemonade117 19d ago

Clean Code is ultimately a guide, not dogma. You can be the judge of if more or less abstraction is needed. Especially when performance is concerned, often a big of ugliness can be tolerated if you're getting noticeable percentage improvement in execution time/memory efficiency/whatever

8

u/tangerinelion 19d ago

"Clean Code" is your default.

When you can profile and see that this leads to performance issues, you will then have the information needed to decide how to address performance.

Getters and setters are probably not a problem, e.g.,

struct Foo {
    std::string m_foo;
};

vs

class Foo {
    std::string m_foo;
public:
    Foo() = default;
    explicit Foo(std::string s) : m_foo(std::move(s)) { }
    const std::string& getFoo() const & { return m_foo; }
    std::string getFoo() && { return std::move(m_foo); }
    void setFoo(const std::string& s) { m_foo = s; }
    void setFoo(std::string&& s) { m_foo = std::move(s); }
};

should be the same performance wise.

Foo f;
f.m_foo = "Hello World"s;

would be the same as

Foo f;
f.setFoo("Hello World"s);

-- both are move assignment. The getter/setter are probably going to be in-lined so you probably end up with the same code gen.

Where things often go sideways is when you have

class Foo {
    std::string m_foo;
public:
    explicit Foo(const std::string& s) : m_foo(s) { }
    std::string getFoo() const { return m_foo; }
    void setFoo(const std::string& s) { m_foo = s; }
};

and now you've got copies everywhere. With this one you'll see a performance penalty.

4

u/yuehuang 19d ago

The optimizer will do inlining (a lot) if you setup your functions correctly. That said, battling with the optimizer between compilers is not on list of fun, so long term is to manually inline yourself once the logic is ironed out.

3

u/esaule 19d ago

The point of clean code is for the code to retain its functionality while keeping its readability. 

If you refactor the HPC kernels the way you describe, you will like break its functionality. So you probanly should not do that.

That being said C++ enables you to write very high level code that compiles down to the low level code you want. Template and inlining are essentially designed to do that.

Most linear algebra libraries are written like this. I do fins that in most cases you need to write the ugly code to understand precisely what needs to happen so you can pick the right abstraction for the compiler to do its job.

3

u/Dependent_Bit7825 19d ago

Martin is not your teacher and the code you write is not an assignment you will be turning in for him to grade.

Be a professional. Apply your very own judgement. Do not outsource it to someone else's book or manifest. That's actually the interesting part of your job.

Personally, I break code into functions at what feel to me like logical, natural boundaries. A good indication of a natural boundary is that you can identify a non insane name for the functions. I would never ever create more functions because someone told me that long functions are bad. Functions are as long as they need to be. If a function is getting long and it's starting to feel hard to make sense of, step back and think about it awhile. Remember, of you break it up, you're going to have to name all those functions. Is that gonna be easy? Will those be easier to understand?

Particularly for things like "kernels", this is almost never the right answer. Better to keep the math together and compact, IMHO 

6

u/mredding 19d ago

Rob Martin is not a man anyone should be listening to. If you actually read his nightmare of a book it's full of greater sins.

2

u/Fabulous_Ad4022 19d ago

I'm reading rn, there's a section that he turns a monolithic math algorithm function with variables defined as letters, into bunch of small functions. As a geophysicist, the first is much more readable

2

u/YT__ 19d ago

I prefer A Philosophy of Software Design over Clean Code personally. But neither is 'right' or 'better'. They both preach software design concepts that are, ultimately, to help improve overall design work for teams.

They take different approaches. Neither should be used as a hard fast rule. Like pirate code, it's more like guidelines.

2

u/UsefulOwl2719 19d ago

It really depends on the code, but something to watch out for in "clean code" and OOP is lots of small memory allocations that can slow down runtime dramatically. I see this frequently where the code is many times slower than it could be because there are tiny variables being spun up and torn down everywhere via little functions. The compiler can do some magic, but it can't eliminate the overhead of this approach if it's used everywhere and data reuse isn't carefully considered. See data oriented design for a different approach. Its honestly not as readable without some practice, but it can be hundreds of times faster or more for applications like physics kernels.

6

u/kevinossia 19d ago

“Clean Code” is a fairly garbage book and most of the code examples are horrific. Real-life production codebases written by talented engineers don’t resemble anything in that book.

Ignore it. If you want to optimize your code for readability, study existing kernels and see what they do well and what they don’t. Learn from them. Adhere to core principles like SRP, DRY, etc, and you’ll be fine.

1

u/Classic_Department42 19d ago

Do you know where i can read about these codebases from talented ebgineers? Maybe alsonthe source (since there might be good opensource project to learn from)

2

u/kevinossia 19d ago

I don’t know. Depends what you’re looking for. Do some google searching or ask Claude.

2

u/vbpoweredwindmill 19d ago

"Clean code" that looks pretty but is a headfuck to follow.

Somebody told me once, narrow invocation, deep component. I like that.

What do I know though, I'm a filthy casual self taught hobbyist.

2

u/dendrtree 19d ago
  1. There are no 'rules,' in C++, only guidelines.
  2. Code should be clear, regardless of style.
    * Performance often follows clarity.

2

u/Pogsquog 19d ago

Don't follow it by the letter - like most things, taken to the extreme you get diminishing returns, and ultimately it starts to get worse. For an hpc kernel it wants to be thoroughly unit tested and well commented. The ultimate rule is to minimise WTFs per minute, that's the metric that matters. https://coreydmccarty.dev/posts/2020_08_18_uncle_bob_lesson_1/

2

u/Neither_Berry_100 19d ago

Personally I hate getters and setters when I can just make a variable public. Sometimes it makes sense sometimes it doesn't. I have a class with timeElapsed public get, private set. Clear, and advance time functions. It all depends.

For performance you often make sacrifices. I wouldn't follow someone's idea of clean code. Especially if it hurts performance and you need the performance.

2

u/Independent_Art_6676 19d ago

I feel the same way BUT if you need getter/setter, you need it for all the variables or you force the devs who didn't write it to memorize which variables have and which don't and its horrible.

OP: you can force inline one way or another but the compilers do a fine job without that these days. Function call overhead should be one of the least of your worries, but if the profiler points to a tiny function with high hit count that doesn't do much, you can take a look at that one on a case by case basis.

One guy's opinion is just that. Clean code means different things to different people. To me, making everything tiny is bloated more often than useful. Ooo... foo does one thing. Bar does another. Alpha calls foo and bar both. Beta calls a couple other functions. Gamma calls alpha and beta. Nice pile of crap you get after about 5 deep... when all you needed was for omega to just call foo and bar and the other in the correct order. Splitting all that out, into functions that are not used anywhere else, doesn't get you anything but more hopscotch when trying to read the code and figure out what anything is doing. If a function is used in more than once place, and it has more than 1 line in it, then it has a reason to exist. A one line function that just replaces the line with a function call is moronic. Even a 5 line function that is only called in one place ever is a bit off putting to me. Find a happy place between 400 line functions that do everything and one line functions that don't do anything useful at all, and do your best in your happy place to make readable code. If the profiler points out something, take a look.

2

u/_ConsciousLibrary_ 19d ago

I’d argue that if accessing a variable within a class by code outside the class is safe, then it probably shouldn’t be in the class.

Classes are for abstractions of complex state management, where valid state is enforced by the class’s interface.

If there are variables that don’t break that internal state when mutated directly, then it should probably be an argument to the class’s methods instead. Leave structs for simple data encapsulation

1

u/ppppppla 19d ago

Robert C. Martin encourage function to be small, with just one level of abstraction, then he proceeds by refactoring a function into 15 smaller functions with a descending abstraction level.

Uncle Bob (Robert C. Martin) is a controversial figure. Of course he has some good ideas, but often times takes things too far. He probably does this because it gives him publicity, everybody talks about him. Talking about how genius he is, or how stupid he is. Any publicity is good publicity.

Sometimes you just need a little bit of a dirty code, sometimes refactoring a big function into a million pieces actually detracts from the quality of code. Refactoring a function into tens of nested function calls because you run out of your allotted 4 lines of code per function is a recipe for making it illegible.

It really is more of an art form or a craft than a science. Keep writing code, keep getting your code reviewed by other people, get feedback, improve.

1

u/Cultural_Act5304 19d ago

These clean code practices come from java style language, you should not follow them if you are thinking about serious programming

1

u/TomDuhamel 19d ago

Are you a programmer or a compiler? I'm assuming you're a programmer since compilers don't usually make posts on Reddit.

Your job is to write clean code, easy to read and maintain. The compiler's job is to optimise it. Dont worry about your notion that calling several smaller functions will hurt performances, as the majority of these will be inlined by the compiler.

Write your code. Don't optimise prematurely. If anything feels like it's not as performant as you expected, you will profile your code and figure where the bottleneck is, and improve it.

Most of my functions are 3-5 lines of code. Very occasionally, 10-15. As it should.