【问题标题】:Fastest way to determine if an integer is between two integers (inclusive) with known sets of values确定整数是否在具有已知值集的两个整数(包括)之间的最快方法
【发布时间】:2013-06-10 07:49:20
【问题描述】:

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

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

更新:在尝试accepted answer 之后,我在一行代码上的速度比正常的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

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

【问题讨论】:

  • 你为什么担心这对你来说不够快?
  • 谁在乎为什么,这是一个有趣的问题。只是为了挑战而挑战。
  • @SLaks 所以我们应该盲目地忽略所有这些问题,只说“让优化器来做?”
  • 问这个问题的原因并不重要。这是一个有效的问题,即使答案是
  • 这是我的一个应用程序功能的瓶颈

标签: c++ c performance math


【解决方案1】:

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

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

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

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

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

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

【讨论】:

  • @TomásBadan:在任何合理的机器上,它们都是一个循环。贵的是分店。
  • 由于短路而进行了额外的分支?如果是这种情况,lower &lt;= x &amp; x &lt;= upper(而不是lower &lt;= x &amp;&amp; x &lt;= upper)也会带来更好的性能吗?
  • @AK4749, jxh:尽管这个块很酷,但我不愿投票,因为不幸的是,没有任何迹象表明这在实践中更快(直到有人对生成的汇编程序和分析进行比较信息)。据我们所知,OP 的编译器可能会使用单个分支操作码来渲染 OP 的代码...
  • 哇!!!这导致我的应用程序针对这行特定的代码行了一个数量级的改进。通过预先计算上下,我的分析从这个函数的 25% 时间减少到不到 2%!瓶颈现在是加减运算,但我认为现在可能已经足够好了:)
  • 啊,现在@PsychoDad 已经更新了这个问题,很明显为什么这会更快。 真实代码在比较中有副作用,这就是编译器无法优化短路的原因。
【解决方案2】:

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

【讨论】:

  • 尽管投反对票,但我坚持我的回答:生成的程序集(请参阅已接受答案的评论中的 pastebin 链接)对于像素处理函数的内部循环中的某些东西来说非常糟糕。公认的答案是一个巧妙的技巧,但它的戏剧性效果远远超出了每次迭代消除一小部分分支的合理预期。一些次要效应占主导地位,我仍然希望在这一测试中优化整个过程的尝试将使巧妙的范围比较所获得的收益付诸东流。
【解决方案3】:

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

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

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

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

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

【讨论】:

  • 所以你会存储某种给定值的查找,开始和结束,它会包含一个 BOOL 告诉你它是否介于两者之间?
  • 正确。这将是一个 3D 查找表:bool between[start][end][x]。如果你知道你的访问模式会是什么样子(例如 x 是单调递增的),你可以设计这个表来保持局部性,即使整个表不适合内存。
  • 我会看看我是否可以尝试这种方法,看看效果如何。我计划在每行使用一个位向量来执行此操作,如果该点在圆圈中,则将设置该位。认为与位掩码相比,这将比字节或 int32 更快?
【解决方案4】:

此答案用于报告使用已接受答案完成的测试。我对一个大的排序随机整数向量进行了封闭范围测试,令我惊讶的是(low

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

【讨论】:

  • 我的数据没有排序,我的测试在 iPhone arm CPU 上。不同数据和 CPU 的结果可能会有所不同。
  • 在我的测试中排序只是为了确保上限不小于下限。
  • 排序后的数字意味着分支预测将非常可靠,并且除了切换点的少数分支外,所有分支都正确。无分支代码的优势在于,它将摆脱对不可预测数据的此类错误预测。
【解决方案5】:

对于任何变量范围检查:

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 times faster

【讨论】:

    【解决方案6】:

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

    • 这个地址有效吗?
    • 此地址属于哪个页面?
    • 这个页面有什么权利?

    【讨论】:

      【解决方案7】:

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

      由于它必须在 0 和 128 之间,如果第 8 位设置为 (2^7),则它是 128 或更多。但是,边缘情况会很痛苦,因为您需要进行包容性比较。

      【讨论】:

      • 他想知道x &lt;= end,其中end &lt;= 128。不是x &lt;= 128
      • 这句话“因为它必须在 0 和 128 之间,如果第 8 位被设置 (2^7) 它是 128 或更多”是错误的。考虑 256。
      • 是的,显然我没有充分考虑这一点。对不起。
      猜你喜欢
      • 2010-09-22
      • 2012-11-17
      • 2023-01-09
      • 2019-06-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多