关于无分支二进制搜索

时间:2012-07-06 10:54:06

标签: algorithm branch

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

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

About reducing the branch miss prediciton

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

更新

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

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

但是时间大约是原来的2.5倍。

4 个答案:

答案 0 :(得分:1)

因为该版本正在进行大量的装载和存储。

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

另一方面,写入内存会产生影响。通常你要写入CPU上的内存缓存,但是MMU必须保持缓存行与系统的其余部分同步如果数组很大并且你以基本随机的顺序访问它,那么你就会变得不变缓存未命中并使CPU重新加载内存缓存。

答案 1 :(得分: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的值与上次迭代的比较结果与数据有关,因此一次访问内存一次,通过数据依赖项。

对于大型数组的搜索,这消除了内存级并行性的可能性,并且您的搜索需要使用log2(N) * average_access_time之类的东西。基于分支的搜索没有这种数据依赖关系:它仅在迭代之间具有推测的控制依赖关系:CPU会选择一个方向并顺其自然。如果猜测正确,则将同时加载当前迭代和下一个迭代的结果!事情还没有结束:猜测仍在继续,您可能一次要乘载十几架飞机。

当然,CPU并不总是会猜对!在最坏的情况下,如果分支是完全不可预测的(您的数据和指针值没有偏差),那么一半的时间是错误的。不过,这意味着平均而言,它将在飞行过程中继续支持0.5 + 0.25 + 0.125 + ... = ~1个超出当前访问权限的访问。这不只是理论上的:尝试对随机数据进行二进制搜索,由于并行度提高了一倍,您可能会看到无分支搜索的基于分支的速度提高了2倍。

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

对于适合缓存的小型阵列,情况则相反。无分支搜索仍然存在相同的“串行依赖”问题,但是负载等待时间很小:很少的周期。另一方面,基于分支的搜索经常会出现错误的预测,其代价约为20个周期,因此在这种情况下,无分支的搜索通常会更快。

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

似乎强烈反直觉的是,增加内存需求两倍实际上可以加快搜索速度,但显然早些时候开始提取内存可以弥补额外的内存损失。

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