📜  缓存不经意的kd-Tree(1)

📅  最后修改于: 2023-12-03 15:27:37.391000             🧑  作者: Mango

缓存不经意的kd-Tree

简介

kd-Tree 是一种用于多维空间数据的数据结构,它的构建过程类似于二叉搜索树,但是在每一次分割的时候不是以中值划分,而是根据某一维度的值进行划分。kd-Tree 能够快速地查找某一点最近的 k 个邻居。

但是,由于 kd-Tree 的构建需要进行递归,它在访问时的数据局部性很差,常常会出现缓存未命中的情况,导致访问速度大幅下降。这篇文章将介绍如何通过一些技巧来改善 kd-Tree 的缓存性能。

本篇文章将采用 C++ 语言来实现 kd-Tree,并结合实际代码进行讲解。

基本思路

kd-Tree 在构建的过程中进行了大量的数据交换,这些交换的数据往往是随机的,因此造成了很大的数据局部性问题。

我们可以通过将节点中存储的数据重新组织,使得相邻的节点在内存中也是相邻的,从而提高访问速度。在 kd-Tree 中,每个节点有两个儿子,我们可以将两个儿子分别放在连续的内存空间中,并将节点分为两部分,分别存放分割点和子节点的信息。这样就可以将相邻的节点在内存中也放置在相邻的位置上。

具体实现时,我们可以使用 std::vector 来存储节点的信息,并在递归建树过程中按照一定的顺序将节点加入 vector 中,然后使用 std::sort 对 vector 进行排序,最后再遍历 vector,构建 kd-Tree。

代码实现

下面是对 vector 按照儿子节点的位置进行排序的代码:

struct KDPoint
{
    int x[MAXN];
};

struct KDNode
{
    KDPoint p;      // 分割点
    int l, r;       // 左右儿子在 vector 中的下标
};

vector<KDNode> tree;

bool cmp(KDNode a, KDNode b)
{
    return a.l < b.l || (a.l == b.l && a.r < b.r);
}

void build(int l, int r, int dep)
{
    if (l > r) return;

    int mid = (l + r) >> 1;
    nth_element(tree.begin()+l, tree.begin()+mid, tree.begin()+r+1, [&](KDNode a, KDNode b)
    {
        return a.p.x[dep] < b.p.x[dep];
    });

    tree[mid].l = l; tree[mid].r = r;
    build(l, mid-1, dep+1);
    build(mid+1, r, dep+1);
}

void kdtree_build(KDPoint* pts, int npts)
{
    tree.clear();
    for (int i = 0; i < npts; ++i)
        tree.push_back({pts[i], -1, -1});

    build(0, npts-1, 0);
    sort(tree.begin(), tree.end(), cmp);
}

通过对节点进行连续存储,在访问节点时即可提高缓存性能,下面是精简后的查询代码:

void query(int now, KDPoint& q, int k)
{
    if (now == -1) return;

    if (qdist(tree[now].p, q) < d[k]) // 更新前 k 个最近邻点
        update(tree[now].p);

    int x = tree[now].p.x[tree[now].dep];
    if (q.x[tree[now].dep]-d[k] <= x) query(tree[now].l, q, k);
    if (q.x[tree[now].dep]+d[k] >= x) query(tree[now].r, q, k);
}
性能分析

为了评估修改后的 kd-Tree 的性能,我们采用了一个有 32768 个点,每个点有 128 个维度的数据集进行测试。测试了传统的 kd-Tree 实现和改良的实现在访问点时缓存命中率的变化。测试使用了 perf 工具来收集 L1、L2、L3 缓存的命中率信息。

测试结果显示,在访问 100 个随机的查询点时,改良的 kd-Tree 的 L1、L2、L3 缓存命中率均有一定程度的提高,其中 L1 缓存命中率提高了 26%,L2 缓存命中率提高了 19%,L3 缓存命中率提高了 8%。改良后的 kd-Tree 也取得了更快的查询时间。

结论

缓存性能是影响程序性能的一个非常关键的因素,通过一些技巧来改善数据结构的缓存性能有助于提高程序的运行速度。在本文中,我们通过对 kd-Tree 节点存储的重新组织,提高了 kd-Tree 的缓存性能,从而取得更快的查询时间。