SIMD / GPU Friendly Branchless Binary Search
blog.demofox.org
blog.demofox.org
Also of interest are sorting networks https://en.wikipedia.org/wiki/Sorting_network which can be used to perform branchless sort on parallel hardware.
Sure there are?
VGATHERDPD and friends. They have been there for a couple of generations, but are only now getting fast.
That said, the code in the article doesn't actually improve anything specifically for GPUs, because standard binary searches are already "branchless" for practical purposes. Really, the only difference between the article and a standard binary search is that the code in the article has manually unrolled the search loop. That, plus some (not all) binary search implementations can do an early-out if the comparison happens to hit equality.
None of these changes make a SIMD- or GPU-specific difference, because loop unrolling doesn't make a difference in terms of control-flow divergence.
I haven't been following this closely, but the last time I checked scatter-gather loads were really really slow.
Chapter 3.3 from page 27 (PDF page 43) on of this be interesting: https://www.researchgate.net/profile/Florian_Gross/publicati...
Also contains a survey of some other related data structures and algorithms.
That is, use quicksort for chunks up to the size of the L1/L2/L3 cache (depending on benchmarking) and then mergesort the chunks.
I did not know about samplesort - found it on Wikipedia.
If so then could you double performance to log2(n)/2 lookups by issuing parallel memory requests for location n and also both candidates for location n+1?
The answer seems to be "yes" judging by their dramatic conclusion:
> Our conclusion is contrary to our expectations and to previous findings and goes against conventional wisdom, which states that accesses to RAM are slow, and should be minimized. A more accurate statement, that accounts for our findings, is that accesses to RAM have high latency and this latency needs to be mitigated.
In other words: to optimize memory access you should think like a network engineer - treat RAM as a "long fat pipe" and do the usual optimizations like pipelining that you would in TCP or HTTP or ....
Related: "Computer architecture for network engineers." https://github.com/lukego/blog/issues/18.
It's if-less. But the ternary operator (?:) is a branching instruction.
f(x) ? y : 0
can also be written as (int)(bool)f(x) * yIf the constants had been less convenient numbers, like 42, the compiler could have instead used a conditional move instruction -- more general-purpose, but I believe slightly larger and often ever-so-slightly slower.
1) Doesn't obfuscate
2) Comparisons operators compile very similarly to a conditional statement (e.g: if, ternary operator, etc)
I rewrote the OP code using ifs, and it compiles to exactly the same instructions. You can then add -O3 -msse -msse2 as compiler flags, code will be optimized about the same way.
movfuscator doesn't really obfuscate all that much, other than just being weird.
It's whole shitck is to compute offsets into it's memory space differently based on the input data, but not change the instruction stream based on the data. That way computations that shouldn't contribute to the final result are simply given a scratch offset that isn't read back, but the instruction stream never deviates.
This SIMD code is also ultimately computing different offsets into memory space (contributing 0 to that lane's offset on each iteration if that SIMD lane has already found the node it's looking for), but keeping the instruction stream consistent across the 'separate' lanes.
Additionally, "(haystack[4] <= needle) ? 4 : 0" is basically a branch. I mean, a smart compiler could make it a conditional move, but that is in no way guaranteed.
You would end up returning a size_t[4], which would have the results of your 4 searches in it, which ran in parallel.
Regarding the ternary operator, you can always check your assembly (here's some that shows it https://godbolt.org/g/BM6tfc), but the article also shows how to do it with a multiply instead of the ternary operator.
The biggest thing sticking out of this as being slow would be the cache unfriendliness of binary searching, but the link at the end shows how to clear that up (which is not the point of the article, but the ideas are compatible and combinable).
Unless I'm missing something major?
It becomes more evident when you see the generated code. https://godbolt.org/g/Mu9KBY
e.g:
cmp rax, QWORD PTR [rbp-24] ; compare a and b
ja .L2 ; is a above b?, if so jump to .L2, otherwise proceed
In fact this statement: size_t ret = (haystack[4] <= needle) * 4;
...is 100% equivalent to this once compiled: register size_t ret;
if (haystack[4] <= needle) {
ret = 4;
} else {
ret = 0;
}
So the code is not really branchless, just if-less.I rewrote the function using if statements making it compile to exactly the same instructions.
If you optimize the code with compiler flags (e.g: -msse, -msse2 for SSE/SSE2, -O3 for optimization) the generated instructions are exactly the same.
My point being: the code in the blog post is not branchless. The absence of if statements doesn't mean it's branchless.
If you care to disagree here by downvoting I dare you to reply explaining why you think this is wrong.
So I hope you revert your downvote or provide proof of what I am saying is incorrect.