Fastest way to determine if an integer is between two integers (inclusive) with known sets of values
在C或C++中是否有一种比EDCOX1(0)更快速的方法来测试一个整数是否在两个整数之间?
更新:我的特定平台是iOS。这是框模糊函数的一部分,它将像素限制在给定正方形中的一个圆上。
更新:在尝试了被接受的答案之后,我在一行代码上得到了一个数量级的加速,超过了正常的x >= start && x <= end方式。
更新:下面是Xcode汇编程序的前后代码:
新途径
1 2 3 4 5 6 7 8 9 10 11
| // 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 |
老路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #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 |
非常令人惊讶的是,减少或消除分支可以提供如此惊人的速度。
- 你为什么担心这对你来说不够快?
- 这是应用程序中的瓶颈测试吗?
- 别担心。优化器非常好。
- 谁在乎为什么,这是个有趣的问题。这只是一个挑战,为了挑战。
- @德格林91:不是真的。它具体取决于哪个编译器、优化器、平台、数据类型,以及谁知道还有什么。
- @所以我们应该盲目地忽略所有这些问题,然后说"让优化器来做吧?"
- 问这个问题的原因无关紧要。这是一个有效的问题,即使答案是否定的。
- 我认为这个问题在c中毫无意义。如果你问"在一个非常具体的平台上组装",可能会有一个合理的答案。正如所问,这不是一个有效的问题,甚至是一个学术问题。
- 这是我的一个应用程序的一个功能瓶颈
- 开始、结束和值都在0和n之间,其中n通常小于128
- @DGRIN91:不,我们应该要求这些提问者提供更多细节。
- 你有没有尝试过不懒惰的和江户人的做法(为了避免额外的分支)
- @关于斯莱克斯,我们应该要求这些提问者提供更多的细节:这不是你所做的。你的评论是不用担心的。优化器非常好。
- "这是盒子模糊功能的一部分"iPhone没有遮影器吗?
- @sigterm是的,它有明暗器,但我的应用程序对所有内容都使用quartz2d和cgbitmap上下文。
- 我想你的原始代码中有一个错误。如果第一次比较是错误的,它不会增加p。新的代码总是以p为增量,这可能解释了大部分的速度提高。
- 你用你以前的方式在薄冰上滑冰,因为增量并不能保证按照你可能假设的顺序发生。对于这一点,我根本不理解它为什么会存在,因为您似乎在增加值,而不是一个指针。
- @JXH是故意的。我不希望P在原始代码中增加以节省几个周期。一旦它越过圆的右边缘或下边缘,条件将返回false,而不进行第二次比较。从视觉上看,两种方法的模糊效果是一样的。
- 我不建议在宏中使用p++,而只使用p。它可能会使您的代码稍微短一些,因为您不必在使用宏后增加p,但通过将其包含在宏中,您可能违反了大多数程序员可能会做出的假设(边界检查不会修改检查中涉及的值)。如果每个宏在averag上使用的周期数不同e足以产生很大的性能差异,那么您可能需要考虑减少宏的使用次数。微选项有时会忽略实际的性能问题。
- @jab好点,我将重命名宏以指示正在进行增量。
- 我收回它,我没有意识到&&定义了一个序列点。请参阅stackoverflow.com/questions/4176328/&hellip;。我想你用一个指针引用来调用宏,比如*p?那就行了,但这不是最容易遵循的代码。
- @Markransom正确。是的,这不是最容易遵循的代码,但它是一个指针引用。
- @等等,如果它是一个指针引用,并且基于用法(应该通过在递增旁边使用点符号注意到它),那么p是一个迭代器吗?我想我可以看到,如果迭代器中过载的++没有得到内联/优化,这将如何提供性能影响。
- @jab它不是一个迭代器,它只是一个指向结构的指针。
- 仅供参考,您应该将这些#define转换为内联函数(它根本不会影响性能)。请看这里,了解原因。
- @BlueRaja Dannypflughoeft谢谢你的链接,我会的。
- PsychoDad,我确实没有这么多的C++经验,但就我所知,一个指向结构(或类实例)的指针仍然需要EDCOX1×0的访问成员,就像在C中一样,如果它是对结构的引用,我不认为这是必要的,但是这将不允许在没有过载的情况下递增。e ++运算符。
- 数据加载优化怎么样?是否尝试使用较小的数据类型,并将其保存在另一个寄存器的偏移量中?当一个32B数据类型中有两个值时,仍然可以用ldr加载它,然后使用要比较的偏移量。
- 你真的应该早点发布你真正的代码。++有很大的不同…
- 出于好奇,这个函数被用在我写的应用程序的blur工具中,you doodle for ios-bit.ly/you doodle app
只有一个比较/分支可以做到这一点。它是否真的会提高速度可能是个问题,即使它确实提高了,也可能是太少的注意或关心,但当你仅仅从两个比较开始时,一个巨大的改进的机会是相当遥远的。代码如下:
1 2 3 4 5 6
| // 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。
- 相当整洁。但这确实依赖于EDOCX1[1]而不是溢出…
- @奥利查尔斯沃思:是的,但他说两者都大于0,所以不能溢出(即上下<上)。
- 很好,但我现在很好奇。从机器周期的角度来看,减法器并不比比较昂贵吗?
- @杰里科芬:啊,我没注意到!
- @汤姆和斯巴丹:在任何合理的机器上,它们都是一个循环。最贵的是树枝。
- 在我的例子中,上下可以预先计算,因为我的数据结构包含一个开始和结束值,很好!
- @AK4749:这就是为什么产生这样的掘金的问题也应该得到一个赞成票。
- 由于短路,是否进行了额外的分支?如果是这样,EDOCX1(而不是lower <= x && x <= upper)是否也会带来更好的性能?
- @AK4749,JXH:虽然这个金块很酷,但我还是犹豫是否要投赞成票,因为不幸的是,没有什么东西可以表明这在实践中更快(除非有人比较结果汇编程序和分析信息)。据我们所知,op的编译器可以用一个分支操作码来呈现op的代码…
- @markusmayr,优化器可能会使用假设规则替换您,因为比较整数没有副作用。
- 真的!!!!这导致我的应用程序在这一特定代码行上有了一个数量级的改进。通过预计算上下,我的配置文件从这个函数的25%的时间减少到不足2%!瓶颈现在是加法和减法运算,但我认为现在已经足够好了:)
- @Olicharlesworth,即使测试表明它在大多数处理器和编译器上没有什么不同,甚至更糟,如果有一个更好的地方,那么它是一个有价值的答案。
- @Markransom我希望任何编译器都能以这种方式优化代码。但是如果是这样的话,我就不明白为什么答案中的代码要比OP提供的代码快很多。
- @心理医生:为了满足我们之间的好奇心,您介意将编译器在每种情况下生成的汇编程序发布出去吗?
- @psycholidad编译器通常支持-S选项来生成程序集文件。实际上,我也会对原始(慢)代码的组装感兴趣。
- 在xcode中找到它,因为它所在的文件超过1000行,现在棘手的部分将找到确切的片段:)
- 我认为这是比较复杂的代码,但我可能是错的,有人告诉我这看起来有点离谱:ltmp1313:ldr r0,[sp,176]@4字节重新加载ldr r1,[sp,164]@4字节重新加载ldr r0,[r0]ldr r1,[r1]sub.w r0,r9,r0 cmp r0,r1 blo lbb44_30
- @精神病患者看起来像这样,除了number和upper-lower似乎来自struct或全球。
- 是的,它们来自具有start、end和diff属性的结构
- 我认为这是较慢的版本:ltmp1301:ldr r1,[sp,172]@4字节重新加载ldr r1,[r1]cmp r0,r1 bls lbb44_32 mov r6,r0 b lbb44_33 lbb44_32:ldr r1,[sp,188]@4字节重新加载添加r6,r0,1 ltmp1302:ldr r1,[r1]cmp r0,r1 bhs lbb44_36
- 出于好奇,所涉及的算法是一个方框模糊,它将模糊的像素限制为一个圆。包含检查是检查当前像素是否为圆中的点。
- @绝对是寡头。我认识到优化器最终可能会生成类似的代码,所以我在一定程度上理解了您的观点。然而,我也看到杰里对他的答案有一些理论上的支持,这使得它(IMO)成为一个很好的答案。不过,我知道这不是一个显而易见的"好答案"。我很矛盾
- 汇编程序的一个粘贴箱,希望是更可读的格式。
- @心理医生:听起来像是GPU应该做的事情…
- @markusmayr使用gcc explorer进行检查,使用&&和&生成完全相同的代码,这就是短路(&&方法。
- @BlueRajaDannypflughoeft整个应用程序都是用核心图形完成的,因此此时OpenGL重写会有点痛苦。即使我的iPhone4S带有2048x2048图像,性能也相当不错。
- @精神病患者,你真的应该把这些细节放在你的问题中(或者适当地把它们编辑成可接受的答案)。评论对于这样的好信息来说是一个不好的地方。信息会丢失,迟早,别人很难理解。
- 啊,现在,@psycholidad更新了这个问题,很清楚为什么会更快。实际的代码在比较中有一个副作用,这就是为什么编译器不能优化短路。
- 我想知道是否可以在Java中应用这个技巧,因为Java没有无符号整数。
- 你有没有提到我可以引用的"老把戏"?
- @不好意思,但不,不是真的。
- @帕特里克萨南:有点晚了,我知道,但我认为这个特别的技巧在亨利·S·沃伦的《黑客之乐》中有详细介绍(还有很多其他很酷的低级黑客)。
很少有人能够对如此小规模的代码进行显著的优化。从更高的级别观察和修改代码可以获得很大的性能提升。您可以完全消除对范围测试的需要,或者只做它们的O(n)而不是O(n^2)。你可以重新排序测试,这样不平等的一面总是隐含的。即使算法是理想的,当您看到这个代码如何进行1000万次范围测试,并且您找到一种方法将它们成批处理,并使用SSE并行执行许多测试时,也更有可能获得收益。
- 尽管有否决票,我还是坚持我的答案:生成的程序集(参见对已接受答案的注释中的Pastebin链接)对于像素处理函数的内部循环中的某些内容来说相当糟糕。公认的答案是一个巧妙的技巧,但其戏剧性的效果远远超出了每次迭代消除一部分分支的合理预期。一些次要的影响占主导地位,我仍然希望通过这一次测试来优化整个过程的尝试将使巧妙的范围比较的收益化为乌有。
这取决于您希望对同一数据执行测试的次数。
如果您只执行一次测试,那么可能没有一种有意义的方法来加快算法的速度。
如果要对非常有限的一组值执行此操作,则可以创建查阅表格。执行索引可能会更昂贵,但是如果您可以在缓存中容纳整个表,那么您可以从代码中删除所有分支,这将加快速度。
对于您的数据,查找表将是128^3=2097152。如果您可以控制这三个变量中的一个,这样您就可以考虑一次使用start = N的所有实例,那么工作集的大小将下降到128^2 = 16432字节,这应该很适合大多数现代缓存。
您仍然需要对实际代码进行基准测试,以查看无分支查找表是否比明显的比较快得多。
- 所以,如果给定一个值,从开始到结束,您将存储某种查找,它将包含一个bool,告诉您它是否介于两者之间?
- 对的。它将是一个三维查找表:bool between[start][end][x]。如果您知道您的访问模式将是什么样子(例如x是单调递增的),那么即使整个表不适合内存,您也可以设计表来保留位置。
- 我会看看我是否可以尝试这个方法,看看它是如何发展的。我计划每行使用一个位向量,如果点在圆中,那么位将被设置。认为这将比字节或Int32的位屏蔽更快吗?
此答案将报告使用已接受答案完成的测试。我对排序后的随机整数的一个大向量进行了一个闭区间测试,令我惊讶的是,(low<=num&;num<=high)的基本方法实际上比上面接受的答案快!在带有6GB内存的HP Pavilion G6(AMD A6-3400APU)上进行了测试。下面是用于测试的核心代码:
1 2 3 4 5 6 7 8 9 10 11 12
| 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; |
与上述公认答案相比:
1 2 3 4 5 6
| 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的结果可能不同。
- 在我的测试中排序只是为了确保上限不小于下限。
- 排序后的数字意味着分支预测将非常可靠,除了几个在切换点的分支外,所有分支都会正确。无分支代码的优点是它可以消除对不可预知数据的这种错误预测。
不可能只对整数执行按位运算吗?
因为它必须在0和128之间,如果第8位被设置为(2^7),则它是128或更多。不过,边缘情况将是一种痛苦,因为您希望进行包容性比较。
- 他想知道是不是以东十一〔3〕,在哪里以东十一〔4〕。不是x <= 128。
- 此语句"因为它必须介于0和128之间,如果设置了第8位(2^7),则它等于或大于128"是错误的。考虑256。
- 是的,显然我没有充分考虑到这一点。对不起的。