5

As far as I understand the concept, CC is determined by how many nested branched logic the given method have. It can be refactored to checking the opposite of original predicate and calling return. For example:

void Foo()
{
  if (predicate0)
  {
    if (predicate1)
    {
      // some code
    }
  }
}

can be refactored to

void Foo()
{
  if (!predicate0) return;

  if (!predicate1) return;

  // some code
}

but the thing is, following the branch predicate0 && predicate1 will generate less cache misses in C++ if this path is taken much more frequently.

Obviously, I am intentionally simplifying the code. For example, there can be dozens of predicates and things like one predicate dependency on another predicate.

What to do in this case? Should I throw away performance for better readability?

3
  • 7
    Note that in your example, both versions have the same cyclomatic complexity of 3. The CC is the number of independent paths through a procedure. Though I still agree that early returns / guards improve readability.
    – amon
    Commented Oct 21, 2016 at 11:28
  • 4
    An even remotely competent optimizing compiler, even for the utterly brain-dead x86 instruction set architecture, will generate identical code for your two examples. Read Wulf's "Design of an Optimizing Compiler" for more information. Commented Oct 21, 2016 at 12:33
  • 2
    This has nothing to do with cyclomatic complexity, as both examples have exactly the same cyclomatic complexity. Commented Oct 21, 2016 at 16:51

3 Answers 3

15

Unless you really need that performance, always prefer better readability.

The biggest performance wins do not come from micro-optimizations, but from algorithmic complexity improvements, and from other ways to avoid doing unnecessary work. Not doing something is always faster than doing something as fast as possible. Cleaner code makes it easier to spot such opportunities.

If you do need that performance, and can prove with a profiler & benchmarking tools that this change will give you that performance, then of course go for it. But you are still at the mercy of your compiler to get machine code that is branch prediction friendly. The compiler might suddenly deduce that a branch that it previously considered unlikely is in fact likely, or vice versa. To get around this, try to look into profile-guided optimization and/or annotations and compiler hints that let you unambiguously mark a branch as likely or unlikely.

4
  • Isn't it always preached by people of C++ community? To pursue data localization, since most of the algorithms fall apart if you have lots of cache misses. I remember Stroustrup's talk about liner vector beating more sophisticated algorithms which supposed to have far better O.
    – GuardianX
    Commented Oct 21, 2016 at 12:54
  • 5
    @GuardianX Big-O analysis discusses how the resource usage (time, memory) of an algorithm scales with input size. An algo with a better Big-O class will always beat other algorithms, regardless of effects like caching – but only for large input sizes. But many inputs aren't large. In terms of absolute performance, algorithms with worse scaling behaviour might be better for small inputs. This doesn't mean “worse is better”. This means “use a profiler to discover hot spots, and choose appropriate algorithms for your specific use case, input structure, and environment.”
    – amon
    Commented Oct 21, 2016 at 13:13
  • 4
    @GuardianX: Looking at your code change, your assumption that they will in any way influence the number of cache misses is completely unwarranted. Your change will produce more and slower code with a non-optimising compiler, and the exact same code with a good compiler. You are not doing any data localisation.
    – gnasher729
    Commented Oct 21, 2016 at 19:00
  • 1
    There will be a tiny effect that not-taken branches are often a bit faster than taken branches. But compilers know this and can rearrange code automatically like OP did by hand. Now a simple heuristic: When I write "if (condition) return;" the condition can be true only once per call but could be false hundred times - so a good compiler would move the "return" out of the way, and branch if condition is true, which will happen only once.
    – gnasher729
    Commented Jun 21, 2022 at 15:41
3

If you don't already know for sure that you should throw away readability for performance because you have a performance problem and you have profiler data proving that the problem lies here, then definitely throw away performance.

0

For code that does exactly the same thing, once readable, once unreadable, an optimising compiler is more likely to produce the fastest code than you can be rearranging bits of code.

If you have code that is unreadable because it is unnecessarily complicated, the "unnecessarily complicated" will tend to make it slower. Simplifying the code will often have the effect of doing less work, AND making it easier for the optimiser to produce highly optimised code, and by good coincidence make the code more readable.

And it is much easier to improve readable code. If it's hard to figure out what your code does in the first place, then it's also hard to find faster code with the exact same effect.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.