Live data from Hacker News

The Weird Concept of Branchless Programming

sanixdk.xyz

51–60 of 92 posts

Re: The Weird Concept of Branchless Programming

#51
post #13

I’m amused to see a for loop in a function that is purportedly branchless (not a critique)

Loops are very predictable in general so the branch isn't a problem. Of course this assumes your branch codition is standard - you can do weird things in the end condition that would food the cpu.

Re: The Weird Concept of Branchless Programming

#52
post #4

Great article, triggers some memories. When you get to think about branchless programming, especially for SIMD optimizations in the real world, you always learn a lot and it’s as if you get a +1 level on your algorithmic skills. The hardest part then is make sure the tricks are clearly laidout so that someone else can take it from here next time

It also triggered some memories for me too. A college professor wanted to teach all the bit manipulating stuff and gave an assignment where students had to transform branchy code into branchless code using shifts and bit operators. Had a lot of fun doing that.

Re: The Weird Concept of Branchless Programming

#53
Interesting article. I remember have solved a leetcode problem called "Trapping Rain Water" https://leetcode.com/problems/trapping-rain-water/descriptio...

On leetcode the "elegant" and fastest solution was something like this:

  class Solution {
  public:
    int trap(vector& height) {
        int i = 1;
        int n = height.size();
        int j = n - 1;
        int leftmax = height[0];
        int rightmax = height[n - 1];
        int tw = 0;
        while (i  height[i]) {
                    tw += leftmax - height[i];
                } else {
                    leftmax = height[i];
                }
                i++;
            } else {
                if (rightmax > height[j]) {
                    tw += rightmax - height[j];
                } else {
                    rightmax = height[j];
                }
                j--;
            }
        }
        return tw;
    }
  };
My solution was this:

  class Solution {
  public:
    int trap(const vector& height) {
        int mx = 0;
        long long sum = 0;

        for (const auto h : height) {
            mx = max(mx, h);
            sum += mx - h;
        }

        int rMx = 0, h, i;

        for (i = height.size() - 1; (h = height[i]) != mx; --i) {
            rMx = max(h, rMx);
            sum += rMx;
        }

        return sum - (height.size() - 1 - i) * mx;
    }
  };
However, I compiled and run some benchmarks on my local machine, and it turned out while on average my solution was slower, but in the worst case it was beating that solultion. My solution was less sensitive to the random input and the degradation was far less dramatic, around 2x, while that solution degraded much much more. I also thought that it was probably caused by the fact that my solution seem to be more predictable for the CPU branch predictor than that one, despite that it iterates twice.

Re: The Weird Concept of Branchless Programming

#54
post #29

Earlier quoted context omitted.

If you look at compiler output, you will always see plenty of small stupidities.

So why does conventional wisdom say that compilers will, in the vast majority of the time, outperform programmers doing assembly by hand? It seems contradictory to me.

Because the cost of that unnecessary mov is very small, so the win from human assembly is very small.

But rules of thumb are like this. If you know enough to question the rule of thumb, go ahead. Hand assembly in hot code can be worth the cost.

It's also possible the value in ecx is used again outside the snippet?

Re: The Weird Concept of Branchless Programming

#55

Is cmov branchless, or just branching by another name?

From my understanding branches are about conditional jump instructions. Here are some of them:

  JZ, JE - Jump if Zero, Jump if Equal
  JNZ, JNE - Jump if Not Zero, Jump if Not Equal
  JC - Jump if Carry
  JNC - Jump if No Carry
  JO - Jump if Overflow
  JNO - Jump if No Overflow
  JS - Jump if Signed (Negative)
  JNS - Jump if Not Signed (Positive or Zero)
  JP, JPE - Jump if Parity, Jump if Parity is Even
  JNP, JPO - Jump if Not Parity, Jump if Parity is Odd

Re: The Weird Concept of Branchless Programming

#56
post #54
post #29

Earlier quoted context omitted.

So why does conventional wisdom say that compilers will, in the vast majority of the time, outperform programmers doing assembly by hand? It seems contradictory to me.

Because the cost of that unnecessary mov is very small, so the win from human assembly is very small. But rules of thumb are like this. If you know enough to question the rule of thumb, go ahead. Hand assembly in hot code can be worth the cost. It's also possible the value in ecx is used again outside the snippet?

In that context, it's not very small, it's 20% (all instructions are register-to-register instructions, so they all have the same weight). It's huge.

Yes, there's the possibility that ecx is used elsewhere, and in that case, my second comment is irrelevant, because I was answering to the possibility that such big wart is to be expected from compilers because they crop up regularly.

But then again, it's unlikely that it's used elsewhere, because eax has the return value of the C snippet, there's nothing else to do, the function can return. So the original question remains: did this come from a C compiler? If yes, it's crappy code.

Re: The Weird Concept of Branchless Programming

#57
post #23

In the part about "abs", there's an assembly breakdown: mov eax, edi sar eax, 31 mov ecx, eax add edi, ecx xor eax, edi Has this been generated by a C compiler? If yes, it's a bit puzzling, because can't you remove "mov ecx, eax", replace "add edi, ecx" by "add edi, eax" and have the exact same result?

This what was generated with clang / gcc x64 with O2/O3 flag on godbolt.org

  abs_branchless(int):
        mov     eax, edi
        neg     eax
        cmovs   eax, edi
        ret

  abs_branch(int):
        mov     eax, edi
        neg     eax
        cmovs   eax, edi
        ret

Re: The Weird Concept of Branchless Programming

#58
post #56
post #54

Earlier quoted context omitted.

Because the cost of that unnecessary mov is very small, so the win from human assembly is very small. But rules of thumb are like this. If you know enough to question the rule of thumb, go ahead. Hand assembly in hot code can be worth the cost. It's also possible the value in ecx is used again outside the snippet?

In that context, it's not very small, it's 20% (all instructions are register-to-register instructions, so they all have the same weight). It's huge. Yes, there's the possibility that ecx is used elsewhere, and in that case, my second comment is irrelevant, because I was answering to the possibility that such big wart is to be expected from compilers because they crop up regularly. But then again, it's unlikely that…

> In that context, it's not very small, it's 20% (all instructions are register-to-register instructions, so they all have the same weight). It's huge.

Huge in space sure. Not in execution time.

Re: The Weird Concept of Branchless Programming

#59
post #55

Is cmov branchless, or just branching by another name?

From my understanding branches are about conditional jump instructions. Here are some of them: JZ, JE - Jump if Zero, Jump if Equal JNZ, JNE - Jump if Not Zero, Jump if Not Equal JC - Jump if Carry JNC - Jump if No Carry JO - Jump if Overflow JNO - Jump if No Overflow JS - Jump if Signed (Negative) JNS - Jump if Not Signed (Positive or Zero) JP, JPE - Jump if Parity, Jump if Parity is Even JNP, JPO - Jump if Not Pari…

The article's claim is that branchless code avoids branch-prediction, and therefore optimizes better on speculative CPUs for certain workloads. Jump instructions obviously incur branch-prediction; my question is is CMOV incurs branch prediction, also? Certainly the internal micro-ops behind the instructions will still need to make a prediction or stall?
Post reply on HN