Linear vs. Binary Search
schani.wordpress.com
schani.wordpress.com
Binary Search eliminates Branch Mispredictions http://www.pvk.ca/Blog/2012/07/03/binary-search-star-elimina...
The second one is that, when linear search outperforms binary search, it does so by breaking out of the search, which needs conditional branches. Binary search, despite its bad reputation, has conditional branches that are easily converted to conditional moves or masks; even a loopy implementation is amenable to trip count prediction (it's a function of the log of the size of the array). If we must avoid mispredicted branches, binary search is intrinsically a better option than linear search.
If I remember correctly, Python's hashtables are initialized with 8 buckets that are linearly searched and then switched to a real hashtable implementation when grown past that size.
I have worked with the L4 microkernel where sooo much emphasis was put on keeping instruction and data footprints as small as possible every time the kernel is entered in order not to dirty i- and d-caches.
And I have also seen game engine developers do amazing things in this regard. An interesting development in the gaming space is data-oriented-design that deviates from OOP among other things for performance and parallelization. See http://www.slideshare.net/mobile/cellperformance/data-orient... (though I don't agree with the three "lies" mentionened, I do like the data-centric approach).
> If you’re very, very serious about performance and know the array size statically, completely unroll the binary search.
and pvk says:
> I’ll focus on one specific case that is of interest to me: searching a short or medium-length sorted vector of known size.
"Known" here means "known at compile time". And of course he concludes:
> In general, I’d just stick to binary search: it’s easy to get good and consistent performance from a simple, portable implementation.
Both bloggers use conditional moves to avoid conditional branches; use unrolled versions; and of course use benchmarks to arrive at their conclusions.
Back around 1980 when I first looked into this the generally accepted cutoff was 25 rather than 64. I don't think people were unrolling loops in the tests back then so it's hard to tell whether loop unrolling or changes in CPU architecture is the greater factor in moving the cutoff point from 25 to 64.
When dealing with small arrays (like the example of 0-250 integers, ie. less than 1 kilobyte), the figures are around 20-120 nanoseconds. In other words, the difference is around one main memory reference ("cache miss") between the best and the worst case.
While this is somewhat interesting, the lesson to take home should be that most of the time, smart memory usage is more important than what the CPU executes. Warming up the caches (e.g. __builtin_prefetch) is as efficient an optimization as rewriting and unrolling the entire loop and should be done first. If the caches are warm and the search is still the bottleneck, then it's time to consider the other optimizations.
You can also think about it as a sort of reductio ad absurdum: Assume that the array is actually already sorted and you use a sorting algorithm with O(n) complexity for already sorted data. In this ideal case you'd need to do n (for sorting) + log n (binary search) predictable operations. For linear search you'd only need to n operations of the same type. In practice you'd need to do n log n operations for sorting, I don't think you can avoid unpredictable branches, and instead of just loading elements sometimes you'll move them around.
This can happen in very unintuitive places. For example, in Java, array.size() reads from the same cache line as the beginning of the array. If you're partitioning an array processing routine for many cores, you should not call array.size() to avoid very expensive synchronization.
This will work for n < MAX_INT/2. For larger n, middle must be calculated as min + ((max - min) >> 1), which is one subtraction per iteration more.
Making middle an unsigned int should make (min + max) >> 1 work for all int values.
If min+max is a negative signed int, then the result of right shifting is implementation dependent.
Instead, you could do
int middle = (unsigned)(min + max) >> 1;
Still, this relies on the original indices falling in the non-negative range of signed ints.