408

有没有比x >= start && x <= endC 或 C++ 更快的方法来测试一个整数是否在两个整数之间?

更新:我的特定平台是 iOS。这是框模糊功能的一部分,该功能将像素限制为给定正方形中的圆形。

更新:在尝试接受的答案后,我在一行代码上的速度比正常x >= start && x <= end方式快了一个数量级。

更新:这是使用 XCode 的汇编程序的前后代码:

新的方法

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

老路

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

令人惊奇的是,减少或消除分支如何提供如此显着的加速。

4

7 回答 7

552

只有一个比较/分支来做到这一点有一个老技巧。它是否真的会提高速度可能值得商榷,即使确实如此,它也可能太少而无法注意到或关心,但是当你只从两个比较开始时,巨大改进的机会非常渺茫。代码如下所示:

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

对于典型的现代计算机(即,任何使用二进制补码的计算机),转换为无符号实际上是一种操作——只是查看相同位的方式发生了变化。

请注意,在典型情况下,您可以upper-lower在(假定的)循环之外进行预计算,因此通常不会占用任何大量时间。除了减少分支指令的数量外,这还(通常)改进了分支预测。在这种情况下,无论数字低于范围的底端还是高于范围的顶端,都会采用相同的分支。

至于它是如何工作的,基本思想非常简单:当被视为无符号数时,负数将大于任何开始为正数的东西。

在实践中,此方法将number区间转换为原点并检查是否number在区间内[0, D],其中D = upper - lower. 如果number低于下限:负数,如果高于上限:大于D

于 2013-06-13T19:34:04.050 回答
22

很少能对如此小规模的代码进行重大优化。巨大的性能提升来自于从更高级别观察和修改代码。您也许可以完全消除对范围测试的需要,或者只做 O(n) 而不是 O(n^2)。您可以重新排序测试,以便始终隐含不等式的一侧。即使算法是理想的,当您看到此代码如何进行 1000 万次范围测试并找到一种方法将它们批量化并使用 SSE 并行执行许多测试时,更有可能获得收益。

于 2013-06-13T19:34:26.823 回答
17

这取决于您希望对相同数据执行多少次测试。

如果您只执行一次测试,则可能没有一种有意义的方法来加速算法。

如果您对一组非常有限的值执行此操作,那么您可以创建一个查找表。执行索引可能会更昂贵,但如果您可以将整个表放入缓存中,那么您可以从代码中删除所有分支,这应该会加快速度。

对于您的数据,查找表将为 128^3 = 2,097,152。如果您可以控制三个变量之一,以便start = N同时考虑所有实例,那么工作集的大小会下降到128^2 = 16432字节,这应该很适合大多数现代缓存。

您仍然需要对实际代码进行基准测试,以查看无分支查找表是否比明显的比较快得多。

于 2013-06-13T19:32:32.387 回答
4

这个答案是报告使用接受的答案完成的测试。我对一个大的排序随机整数向量进行了封闭范围测试,令我惊讶的是(low <= num && num <= high)的基本方法实际上比上面接受的答案更快!测试是在 HP Pavilion g6(AMD A6-3400APU,6GB 内存)上完成的。这是用于测试的核心代码:

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

与以下是上面公认的答案相比:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

注意 randVec 是一个排序向量。对于任何大小的 MaxNum,第一种方法在我的机器上胜过第二种方法!

于 2017-02-03T07:54:23.537 回答
3

对于任何变量范围检查:

if (x >= minx && x <= maxx) ...

使用位运算更快:

if ( ((x - minx) | (maxx - x)) >= 0) ...

这会将两个分支合并为一个。

如果您关心类型安全:

if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...

您可以将更多变量范围检查组合在一起:

if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...

这会将 4 个分支减少为 1 个。

它比 gcc 中的旧版本快 3.4 倍:

在此处输入图像描述

于 2020-06-02T10:07:38.687 回答
0

我可以确切地告诉你为什么这很重要。想象一下,您正在模拟一个 MMU。您必须不断地确保给定的内存地址与给定的页面集一起存在。这些小东西加起来很快,因为你总是说

  • 这个地址有效吗?
  • 该地址属于哪个页面?
  • 这个页面有什么权利?
于 2021-02-16T02:47:53.837 回答
-4

不能只对整数执行按位运算吗?

由于它必须介于 0 和 128 之间,如果设置了第 8 位 (2^7),则它是 128 或更多。但是,边缘情况会很痛苦,因为你想要一个包容性的比较。

于 2013-06-14T03:36:33.510 回答