A lot of it still rings true today. The sad thing is that we _knew_ that _something_ had to be done, but our political and economic structures provide no incentive for long-term thinking, and so we didn't change anything.
80 karma · joined April 28, 2023
A lot of it still rings true today. The sad thing is that we _knew_ that _something_ had to be done, but our political and economic structures provide no incentive for long-term thinking, and so we didn't change anything.
loop:
lea (%rdx,%rax,4),%rcx
cmp (%rcx),%esi
cmovg %rcx,%rdx
shr %rax
jne loop
Here's a simulated CPU trace on Intel skylake: https://uica.uops.info/tmp/2de9d862d05d482ebed576d7e3923b93_...Note that this tracer makes the assumption that all memory loads are in cache (otherwise the memory lookup will dominate). So bear that in mind for this code, especially since memory reads will likely dominate the cost of a binary search.
Regardless, it appears that the cost of conditional move is not the source of bottleneck.
cmovne rax, rdx
jmp rax
that is, a cmov followed by an indirect jump to the address contained in rax, "jmp rax" is _always_ an indirect jump. It doesn't matter whether rax was set via a conditional move instruction or not.This is more than just a theoretical issue. These channel attacks have been demonstrated in practice, even if the victim CPU is running across the internet.
It's true that the cmov instruction uses the CPU flags register (which I'm sure the CPU designers at Intel hate), but that doesn't mean that it branches.
You can achieve the same effect as cmov by using bitwise operations and a subtraction, though it'd just be a few cycles slower--but it would be even more clear that it doesn't branch.
In addition to being branchless, it also has better cache properties than a standard binary search tree.
If you're doing a binary search in an array, you start in the middle, and then jump all the way to the midway point of one half, and so on. As a result, there's a lot of distance between each read that you do in the array. Thus, each read is putting extra strain on the cache (or the reads won't be cached).
The CPU cache performs much better if the values you want to read are close to each other. Enter the Eytzinger Binary Search. The idea is that, if you're always going to be accessing the root of the tree, and then one of its two children--you should just put those children physically close to the root, so they'll all be on the same cache line!
> And it doesn't really matter whether a branch can be well predicted.
I'm not so sure this is true. See https://lemire.me/blog/2019/10/15/mispredicted-branches-can-... as an example.
A properly predicted branch can be _faster_ than a cmov instruction, however the important point is avoiding the branch mispredict. The fact that we're using cmov is beside the point. We could achieve a similar effect by performing a subtraction and doing bitwise arithmetic to extract the sign bit (which, in effect, performs a comparison).