更有效的地板双打方法以获得数组索引
Posted
技术标签:
【中文标题】更有效的地板双打方法以获得数组索引【英文标题】:more efficient way of flooring doubles to get array indices 【发布时间】:2013-05-09 20:12:37 【问题描述】:我有double x
和double y
。我需要将其转换为int boxnum
,它被定义为(x,y)
落在WIDTH x HEIGHT
网格中的(下限)索引,其大小为BOX_SIZE
。超过WIDTH
的坐标被回绕; HEIGHT
同上。
我目前正在使用:
( (((int)(x))/BOX_SIZE)%WIDTH+ WIDTH*((((int)(y))/BOX_SIZE)%HEIGHT) )
这个语句目前占用了我 20% 的执行时间,如果我让它对负坐标完全安全,情况会变得更糟(大约 40-50%):
( (( ((int)(x)) /BOX_SIZE)%WIDTH+WIDTH)%WIDTH
+WIDTH*(( (((int)(y)) /BOX_SIZE)%HEIGHT+HEIGHT)%HEIGHT) )
我实际上正在考虑将应用程序完全转换为定点,只是为了避免这种情况,这样我就可以掩码掉我想要的部分,而不是进行这种可怕的转换。
有没有更好的方法来进行这种 double->int 转换?确保0<x<WIDTH*BOX_SIZE
和0<y<HEIGHT*BOX_SIZE
这样我可以放弃两个余数操作是否值得? (这样做太难了,不值得作为基准,除非它可能是一个显着的改进)
编辑:在 cmets 进行适当的惩罚后,更多细节:
x
和 y
是一组(多达 10^6 个)粒子的坐标。我正在使用一种算法,该算法要求我在每个时间步长对一个盒子内的所有粒子进行一些简单的求和。因此,我遍历粒子,计算粒子在哪个盒子中,然后将其用作添加到该盒子的数组索引。粒子经常移动得足够远,以至于它们过去的位置并不能表明它们未来的位置。它们也是无序的,这意味着我不能对此做出任何假设。
WIDTH
、HEIGHT
和 BOX_SIZE
在技术上是免费的,只要 WIDTH
和 HEIGHT
是 BOX_SIZE
的偶数倍数。实际上,它们都是指定的编译时间,并且是带有BOX_SIZE=1
的整数。我已经运行了从 WIDTH=HEIGHT=4
到 WIDTH=HEIGHT=512
的所有内容,虽然我通常是 2 的平方幂(因为为什么不呢?),WIDTH=37;HEIGHT=193
应该可以正常工作。
这个计算是不可避免的,每个粒子每个时间步执行一次;在当前的实现中,它被执行了两次。我尝试缓存该值以避免重新计算,但最终基准的表现更差,所以我又重新计算了两次。
使用10 particles/box * 100 WIDTH * 100 HEIGHT* 10000 steps = 1 billion particle*timesteps
的基本测试在阴凉处运行了一分钟。
这些坐标的顺序是它们的“常规数字”(1-1000),所以我在double
上没有任何限制。
【问题讨论】:
这是哪个更大的算法的一部分? x 和 y 是如何生成或指定的?您是否可以控制 BOX_SIZE、WIDTH 和 HEIGHT 的(大概)常量值?并不是说转换为定点不一定允许您将其更改为掩码操作 我认为这是一个与@jerry 的第一个问题类似的问题,只是措辞略有不同:您是否对传递给函数的每个数字执行昂贵的操作?还是只在 x 为负等时对 x 做双余? 无法在黑暗中有效地诊断出此类性能问题。相关因素包括 WIDTH 和 HEIGHT 是否是编译时常量、它们的具体值、相对于其他代码执行这些计算的频率、索引中是否有任何可以使用的模式(例如遍历列或对角线)从循环中提升计算,以及 x 和 y 相对于它们的类型的潜在值范围(是否有足够的空间添加 WIDTH 的倍数以便可以消除第一个%
?)。
对不起,如果我的评论被认为是责备,那不是我的意图。我猜您正在使用高优化级别进行编译,因此如果您可以通过选择 2 的幂和移位/屏蔽来节省周期,那么您的编译器可能已经这样做了。不过,以防万一,您是否测试过显式移位和屏蔽?
你说的“……他们……是BOX_SIZE=1
的整数”是什么意思?
【参考方案1】:
您的代码的问题在于 (int)
强制转换导致浮点单元的舍入模式从 IEEE754 默认 round to nearest 更改为 C 标准 向零舍入 或标准中定义的“截断”。
有关 IEEE754 舍入模式的更多信息,请参阅 gcc 文档 here。
在现代深度流水线处理器上,当舍入模式发生变化时,必须刷新整个流水线,导致每次(int)
强制转换时流水线都被清空,从而导致速度大幅下降。当您循环执行此操作时,您遇到的减速是典型的。
Erik de Castro Lopo(libsndfile 和秘密兔子代码的作者)有一篇关于这个问题的非常有趣的文章。在他的音频转换例程中,浮点舍入性能至关重要,他使用 POSIX lrintf()
调用以及一些用于非 POSIX 平台的 x86 程序集为这个问题提供了一组有趣的解决方案。
文章可以找到here.
简短的回答是使用 C99/POSIX lrintf()
函数,或者使用一些内联汇编来执行整数截断而不更改浮点舍入模式。
【讨论】:
哇我不知道这会引起这么多问题;当我使用callgrind
进行分析时,我刚刚看到了一个巨大的惩罚,当我转向做更多的模运算时,惩罚增加了很多,所以我认为这就是问题所在。我会试着调查一下,谢谢。
这个答案不正确。处理器通常具有不使用浮点舍入模式将浮点数转换为整数的指令,例如 IA-32 指令 CVTTSD2SI 或旧的 FISTTP。
我刚刚发现 Eric 是正确的。虽然读起来很酷,但我发现它最初使用的是cvttsd2si
,当我将其更改为使用lrint()
时,生成的asm 使用了cvtsd2si
。区别似乎在于有符号数字的工作方式,在性能上没有真正的区别。
我不知道cvttsd2si
。在阅读 ASM 文档时,@EricPostpischil 似乎走在了正确的轨道上。我很想看到一个 SSCCE,并在我自己的机器上对此进行调查。【参考方案2】:
除法与余数
在 cmets 中已经暗示的一个问题是除法(和/或余数运算)可能很昂贵。与加法和乘法的单个周期相比,除法需要几十个处理器周期并不少见。
避免这种开销的最简单方法可能是使 WIDTH 和 HEIGHT 编译时常量为 2 的幂。这允许编译器将使用% WIDTH
或% HEIGHT
的余数运算更改为快速位掩码运算。同样,如果 BOX_SIZE 是一个 2 的幂的编译时常量,它允许编译器将除法更改为位移位。
这也是我评论将 ((int) x / BOX_SIZE % WIDTH + WIDTH) % WIDTH
更改为 ((int) x / BOX_SIZE + Number) % WIDTH
的原因,其中 Number 是 WIDTH 的某个倍数,因此总和保证为非负数。这消除了余数运算。 (但是,您提出这个表达式来处理负坐标,它可能有一个缺陷:(int) x / BOX_SIZE
将商向零舍入,这可能会为负 x 提供错误的框数。所以您可能需要在我们之前修复这个表达式考虑优化方面。)
其他
通常,我会怀疑缓存和处理器时间对源代码的不精确归因是索引计算似乎需要 20% 的执行时间的原因。您显示的索引计算没有缓存影响,因为它们不访问内存。但是,编译后的代码往往会导致指令交错:每条源代码语句都会产生多条指令,而不同语句的指令由于各种原因被交错,而不是作为一个语句的所有指令出现,然后所有的指令另一个声明,依此类推。这种交错使得软件性能报告难以准确指示处理器时间消耗的位置。
还有其他影响此类测量的影响。通过采样进行一些测量:处理器每隔一段时间中断一次,并记录中断时程序计数器的值。这表明您是处理器正在等待的东西,而不是它正在等待的东西。例如,如果 x
到 int
的转换正在等待一个浮点单元可用,但由于前一条指令正在执行完全不相关的数据相加,所以没有可用的单元,那么 20% 的样本似乎在(int) x
中具有误导性。
您正在对一百万个粒子进行操作这一事实与某些数据访问一致,这会导致缓存抖动和性能下降。另一方面,添加更多余数运算(用于支持负坐标)使索引计算看起来消耗更多时间这一事实反表明存在缓存问题。
但是,这些索引计算占用程序大部分时间是不寻常的,除非程序几乎没有做其他工作。
如果您可以展示演示该问题的自包含可编译代码,这可能会有所帮助。
【讨论】:
以上是关于更有效的地板双打方法以获得数组索引的主要内容,如果未能解决你的问题,请参考以下文章