确定整数是否在具有已知值集的两个整数(包括两个整数)之间的最快方法


389

有没有比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

令人惊讶的是,减少或消除分支可以提供如此惊人的速度。


28
您为什么担心这还不够快?
马特·鲍尔

90
谁在乎为什么,这是一个有趣的问题。为了挑战而只是挑战。
大卫说恢复莫妮卡

46
@SLaks因此,我们应该盲目地忽略所有这些问题,而只是说“让优化器去做?”
大卫说恢复莫妮卡(Monica)2013年

87
为什么要问这个问题都没有关系。即使答案是否定的
tay10r 2013年

41
这是我的一个应用程序中的一个功能瓶颈
jjxtra 2013年

Answers:


527

只有一个比较/分支才能做到这一点。能否真正提高速度可能尚有待商question,即使这样做,也可能很少引起注意或关注,但是当您仅从两个比较开始时,进行巨大改进的机会就很少了。代码如下:

// 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


8
@TomásBadan:在任何合理的机器上,它们都将是一个循环。最昂贵的是分支机构。
奥利弗·查尔斯沃思

3
是否因短路而进行了其他分支?如果是这样,lower <= x & x <= upper(而不是lower <= x && x <= upper)更好的性能吗?
Markus Mayr 2013年

6
@ AK4749,jxh:尽管很酷,但我还是犹豫不决,因为不幸的是,没有任何东西可以暗示这在实践中会更快(直到有人对生成的汇编程序和性能分析信息进行比较为止)。就我们所知,OP的编译器可以使用单个分支操作码来呈现OP的代码...
Oliver Charlesworth

152
哇!!!这导致我的应用程序中针对此特定代码行的数量级改进。通过预先计算上下,我的分析从该函数的25%时间减少到不到2%!现在瓶颈是加法和减法运算,但我认为现在可能已经足够了:)
jjxtra

28
嗯,现在@PsychoDad已更新了问题,很清楚为什么这样做更快。的真实代码具有在比较中,这就是为什么编译器不能优化短路离开的副作用。
奥利弗·查尔斯沃思

17

能够如此大规模地进行重大优化以进行编码的情况很少见。从更高层次上观察和修改代码,可以显着提高性能。您也许可以完全不需要范围测试,或者只对它们进行O(n)而不是O(n ^ 2)。您可能可以对测试进行重新排序,以便始终隐含不平等的一侧。即使该算法是理想的算法,当您看到此代码如何进行1000万次范围测试并找到一种将它们进行批处理并使用SSE并行进行许多测试的方法时,获得收益的可能性更大。


16
尽管投票不赞成,但我仍然坚持我的回答:对于像素处理函数内循环中的某些内容,生成的程序集(请参见注释中的pastebin链接)已被接受。公认的答案是一个巧妙的窍门,但是它的戏剧性效果远远超出了合理的预期,因为它消除了每次迭代的一小部分分支。一些次要作用占主导地位,我仍然希望在这一测试中尝试优化整个过程会在尘埃中留下精巧的范围比较优势。
本杰克逊

17

这取决于您要对同一数据执行测试的次数。

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

如果要对一组非常有限的值进行此操作,则可以创建一个查找表。执行索引可能会更昂贵,但是如果您可以将整个表容纳在缓存中,则可以从代码中删除所有分支,这样可以加快处理速度。

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

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


因此,您将存储某种给定值,开始和结束的查找,并且它将包含一个BOOL告诉您它是否介于两者之间?
jjxtra 2013年

正确。这将是一个3D查找表:bool between[start][end][x]。如果您知道访问模式将是什么样(例如x呈单调递增),则可以设计表以保留局部性,即使整个表都无法容纳在内存中也是如此。
Andrew Prock

我将看看是否可以尝试这种方法并看看它如何进行。我打算用每条线的位向量进行操作,如果该点在圆中,则将设置位。认为与位掩码相比,它会比字节或int32更快吗?
jjxtra 2013年

2

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

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,第一种方法在我的计算机上都胜过第二种方法!


1
我的数据未排序,测试在iPhone臂CPU上进行。使用不同数据和CPU的结果可能会有所不同。
jjxtra

在我的测试中排序仅是为了确保上限不小于下限。
rezeli

1
排序的数字表示分支预测将非常可靠,并且除切换点处的少数分支外,所有分支均正确无误。无分支代码的优点是,它将消除对不可预测数据的这类错误预测。
安德烈亚斯·克莱宾格

0

对于任何可变范围检查:

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倍

在此处输入图片说明


-4

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

由于必须在0到128之间,因此如果设置了第8位(2 ^ 7),则它为128或更大。但是,边缘情况会很麻烦,因为您想要一个包容性的比较。


3
他想知道是否x <= end在哪里end <= 128。不x <= 128
Ben Voigt 2013年

1
这条语句“ 由于必须在0到128之间,因此如果设置了第8位(2 ^ 7),则它等于或大于128 ”是错误的。考虑256
快乐儿童绿色小睡

1
是的,显然我认为还不够。抱歉。
icedwater 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.