Speeding up independent binary searches by interleaving them
lemire.me
lemire.me
Or am I wrong?
Conditional moves create a data dependency, and thus do not involve speculative execution. While the program can explicitly try to preload both possible values (and can benefit from doing so) the processor doesn't ever do this automatically. Other instructions can still be executed (out-of-order) but the path of execution does not change.
With a branch, rather than waiting for the results of the comparison to be known, the processor guesses at the result and chooses a path of execution. It thus executes the fetch immediately, but (about) half the time it's the wrong fetch. If after the original load completes it realizes that it guessed the wrong side of the branch, an 'exception' occurs, the speculative work is thrown away, and the execution begins again with the correct branch.
When the processor reaches a conditional branch (the 'if' statement comparing the test value), rather than waiting for the actual results of the comparison to be known (and thus waiting for the test value to be loaded), it blindly guesses one of the branches and continues executing instructions as fast as it can. It doesn't commit the results of these instructions ('retire' the instructions) until the real answer is known, but crucially, it executes the loads sooner than they would otherwise be executed.
Most of these early loads are along the wrong path and the load is wasted, but half the time, the first guess is right, and half the time, the branch after that, etc. The end result is that branchy binary search ends up roughly twice as fast as a branchless version that waits for the loads to complete and efficiently makes only the loads that need to be made.
It wouldn’t be a huge change to also make the N-value “per query”, but then you get a lot more branches in the inner loop. Even then though, I’m struggling to remember situations where I thought “I have a dozen lists where I need to do binary search for each of them”.
There's all sorts database indexing problems that can use this too, I'm sure.
The problem (well, at least very similar problems that we've thought about more) ends up running at very low IPC, with most of the time being spent waiting for data from RAM or L3. You can fit a lot of extra instructions in without seeing a slowdown. If there is a benefit to SIMD, both of us think it will be a fairly small effect, and only at very high levels of MLP. But we'd be excited to be proven wrong!
Edit: https://eli.thegreenplace.net/2018/measuring-context-switchi... cites a couple microseconds, whereas memory latency is measured in tens of nanoseconds. So several dozen times more expensive to context switch.
The point is that it's expensive for the OS to switch threads, so the CPU instead tells the OS it can run two threads at the same time, and does so by sharing execution resources.
Gives a nice boost for programs that do a fair bit of calculations while also having a fair share of cache misses. If you have tight loops with little memory access you won't gain much.
It's possible my numbers are off by a bit, but I think this means that on a standard CPU, you really need to parallelize the search more explicitly rather than letting the OS handle it for you. GPU's on the other hand do take a similar "brute force" approach to context switching, and clearly have good results on some similar problems.
[1] https://eli.thegreenplace.net/2018/measuring-context-switchi...
[2] https://www.anandtech.com/show/9483/intel-skylake-review-670...
You can learn more about the process here : https://en.wikipedia.org/wiki/Out-of-order_execution
Because the process does not create full "threads," the CPU doesn't need to spend time doing things like saving registers or figuring out what to execute next.
EDIT: Ah, each look-up was in different trees. That takes away that optimization.
The answer of course, is it heavily depends on your problem space and data size and characteristics.
Caching is somewhat discussed, and I get that the topic is not data structures and algorithms; However it seems impossible to say when this approach would be useful in general without more practical considerations.
How easy would it be maintain or enhance this kind of code? Would you get emails like "...we have a few bugs in the 16-way interleaved multithreaded binary search code, who wants quickly get those cleaned up...".
It considers it plenty for an article in the category "neat trick/wrinkle of processor caches and pipelining you may not have known/thought about to its limit." If you're actually doing binary searches on sets large enough to where this would matter, most often some kind of hash based solution is going to be preferable anyway, because of its much higher degree of cache efficiency and better asymptotic performance. That's not this article's purpose, though. Not every article is a tutorial.
That doesn't mean it can't discuss practicalities. Plenty of advanced papers, blog posts, documentation, experiments, whatever, discuss engineering realities because they feel like it.
The reality is some things are less intuitive and require more outside the problem thinking than others.
If someone is writing about how binary trees work alone, not concurrent programming as well as is done here, there's little point to considering applications because it's simply useful to know and apply the concept so often.
However this one is an edge case, and even when you need it I would doubt many seasoned engineers wouldn't still benefit from contextual considerations from the author.
Harder than the naive binary search, but that's a good thing, because the guy who maintains the library code to do these parallel searches likes hard problems in low level programming.