Almost certainly makes it worse to add additional conditional dependencies, meaning branches upon branches.

Some examples:

1. Branches with dependencies.

static void minMax(u8* pxmin, u8* pxmax, const u8* xs, size_t n)
{
const u8 init = n > 0u ? xs[0] : (u8)0u;
u8 xmin = init
u8 xmax = init;
for (size_t i = 1u; i < n; ++i)
{
const u8 x = xs[i];
if (x < xmin) { xmin = x; }
else if (x > xmax) { xmax = x; }
}
*pxmin = xmin;
*pxmax = xmax;
}

2. Branches w/o dependencies

static void minMax(u8* pxmin, u8* pxmax, const u8* xs, size_t n)
{
const u8 init = n > 0u ? xs[0] : (u8)0u;
u8 xmin = init
u8 xmax = init;
for (size_t i = 1u; i < n; ++i)
{
const u8 x = xs[i];
if (x < xmin) { xmin = x; }
if (x > xmax) { xmax = x; } /*not doing else if*/
}
*pxmin = xmin;
*pxmax = xmax;
}

Compiling with auto-vectorization enabled (-O3 for most versions of GCC, and I was using -march=native on a Zen1 AMD CPU, so AVX2 capable), version 2 is 0.05x the runtime of the version 1 (so a lot faster). Version 2 disassembly will show lots of “packed” vector instructions.

3. Branchless

inline u8 minBranchless(u8 x, u8 y)
{
return x * (u8)(x <= y ? 1u : 0u)
+ y * (u8)(x > y ? 1u : 0u);
}

inline u8 maxBranchless(u8 x, u8 y)
{
return x * (u8)(x >= y ? 1u : 0u)
+ y * (u8)(x < y ? 1u : 0u);
}
static void minMax(u8* pxmin, u8* pxmax, const u8* xs, size_t n)
{
const u8 init = n > 0u ? xs[0] : (u8)0u;
u8 xmin = init
u8 xmax = init;
for (size_t i = 1u; i < n; ++i)
{
const u8 x = xs[i];
xmin = minBranchless(xmin, x);
xmax = maxBranchless(xmax, x);
}
*pxmin = xmin;
*pxmax = xmax;
}

Version 3 is probably a better comparison for the code from the article — there are only 1 or 2 joystick direction vectors that need normalization, so perhaps not enough to show major vectorization benefit, or to even prompt compiler auto-vectorization. Version 3 will most likely be compiled into cmov instructions on x86_64, which are branchless, but branches can still be better, if they can be predicted well. The test data I used was random, so cmov should be expected to be beneficial.

Last time I ran comparisons, Version 2 was 0.07x the runtime of Version 3, so Version 3 was not that much faster than Version 1, but still a good improvement.

As a final note, I should say I’m not a fan of relying on compiler auto-vectorization. If you depend on the performance of vectorized code (and vectorizing code is one of the best optimizations out there), then I’m a fan of explicitly vectorizing. SSE2 is in nearly 100% of relevant x86_64 systems (first coming out in 2003). If you’re targeting consumers, AVX is almost certainly worth it. If you’re writing server code, you should be taking full advantage of AVX2 and maybe 512. For arm, NEON is the current vector extension of relevance, and you can polyfill with DLTcollab/sse2neon or something like that, until you go native. Writing vectorized code using SIMD intrinsics (#include <immintrin.h>) is preferable. Especially since compilers just cannot know how to auto-vectorize a lot of floating point code, because of strict assumptions about requirements around non-commutativity, so they almost never generate packed FMADDs.

As for the article code, I wouldn’t make any assumptions about whether the compiler is generating branches or cmov-s. I’d guess cmov-s would be faster, but I haven’t really thought about it. All I’ll say is that turning a lot of `if`s into `else if`s would make it impossible to generate cmov-s, and chaining branches just to avoid assigning to a local that’s almost certainly in a register is only going to make branching slower.