【问题标题】:About the branchless binary search关于无分支二分查找
【发布时间】:2012-07-06 19:57:26
【问题描述】:

我问了一个关于减少未命中预测的问题。

Jerry Coffin 给了我一个令人印象深刻的答案。

About reducing the branch miss prediciton

二分查找是无分支的,但是当我在我的集​​合交集算法中使用它时,我发现它比原来的二分查找慢得多。是什么原因?

更新:

我使用以下事件来测试 i7 处理器的分支未命中预测数:BR_MISS_PRED_RETIRED。我发现无分支版本的分支缺失大约是原始版本的一半。

对于缓存未命中:我使用 LLC_MISSES 来测试最后一级缓存未命中的数量,也是一半。

但是时间是原来的2.5倍左右。

【问题讨论】:

  • 在不执行乱序执行的 CPU 上,你可能会得到完全不同的结果,比如 atom..

标签: algorithm branch


【解决方案1】:

条件移动(无分支)搜索的问题发生在数组很大并且内存访问时间相对于分支错误预测很大的情况下。

条件移动搜索类似于:

int needle; // value we are searching for
int *base = ...; // base pointer
int n; // number of elements in the current region
while (n > 1) {
  int middle = n/2;
  base += (needle < *base[middle]) ? 0 : middle;
  n -= middle;
}

请注意,我们有条件地更新base 而不使用分支(至少假设编译器不决定将三元运算符实现为分支)。问题在于,每次迭代中base 的值数据依赖取决于上一次迭代中的比较结果,因此对内存的访问一次发生一次,通过 a 序列化数据依赖。

对于大型数组的搜索,这消除了内存级并行性的可能性,并且您的搜索需要像log2(N) * average_access_time 这样的东西。基于分支的搜索没有这样的数据依赖性:它只有迭代之间的推测控制依赖性:CPU 选择一个方向并跟随它。如果它猜对了,您将同时加载当前迭代和下一次迭代的结果!它并没有就此结束:猜测仍在继续,您可能同时有十几个负载在飞行。

当然,CPU 并不总是猜对!在最坏的情况下,如果分支是完全不可预测的(您的数据和针值没有偏差),那么一半的时间都是错误的。尽管如此,这意味着平均而言,它将维持0.5 + 0.25 + 0.125 + ... = ~1 在当前访问之外的额外访问。这不仅仅是理论上的:尝试对随机数据进行二分搜索,您可能会看到基于分支的搜索速度比无分支搜索快 2 倍,因为并行度增加了一倍。

对于许多数据集,分支方向并非完全随机,因此您可以看到超过 2 倍的加速,就像您的情况一样。

对于适合缓存的小数组,情况正好相反。无分支搜索仍然存在同样的“串行依赖”问题,但加载延迟很小:几个周期。另一方面,基于分支的搜索经常遭受错误预测,其成本约为 20 个周期,因此在这种情况下,无分支通常会更快结束。

【讨论】:

    【解决方案2】:

    不久前我看到了一种有趣的方法,可能也是在 stackoverflow 上,关于避免数据获取成本。有人以这样一种方式编写了二分搜索,即他们将数组视为隐式树并预取左孩子 右孩子。这是在当前元素与测试值进行比较之前完成的。

    将内存需求增加两倍实际上可以加快搜索速度,这似乎非常违反直觉,但显然更早开始获取可以弥补额外的内存命中。

    如果我没记错的话,一半的读取实际上是不依赖的,因为没有使用这些值。它可以通过推测性预取加载、非依赖加载或普通加载来完成,其中在循环时将获取的值之一移动到保存当前元素的寄存器中。

    【讨论】:

    • 这是有趣的信息,但我不确定它作为答案有多大用处。我们依赖于您的记忆,即使您也不确定,而且您并没有真正提供任何关于如何实现这一点的参考或信息。我认为如果它充实一点,这可能是一个非常好的答案。
    • @blm:1/4 和 3/4 元素的软件预取是软件预取的一个已知用例,用于部分隐藏数据依赖链中加载的延迟。现代 CPU 中的内存级并行性意味着可以同时处理多个请求,而不会降低彼此的速度。 (但如果每个内核都这样做,共享带宽可能会开始增加。)
    【解决方案3】:

    因为该版本正在执行大量加载和存储。

    像这样的紧密循环中的分支预测通常没有效果,因为处理器有多个管道。在评估分支测试时,两个代码路径都已被解码和评估。只保留一条路径的结果 - 但通常不会有来自分支的管道停顿。

    另一方面,写入内存会产生影响。通常您正在写入 CPU 上的内存缓存,但 MMU 必须保持缓存行与系统的其余部分同步缓存未命中并使 CPU 重新加载内存缓存。

    【讨论】:

      【解决方案4】:

      然后使用您原来的二进制搜索。对随机位置的数组访问并不比分支未命中好多少,尤其是因为在这种情况下编译器不能为变量使用寄存器。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-12-10
        • 2021-03-07
        • 1970-01-01
        • 2020-08-23
        • 1970-01-01
        • 2014-12-21
        相关资源
        最近更新 更多