40x faster than binary search · CuriousCoding

🔥 Discover this trending post from Hacker News 📖

📂 **Category**:

📌 **What You’ll Learn**:

In this post, we will implement a static search tree (S+ tree) for
high-throughput searching of sorted data, as introduced on Algorithmica.
We’ll mostly take the code presented there as a starting point, and optimize it
to its limits. For a large part, I’m simply taking the ‘future work’ ideas of that post
and implementing them. And then there will be a bunch of looking at assembly
code to shave off all the instructions we can.
Lastly, there will be one big addition to optimize throughput: batching.

All source code, including benchmarks and plotting code, is at github:RagnarGrootKoerkamp/static-search-tree.

Discuss on r/programming, hacker news, twitter, bsky, or youtube.

Input. A sorted list of \(n\) 32bit unsigned integers vals: Vec.

Output. A data structure that supports queries \(q\), returning the smallest
element of vals that is at least \(q\), or u32::MAX if no such element exists.
Optionally, the index of this element may also be returned.

Metric. We optimize throughput. That is, the number of (independent) queries
that can be answered per second. The typical case is where we have a
sufficiently long queries: &[u32] as input, and return a corresponding answers: Vec.

Note that we’ll usually report reciprocal throughput as ns/query (or just
ns), instead of queries/s. You can think of this as amortized (not average) time spent per query.

Benchmarking setup. For now, we will assume that both the input and queries
are simply uniform random sampled 31bit integers.

Code.
In code, this can be modelled like this:

1
2
3
4
5
6
7
8
9
trait SearchIndex 
        let idx = self.vals.binary_search(&q).unwrap_or_else(

Aside from doing this project just for the fun of it, there is some higher
goal.
One of the big goals of bioinformatics is to make efficient datastructures to
index DNA, say a single human genome (3 billion basepairs/characters) or even a
bunch of them. One such datastructure is the suffix array (wikipedia), that
sorts the suffixes of the input string. Classically, one can then find the
locations where a string occurs by binary searching the suffix array.

This project is a first step towards speeding up the suffix array search.

Also note that we indeed assume that the input data is static, since usually
we use a fixed reference genome.

The classical solution to this problem is binary search, which we will briefly
visit in the next section. A great paper on this and other search layouts is
“Array Layouts for Comparison-Based Searching” by Khuong and Morin (2017).
Algorithmica also has a case study based on that paper.

This post will focus on S+ trees, as introduced on Algorithmica in the
followup post, static B-trees. In the interest of my time, I will mostly assume
that you are familiar with that post.

I also recommend reading my work-in-progress introduction to CPU performance,
which contains some benchmarks pushing the CPU to its limits. We will use the
metrics obtained there as baseline to understand our optimization attempts.

Also helpful is the Intel Intrinsics Guide when looking into SIMD instructions.
Note that we’ll only be using AVX2 instructions here, as in, we’re assuming
intel. And we’re not assuming less available AVX512 instructions (in
particular, since my laptop doesn’t have them).

1.4 Binary search and Eytzinger layout