【问题标题】:Binary search with looking nearby values查找附近值的二进制搜索
【发布时间】:2021-10-19 12:04:21
【问题描述】:

我正在尝试查找是否有人以以下方式实现了二进制搜索 -

假设我们有一些元素的数组,放置在连续的内存中。

那么当你比较中间元素时,接下来的几个元素应该已经在 CPU 缓存中了。比较应该已经免费了?

但我找不到这样做的人。

如果没有人这样做,可能是什么原因?

【问题讨论】:

  • 看起来更新这样的集合会导致大量开销。然而,如果一个集合是只读的或不经常修改的,这无关紧要。 (毕竟,有boost::flat_map 对每次插入进行排序,这也是很大的开销)。
  • 作为一个接近的选择,看看这个abseil.io/about/design/btree
  • 在庞大的数据结构中,搜索到的值与当前查看的值非常接近的概率非常小。当二分法即将结束时,该策略开始有用,但由于可能的缓存未命中,与之前的二分法步骤相比,在加载的缓存行上执行二分法的成本很小。如果项目在内存中很小,这对于提前预取数据更有用。如果整个数据结构不适合缓存,所有这些都尤其正确。

标签: performance containers binary-search cpu-cache


【解决方案1】:

经典的二叉搜索可以被认为是在元素上定义一个二叉树结构。例如,如果您的数组有 15 个元素,编号从 1 到 15,您将从查看中间元素“8”开始,然后从那里向左或向右移动到元素“4”或“12”:

(来自 Brodal 等人,“通过小高度二叉树缓存遗忘搜索树”,SODA'02,PDF preprint link)

您的提议本质上是在每个节点中添加更多元素,以便“8”元素也包含一些相邻元素,例如“9”、“10”、“11”。我不认为这已经被广泛研究过,但是还有另一个非常相关的想法已经被研究过,即从二叉树(每个节点上有两个孩子)到 B-ary 树(“B-tree” ,每个节点上有 B 个子节点)。这是您在一个节点中有多个元素的地方,这些元素将结果数据分成许多不同的范围。

通过将搜索键与节点中的 B-1 个不同元素进行比较,您可以确定要递归到 B 个子节点中的哪一个。

可以重新排列数组中的元素,这样就可以在不使用指针和多次内存分配的情况下实现这种搜索结构,这样它就可以像在常规排序数组中进行二分搜索一样节省空间,但使用更少搜索时“跳跃”。在常规排序数组中,二分查找大致执行 log_2(N) 次跳转,而 B-tree 中的搜索仅执行 log_2(N) / log_2(B) = log_B(N) 次跳转,比 log_2(B) 少!

Sergey Slotin 写了一篇博文,其中包含一个完整示例,说明如何实现隐式存储在数组中的静态 B 树:https://algorithmica.org/en/b-tree

【讨论】:

    【解决方案2】:

    我认为你的提议很不错,尽管它比你最初想象的要复杂一些。它仅适用于被比较的数据位于连续数组本身(而不是让数组保存指针)中的情况。这在高级语言中不太常见。

    从那里它变得更加技术性。你打算使用 L1 缓存还是 L2 缓存?实际上,如果它在 L1 缓存中,它可能只是“免费”的。您是要查询 CPU 架构以获取缓存行大小,还是要使用通用架构的规范?您的操作系统和架构是否允许您根据地址模数确定缓存行中的位置? (我假设可以确定,但我不确定。)

    你试图比 O(log(n)) 更快。使用这种方法可以节省多少跳跃?如果您的下一个跳转在同一个缓存行中,那么您通过迭代完成了更多工作。它仅在您的目标在您的缓存行中但您的下一个跳转不是时才有帮助。 L1 缓存行可能包含 8 个(8 字节)或 16 个(4 字节)值,如果我们平均位于缓存行的中间,则只有一半可用。

    我认为在这一点上它变得依赖于数据。你能预测你的目标“接近”你当前的位置吗?接近,我的意思是在 16 个值以内?如此接近意味着您处于 log2(16)=4 接近跳跃范围内,并且其中一些跳跃很可能在缓存中。它们都可能在 L2 缓存中。毕竟,我认为这不值得;你不会更快。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-11-17
      • 1970-01-01
      • 2021-12-28
      • 2014-07-10
      • 2021-01-28
      • 1970-01-01
      • 2020-08-23
      • 1970-01-01
      相关资源
      最近更新 更多