Faster sorted array unions by reducing branches
lemire.me
lemire.me
I'd be curious to compare that to his normal version in a benchmark
https://cantrip.org/sortfast.html
It seems like the code presented could be improved further, something like:
while ((pos1 < size1) & (pos2 < size2)) {
v1 = input1[pos1];
v2 = input2[pos2];
bool less = v1 <= v2;
output_buffer[pos++] = less ? v1 : v2;
pos1 += less, pos2 += (1-less);
}
This is particularly likely in Gcc, which will not generate more than one CMOV instruction in a basic block.(Note: I did not check this in Godbolt. It probably needs some changes, such as s/(1-less)/(!less)/.) Where the compiler refuses to generate CMOV, such as when building without -march=native, something like
output_buffer[pos++] = (-less & v1) | ((less-1) & v2);
can be better than nothing. Again, maybe s/(less-1)/-!less/.
Always benchmark! There are numerous surprises in this area.The fact that you have to "lure" the compiler into making the hot loop branchless is a little bit weird: you'd think there would be a compiler intrinsic or something which was like `cond_val(condition, val1, val2)` which worked something like `condition ? val1 : val2` but was guaranteed (or close to it) to be free of branches. Shaders have something like that with the `step()` function, seems like a useful thing for this kind of high-performance code.
I mean, obviously you could always write this yourself with inline assembly (or using SSE2 intrinsics or whatever), but that would only work on some platforms and might mess with compiler optimizations, so that seems less optimal.
Flag tricks are a bit more reliable.
Is that cmov is only faster than regular branches if the branch prediction regularly fails. Otherwise it's the same speed, or it can even be slower. And most branches are predictable.
It doesn't change the data dependencies, it only reduces overhead from branch misprediction. I suppose the instruction itself has a lot of overhead.
Pipeline stalls while recovering from branch mispredictions are only part of the problem. Dependency chains are trickier to reason about, and can be as big a problem. Generally, if the next iteration cares about what happened in the last iteration, you have a dependency chain and a problem.
In that thread the annotation __builtin_unpredictable() is discussed, which would be a good solution, but it does not support this case yet.
In my code the only reliable alternative I found was inline ASM. Luckily with enough templating you can hide it away.
Branch prediction would break the dependency, but the misprediction rate here is high enough for this specific algorithm to be usually a net negative.
Regarding A, you could have a meta predictor that decides whether you should speculate or not, trained on the misprediciton rate. But then you do not need the cmov in the first place and you can just dynamically convert non-diverging jumps to predication. This is, I think, called Dynamic Hammock Predication and was implemented, in some restricted cases (IIRC jumping over a single instruction) in some recent POWER cpus from IBM. I think that RISC-V also doesn't have predication and an implementation is supposed to use this optimization instead.
If one side is bigger than the other, try to find how many elements you can add from that side and advance that far.
I'm wondering if this removing the branch idea and the galloping idea can coexist.
Otherwise you could even use this monstrosity:
output_buffer[pos++] = (input1[pos1] <= input1[pos2]) ? input1[pos1++] : input2[pos2++]; pos1 += (v1<=v2);
?