静态搜索树:比二分搜索快 40 倍(2024)
Static search trees: 40x faster than binary search (2024)

原始链接: https://curiouscoding.nl/posts/static-search-tree/

本文详细介绍了 **S+ 树**的实现,这是一种针对已排序 32 位整数进行高吞吐量搜索而优化的静态搜索树结构。 **关键优化策略:** * **数据布局:** 从传统的二分搜索转向 **Eytzinger 布局**,改善了缓存局部性,使得预取后续树节点成为可能。 * **SIMD 向量化:** 使用手动 SIMD (AVX2) 指令代替线性搜索,使 CPU 能够并行比较 16 个值,从而显著减少指令数量并消除分支预测错误带来的性能损耗。 * **批处理与预取:** 通过分批处理查询(例如每批 128 个),CPU 可以同时处理多个内存请求,从而隐藏延迟并充分利用内存带宽。 * **交错处理:** 当树的大小超过缓存容量时,将搜索层级在多个批次间交错处理,可平衡 CPU 密集型与内存密集型操作,实现接近最优的性能。 **结果:** 通过上述优化,作者实现了比标准二分搜索快 **40 倍**的性能,在 4GB 数据集上达到了约 27 纳秒/次查询的速度。尽管文中也探讨了前缀分区和重叠树等技术,但作者得出结论:简单且经过批处理、交错优化的 S+ 树在性能与复杂度之间达到了最佳平衡。这项工作为构建大规模生物信息学(DNA)数据的高性能索引结构奠定了基础。

本次讨论聚焦于 Eytzinger 布局在搜索树中的效率,其性能最高可比标准二分查找快 40 倍。 Eytzinger 布局的核心优势在于其缓存友好性:通过将根节点置于索引 1,并将子节点置于 2i 和 2i + 1,它模拟了二叉堆的布局。这种排列方式确保了搜索初始阶段频繁访问的节点在内存中彼此靠近。因此,树的上层结构通常位于同一个缓存行或虚拟内存页中,从而显著减少了遍历过程中的缓存缺失。 虽然二叉堆采用了这种布局,但它本身并不支持高效搜索。评论者指出,尽管这一概念对于优化而言直观易懂,但它与 Van Emde Boas 树等结构有所不同。归根结底,Eytzinger 方法利用空间局部性,极大地加速了平衡二叉树中的搜索操作。
相关文章

原文

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<u32>.

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<u32>.

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 {
    /// Two functions with default implementations in terms of each other.
    fn query_one(&self, query: u32) -> u32 {
        Self::query(&[query])[0]
    }
    fn query(&self, queries: &[u32]) -> Vec<u32> {
        queries.iter().map(|&q| Self::query_one(q)).collect()
    }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub struct SortedVec {
    vals: Vec<u32>,
}

impl SortedVec {
    pub fn binary_search_std(&self, q: u32) -> u32 {
        let idx = self.vals.binary_search(&q).unwrap_or_else(|i| i);
        self.vals[idx]
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub struct Eytzinger {
    /// The root of the tree is at index 1.
    vals: Vec<u32>,
}

impl Eytzinger {
    /// L: number of levels ahead to prefetch.
    pub fn search_prefetch<const L: usize>(&self, q: u32) -> u32 {
        let mut idx = 1;
        while (1 << L) * idx < self.vals.len() {
            idx = 2 * idx + (q > self.get(idx)) as usize;
            prefetch_index(&self.vals, (1 << L) * idx);
        }
        // The last few iterations don't need prefetching anymore.
        while idx < self.vals.len() {
            idx = 2 * idx + (q > self.get(idx)) as usize;
        }
        let zeros = idx.trailing_ones() + 1;
        let idx = idx >> zeros;
        self.get(idx)
    }
}

1
sudo cpupower frequency-set -g powersave -d 2.6GHz -u 2.6GHz

1
2
3
4
#[repr(align(64))]
pub struct TreeNode<const N: usize> {
    data: [u32; N],
}

1
2
3
4
5
6
7
8
9
/// N: #elements in a node, always 16.
/// B: branching factor <= N+1. Typically 17.
pub struct STree<const B: usize, const N: usize> {
    /// The list of tree nodes.
    tree: Vec<TreeNode<N>>,
    /// The root is at index tree[offsets[0]].
    /// It's children start at tree[offsets[1]], and so on.
    offsets: Vec<usize>,
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn search(&self, q: u32, find: impl Fn(&TreeNode<N>, u32) -> usize) -> u32 {
    let mut k = 0;
    for o in self.offsets[0..self.offsets.len()-1] {
        let jump_to = find(self.node(o + k), q);
        k = k * (B + 1) + jump_to;
    }

    let o = self.offsets.last().unwrap();
    // node(i) returns tree[i] using unchecked indexing.
    let mut idx = find(self.node(o + k), q);
    // get(i, j) returns tree[i].data[j] using unchecked indexing.
    self.get(o + k + idx / N, idx % N)
}

1
2
3
4
5
6
7
8
pub fn find_linear(&self, q: u32) -> usize {
    for i in 0..N {
        if self.data[i] >= q {
            return i;
        }
    }
    N
}

1
2
3
4
5
6
7
8
9
pub fn find_linear_count(&self, q: u32) -> usize {
    let mut count = 0;
    for i in 0..N {
        if self.data[i] < q {
            count += 1;
        }
    }
    count
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
vmovdqu      (%rax,%rcx), %ymm1     ; load data[..8]
vmovdqu      32(%rax,%rcx), %ymm2   ; load data[8..]
vpbroadcastd %xmm0, %ymm0           ; 'splat' the query value
vpmaxud      %ymm0, %ymm2, %ymm3    ; v
vpcmpeqd     %ymm3, %ymm2, %ymm2    ; v
vpmaxud      %ymm0, %ymm1, %ymm0    ; v
vpcmpeqd     %ymm0, %ymm1, %ymm0    ; 4x compare query with values
vpackssdw    %ymm2, %ymm0, %ymm0    ;
vpcmpeqd     %ymm1, %ymm1, %ymm1    ; v
vpxor        %ymm1, %ymm0, %ymm0    ; 2x negate result
vextracti128 $1, %ymm0, %xmm1       ; v
vpacksswb    %xmm1, %xmm0, %xmm0    ; v
vpshufd      $216, %xmm0, %xmm0     ; v
vpmovmskb    %xmm0, %ecx            ; 4x extract mask
popcntl      %ecx, %ecx             ; popcount the 16bit mask

1
2
3
4
5
6
7
8
pub fn find_ctz(&self, q: u32) -> usize {
    // Simd<u32, N> is the protable-rust type for a SIMD vector of N(=16) u32 values.
    let data: Simd<u32, N> = Simd::from_slice(&self.data[0..N]);
    // splat takes a single u32 value, and copies it to all N lanes.
    let q = Simd::splat(q);
    let mask = q.simd_le(data);
    mask.first_set().unwrap_or(N)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
vpminud      32(%rsi,%r8), %ymm0, %ymm1  ; take min of data[8..] and query
vpcmpeqd     %ymm1, %ymm0, %ymm1         ; does the min equal query?
vpminud      (%rsi,%r8), %ymm0, %ymm2    ; take min of data[..8] and query
vpcmpeqd     %ymm2, %ymm0, %ymm2         ; does the min equal query?
vpackssdw    %ymm1, %ymm2, %ymm1         ; pack the two results together, interleaved as 16bit words
vextracti128 $1, %ymm1, %xmm2            ; extract half (both halves are equal)
vpacksswb    %xmm2, %xmm1, %xmm1         ; go down to 8bit values, but weirdly shuffled
vpshufd      $216, %xmm1, %xmm1          ; unshuffle
vpmovmskb    %xmm1, %r8d                 ; extract the high bit of each 8bit value.
orl          $65536,%r8d                 ; set bit 16, to cover the unwrap_or(N)
tzcntl       %r8d,%r15d                  ; count trailing zeros

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 pub fn find_ctz_signed(&self, q: u32) -> usize
 where
     LaneCount<N>: SupportedLaneCount,
 {
-    let data: Simd<u32, N> = Simd::from_slice(                   &self.data[0..N]   );
+    let data: Simd<i32, N> = Simd::from_slice(unsafe { transmute(&self.data[0..N]) });
-    let q = Simd::splat(q       );
+    let q = Simd::splat(q as i32);
     let mask = q.simd_le(data);
     mask.first_set().unwrap_or(N)
 }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-vpminud      32(%rsi,%r8), %ymm0, %ymm1
-vpcmpeqd     %ymm1, %ymm0, %ymm1
+vpcmpgtd     32(%rsi,%rdi), %ymm1, %ymm2 ; is query(%ymm1) > data[8..]?
-vpminud      (%rsi,%r8), %ymm0, %ymm2
-vpcmpeqd     %ymm2, %ymm0, %ymm2
+vpcmpgtd     (%rsi,%rdi), %ymm1, %ymm1   ; is query(%ymm1) > data[..8]?
 vpackssdw    %ymm2, %ymm1, %ymm1         ; pack results
+vpxor        %ymm0, %ymm1, %ymm1         ; negate results (ymm0 is all-ones)
 vextracti128 $1, %ymm1, %xmm2            ; extract u16x16
 vpacksswb    %xmm2, %xmm1, %xmm1         ; shuffle
 vpshufd      $216, %xmm1, %xmm1          ; extract u8x16
 vpmovmskb    %xmm1, %edi                 ; extract u16 mask
 orl          $65536,%edi                 ; add bit to get 16 when none set
 tzcntl       %edi,%edi                   ; count trailing zeros

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 pub fn find_popcnt_portable(&self, q: u32) -> usize
 where
     LaneCount<N>: SupportedLaneCount,
 {
     let data: Simd<i32, N> = Simd::from_slice(unsafe { transmute(&self.data[0..N]) });
     let q = Simd::splat(q as i32);
-    let mask = q.simd_le(data);
+    let mask = q.simd_gt(data);
-    mask.first_set().unwrap_or(N)
+    mask.to_bitmask().count_ones() as usize
 }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 vpcmpgtd     32(%rsi,%rdi), %ymm0, %ymm1
 vpcmpgtd     (%rsi,%rdi), %ymm0, %ymm0
 vpackssdw    %ymm1, %ymm0, %ymm0     ; 1
-vpxor        %ymm0, %ymm1, %ymm1
 vextracti128 $1, %ymm0, %xmm1        ; 2
 vpacksswb    %xmm1, %xmm0, %xmm0     ; 3
 vpshufd      $216, %xmm0, %xmm0      ; 4
 vpmovmskb    %xmm0, %edi             ; 5
-orl          $65536,%edi
+popcntl      %edi, %edi

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pub fn find_popcnt(&self, q: u32) -> usize {
    // We explicitly require that N is 16.
    let low: Simd<u32, 8> = Simd::from_slice(&self.data[0..N / 2]);
    let high: Simd<u32, 8> = Simd::from_slice(&self.data[N / 2..N]);
    let q_simd = Simd::<_, 8>::splat(q as i32);
    unsafe {
        use std::mem::transmute as t;
        // Transmute from u32 to i32.
        let mask_low = q_simd.simd_gt(t(low));
        let mask_high = q_simd.simd_gt(t(high));
        // Transmute from portable_simd to __m256i intrinsic types.
        let merged = _mm256_packs_epi32(t(mask_low), t(mask_high));
        // 32 bits is sufficient to hold a count of 2 per lane.
        let mask: i32 = _mm256_movemask_epi8(t(merged));
        mask.count_ones() as usize / 2
    }
}
1
2
3
4
5
6
7
8
9
 vpcmpgtd     (%rsi,%rdi), %ymm0, %ymm1
 vpcmpgtd     32(%rsi,%rdi), %ymm0, %ymm0
 vpackssdw    %ymm0, %ymm1, %ymm0
-vextracti128 $1, %ymm0, %xmm1
-vpacksswb    %xmm1, %xmm0, %xmm0
-vpshufd      $216, %xmm0, %xmm0
-vpmovmskb    %xmm0, %edi
+vpmovmskb    %ymm0, %edi
 popcntl      %edi, %edi

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn batch<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
    let mut k = [0; P];
    for [o, _o2] in self.offsets.array_windows() {
        for i in 0..P {
            let jump_to = self.node(o + k[i]).find(qb[i]);
            k[i] = k[i] * (B + 1) + jump_to;
        }
    }

    let o = self.offsets.last().unwrap();
    from_fn(|i| {
        let idx = self.node(o + k[i]).find(qb[i]);
        self.get(o + k[i] + idx / N, idx % N)
    })
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 fn batch<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let mut k = [0; P];
     for [o, o2] in self.offsets.array_windows() {
         for i in 0..P {
             let jump_to = self.node(o + k[i]).find(qb[i]);
             k[i] = k[i] * (B + 1) + jump_to;
+            prefetch_index(&self.tree, o2 + k[i]);
         }
     }

     let o = self.offsets.last().unwrap();
     from_fn(|i| {
         let idx = self.node(o + k[i]).find(qb[i]);
         self.get(o + k[i] + idx / N, idx % N)
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 pub fn batch_splat<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let mut k = [0; P];
+    let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     for [o, o2] in self.offsets.array_windows() {
         for i in 0..P {
-            let jump_to = self.node(o + k[i]).find      (qb[i]    );
+            let jump_to = self.node(o + k[i]).find_splat(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
             prefetch_index(&self.tree, o2 + k[i]);
         }
     }

     let o = self.offsets.last().unwrap();
     from_fn(|i| {
-        let idx = self.node(o + k[i]).find      (qb[i]    );
+        let idx = self.node(o + k[i]).find_splat(q_simd[i]);
         self.get(o + k[i] + idx / N, idx % N)
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
movq         (%rsp,%r11),%r15
leaq         (%r9,%r15),%r12
shlq         $6, %r12
vmovdqa      1536(%rsp,%r11,4),%ymm0
vpcmpgtd     (%rsi,%r12), %ymm0, %ymm1
vpcmpgtd     32(%rsi,%r12), %ymm0, %ymm0
vpackssdw    %ymm0, %ymm1, %ymm0
vpmovmskb    %ymm0, %r12d
popcntl      %r12d, %r12d
shrl         %r12d
movq         %r15,%r13
shlq         $4, %r13
addq         %r15,%r13
addq         %r12,%r13
movq         %r13,(%rsp,%r11)
shlq         $6, %r13
prefetcht0   (%r10,%r13)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 pub fn batch_ptr<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let mut k = [0; P];
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

+    // offsets[l] is a pointer to self.tree[self.offsets[l]]
+    let offsets = self.offsets.iter()
+        .map(|o| unsafe { self.tree.as_ptr().add(*o) })
+        .collect_vec();

     for [o, o2] in offsets.array_windows() {
         for i in 0..P {
-            let jump_to = self.node(o  +  k[i])  .find_splat(q_simd[i]);
+            let jump_to = unsafe { *o.add(k[i]) }.find_splat(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
-            prefetch_index(&self.tree, o2 + k[i]);
+            prefetch_ptr(unsafe { o2.add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
-        let idx = self.node(o  +  k[i])  .find_splat(q_simd[i]);
+        let idx = unsafe { *o.add(k[i]) }.find_splat(q_simd[i]);
-        self.get(o + k[i] + idx / N, idx % N)
+        unsafe { *(*o.add(k[i] + idx / N)).data.get_unchecked(idx % N) }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 pub fn batch_byte_ptr<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let mut k = [0; P];
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     for [o, o2] in offsets.array_windows() {
         for i in 0..P {
-            let jump_to = unsafe { *o.     add(k[i]) }.find_splat(q_simd[i]);
+            let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat(q_simd[i]);
-            k[i] = k[i] * (B + 1) + jump_to     ;
+            k[i] = k[i] * (B + 1) + jump_to * 64;
-            prefetch_ptr(unsafe { o2.     add(k[i]) });
+            prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
-        let idx = unsafe { *o.     add(k[i]) }.find_splat(q_simd[i]);
+        let idx = unsafe { *o.byte_add(k[i]) }.find_splat(q_simd[i]);
-        unsafe { *(*o.add(k[i] + idx / N)).data.get_unchecked(idx % N) }
+        unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
movq         32(%rsp,%rdi),%r8
vmovdqa      1568(%rsp,%rdi,4),%ymm0
vpcmpgtd     (%rsi,%r8), %ymm0, %ymm1
vpcmpgtd     32(%rsi,%r8), %ymm0, %ymm0
vpackssdw    %ymm0, %ymm1, %ymm0
vpmovmskb    %ymm0, %r9d
popcntl      %r9d, %r9d
movq         %r8,%r10
shlq         $4, %r10
addq         %r8,%r10
shll         $5, %r9d
andl         $-64,%r9d
addq         %r10,%r9
movq         %r9,32(%rsp,%rdi)
prefetcht0   (%rcx,%r9)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 pub fn find_splat64(&self, q_simd: Simd<u32, 8>) -> usize {
     let low: Simd<u32, 8> = Simd::from_slice(&self.data[0..N / 2]);
     let high: Simd<u32, 8> = Simd::from_slice(&self.data[N / 2..N]);
     unsafe {
         let q_simd: Simd<i32, 8> = t(q_simd);
         let mask_low = q_simd.simd_gt(t(low));
         let mask_high = q_simd.simd_gt(t(high));
         use std::mem::transmute as t;
         let merged = _mm256_packs_epi32(t(mask_low), t(mask_high));
         let mask = _mm256_movemask_epi8(merged);
-        mask.count_ones() as usize / 2
+        mask.count_ones() as usize * 32
     }
 }

 pub fn batch_byte_ptr<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let mut k = [0; P];
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     for [o, o2] in offsets.array_windows() {
         for i in 0..P {
-            let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat  (q_simd[i]);
+            let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
-            k[i] = k[i] * (B + 1) + jump_to * 64;
+            k[i] = k[i] * (B + 1) + jump_to     ;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
         let idx = unsafe { *o.byte_add(k[i]) }.find_splat(q_simd[i]);
         unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
pub fn batch_interleave_full_128(&self, qs: &[u32]) -> Vec<u32> {
    match self.offsets.len() {
        // 1 batch of size 128
        1 => self.batch_interleave_full::<128, 1, 128>(qs),
        // 2 batches of size 64 in parallel, with product 128
        2 => self.batch_interleave_full::<64, 2, 128>(qs),
        // 3 batches of size 32 in parallel with product 96
        3 => self.batch_interleave_full::<32, 3, 96>(qs),
        4 => self.batch_interleave_full::<32, 4, 128>(qs),
        5 => self.batch_interleave_full::<16, 5, 80>(qs),
        6 => self.batch_interleave_full::<16, 6, 96>(qs),
        7 => self.batch_interleave_full::<16, 7, 112>(qs),
        8 => self.batch_interleave_full::<16, 8, 128>(qs),
        _ => panic!("Unsupported tree height {}", self.offsets.len()),
    }
}

pub fn batch_interleave_full<const P: usize, const L: usize, const PL: usize>(
    &self,
    qs: &[u32],
) -> Vec<u32> {
    assert_eq!(self.offsets.len(), L);

    let mut out = Vec::with_capacity(qs.len());
    let mut ans = [0; P];

    // Iterate over chunks of size P of queries.
    // Omitted: initialize
    let first_i = L-1;
    for chunk in qs.array_chunks::<P>() {
        let i = first_i;

        // Decrement first_i, modulo L.
        if first_i == 0 {
            first_i = L;
        }
        first_i -= 1;

        // Process 1 element per chunk, starting at element first_i.
        // (Omitted: process first up-to L elements.)
        // Write output and read new queries from index j.
        let mut j = 0;
        loop {
            // First L-1 levels: do the usual thing.
            // The compiler will unroll this loop.
            for l in 0..L - 1 {
                let jump_to = unsafe { *offsets[l].byte_add(k[i]) }.find_splat64(q_simd[i]);
                k[i] = k[i] * (B + 1) + jump_to;
                prefetch_ptr(unsafe { offsets[l + 1].byte_add(k[i]) });
                i += 1;
            }

            // Last level: read answer.
            ans[j] = {
                let idx = unsafe { *ol.byte_add(k[i]) }.find_splat(q_simd[i]);
                unsafe { (ol.byte_add(k[i]) as *const u32).add(idx).read() }
            };
            // Last level: reset index, and read new query.
            k[i] = 0;
            q_simd[i] = Simd::splat(chunk[j]);

            i += 1;
            j += 1;

            if i > PL - L {
                break;
            }
        }
        // (Omitted: process last up-to L elements.)

        out.extend_from_slice(&ans);
    }

    out
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 pub fn batch_ptr3_full<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let mut k = [0; P];
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

+    let o = self.tree.as_ptr();

-    for [o, o2] in offsets.array_windows() {
+    for _l      in 0..self.offsets.len() - 1 {
         for i in 0..P {
             let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
-            k[i] = k[i] * (B + 1) + jump_to     ;
+            k[i] = k[i] * (B + 1) + jump_to + 64;
             prefetch_ptr(unsafe { o.byte_add(k[i]) });
         }
     }

     from_fn(|i| {
         let idx = unsafe { *o.byte_add(k[i]) }.find_splat(q_simd[i]);
         unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 pub fn search_prefix<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     // Initial parts, and prefetch them.
     let o0 = offsets[0];
-    let mut k = [0; P];
+    let mut k = qb.map(|q| {
+        (q as usize >> self.shift) * 64
+    });
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     for [o, o2] in offsets.array_windows() {
         for i in 0..P {
             let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
         let idx = unsafe { *o.byte_add(k[i]) }.find_splat(q_simd[i]);
         unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 pub fn search<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     // Initial parts, and prefetch them.
     let o0 = offsets[0];
+    let mut k: [usize; P] = [0; P];
+    let parts: [usize; P] = qb.map(|q| {
+        // byte offset of the part.
+        (q as usize >> self.shift) * self.bpp * 64
+    });
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     for [o, o2] in offsets.array_windows() {
         for i in 0..P {
-            let jump_to = unsafe { *o.byte_add(           k[i]) }.find_splat64(q_simd[i]);
+            let jump_to = unsafe { *o.byte_add(parts[i] + k[i]) }.find_splat64(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
-            prefetch_ptr(unsafe { o2.byte_add(           k[i]) });
+            prefetch_ptr(unsafe { o2.byte_add(parts[i] + k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
-        let idx = unsafe { *o.byte_add(           k[i]) }.find_splat(q_simd[i]);
+        let idx = unsafe { *o.byte_add(parts[i] + k[i]) }.find_splat(q_simd[i]);
-        unsafe { (o.byte_add(           k[i]) as *const u32).add(idx).read() }
+        unsafe { (o.byte_add(parts[i] + k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 pub fn search_b1<const P: usize>(&self, qb: &[u32; P]) -> [u32; P] {
     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     let o0 = offsets[0];
     let mut k: [usize; P] = qb.map(|q| {
          (q as usize >> self.shift) * 64
     });
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

-    for         [o, o2]  in offsets.array_windows()        {
+    if let Some([o1, o2]) = offsets.array_windows().next() {
         for i in 0..P {
             let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
-            k[i] = k[i] * (B + 1) + jump_to;
+            k[i] = k[i] * self.b1 + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

-    for [o, o2] in offsets     .array_windows() {
+    for [o, o2] in offsets[1..].array_windows() {
         for i in 0..P {
             let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
         let idx = unsafe { *o.byte_add(k[i]) }.find_splat(q_simd[i]);
         unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 pub fn search<const P: usize, const PF: bool>(&self, qb: &[u32; P]) -> [u32; P] {
     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     let o0 = offsets[0];
     let mut k: [usize; P] = qb.map(|q| {
-        (q as usize >> self.shift) * 4 *  16
+        (q as usize >> self.shift) * 4 * (16 - self.overlap)
     });
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     if let Some([o1, o2]) = offsets.array_windows().next() {
         for i in 0..P {
+            // First level read may be unaligned.
-            let jump_to = unsafe { *o.byte_add(k[i])                  }.find_splat64(q_simd[i]);
+            let jump_to = unsafe {  o.byte_add(k[i]).read_unaligned() }.find_splat64(q_simd[i]);
             k[i] = k[i] * self.l1 + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     for [o, o2] in offsets[1..].array_windows() {
         for i in 0..P {
             let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
-        let idx = unsafe { *o.byte_add(k[i])                  }.find_splat(q_simd[i]);
+        let idx = unsafe {  o.byte_add(k[i]).read_unaligned() }.find_splat(q_simd[i]);
         unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 pub fn search<const P: usize, const PF: bool>(&self, qb: &[u32; P]) -> [u32; P] {
     let offsets = self
         .offsets
         .iter()
         .map(|o| unsafe { self.tree.as_ptr().add(*o) })
         .collect_vec();

     let o0 = offsets[0];
     let mut k: [usize; P] = qb.map(|q| {
-                 4 * (16 - self.overlap)         * (q as usize >> self.shift)
+        unsafe { 4 * *self.prefix_map.get_unchecked(q as usize >> self.shift) }
     });
     let q_simd = qb.map(|q| Simd::<u32, 8>::splat(q));

     if let Some([o1, o2]) = offsets.array_windows().next() {
         for i in 0..P {
             let jump_to = unsafe {  o.byte_add(k[i]).read_unaligned() }.find_splat64(q_simd[i]);
             k[i] = k[i] * self.l1 + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     for [o, o2] in offsets[1..].array_windows() {
         for i in 0..P {
             let jump_to = unsafe { *o.byte_add(k[i]) }.find_splat64(q_simd[i]);
             k[i] = k[i] * (B + 1) + jump_to;
             prefetch_ptr(unsafe { o2.byte_add(k[i]) });
         }
     }

     let o = offsets.last().unwrap();
     from_fn(|i| {
         let idx = unsafe {  o.byte_add(k[i]).read_unaligned() }.find_splat(q_simd[i]);
         unsafe { (o.byte_add(k[i]) as *const u32).add(idx).read() }
     })
 }