更高效的地板方式可以获得数组索引

时间:2013-05-09 20:12:37

标签: c performance floating-point

我有double xdouble y。我需要将其转换为int boxnum(x,y)被定义为({1}}落入WIDTH x HEIGHT网格中的BOX_SIZE索引,其大小为WIDTH的正方形。超过HEIGHT的坐标被包裹回来;同上( (((int)(x))/BOX_SIZE)%WIDTH+ WIDTH*((((int)(y))/BOX_SIZE)%HEIGHT) )

我目前正在使用:

( (( ((int)(x)) /BOX_SIZE)%WIDTH+WIDTH)%WIDTH
    +WIDTH*(( (((int)(y)) /BOX_SIZE)%HEIGHT+HEIGHT)%HEIGHT) )

这句话目前占用了我执行时间的20%,如果我对负坐标完全安全的话,情况会更糟(大约40-50%):

0<x<WIDTH*BOX_SIZE

我实际上正在考虑将应用程序完全转换为定点,只是为了避免这种情况,以便我可以对我想要的部分进行掩码,而不是进行这种可怕的转换。

有没有更好的方法来进行这种双重&gt; int转换?是否值得确保0<y<HEIGHT*BOX_SIZEx以便我可以放弃其余的两个操作? (这样做很难在基准测试中不值得,除非它可能是一个重大改进)

编辑:在评论中进行适当的惩罚后,更多细节:

yWIDTH是一组(多达10 ^ 6)个粒子的坐标。我正在使用一种算法,它要求我在每个时间步,对一个框内的所有粒子做一些简单的求和。因此,我遍历粒子,计算粒子所在的盒子,然后将其用作添加到该盒子的数组索引。粒子通常移动得足够远,以至于它们过去的位置并不能表明它们未来的位置。这也是无序的,这意味着我不能对此做出任何假设。

只要HEIGHTBOX_SIZEWIDTH的倍数,

HEIGHTBOX_SIZEBOX_SIZE=1在技术上是免费的。实际上,它们都是指定的编译时间,并且是WIDTH=HEIGHT=4的整数。我已经运行了从WIDTH=HEIGHT=512WIDTH=37;HEIGHT=193的所有内容,而我通常使用2的平方幂(因为为什么不是?),10 particles/box * 100 WIDTH * 100 HEIGHT* 10000 steps = 1 billion particle*timesteps应该没有问题。

每个粒子每个时间步执行一次该计算是不可避免的;在当前实现中,它执行两次。我尝试缓存该值以避免重新计算,但最终基准测试表现更差,所以我回去计算它两次。

double的基本测试运行时间超过一分钟。

这些坐标是他们的“常规数字”(1-1000)的顺序,所以我在{{1}}上没有任何约束。

2 个答案:

答案 0 :(得分:7)

代码的问题在于(int)强制转换导致浮点单元的舍入模式从IEEE754默认轮更改为最近的到C标准回合朝向零或'截断',因为它在标准中定义。

有关IEEE754舍入模式的更多信息,请参阅gcc docs here

在现代深度流水线处理器上,当舍入模式发生变化时,必须刷新整个管道,导致每个(int)转换时清空pipleline时大幅减速。当您在循环中执行此操作时,您遇到的减速是典型的。

Erik de Castro Lopo(libsndfile和秘密兔子代码的作者)在这个问题上有一篇非常有趣的文章。在他的音频转换例程中,浮点舍入性能至关重要,他使用POSIX lrintf()调用以及非POSIX平台的一些x86程序集为这个问题提供了一组有趣的解决方案。

可以找到该文章here.

简短的回答是使用C99 / POSIX lrintf()函数,或使用一些内联汇编来执行整数截断而不更改浮点舍入模式。

答案 1 :(得分:3)

Division and Remainder

评论中暗示的一个问题是划分(和/或其余操作)可能很昂贵。与用于加法和乘法的单个周期相比,除法需要花费几十个处理器周期并不罕见。

避免这种费用的最简单方法可能是使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%的执行时间。您显示的索引计算没有缓存影响,因为它们不访问内存。但是,编译后的代码通常会产生交错的指令:每个源代码语句都会导致生成多个指令,不同语句的指令因各种原因而交错,而不是作为一个语句的所有指令出现,然后是所有指令。另一个陈述,等等。这种交错使得软件报告难以准确地指示处理器时间的消耗。

还有其他影响干扰此类测量。通过采样进行一些测量:处理器每隔一段时间中断,并记录中断时程序计数器的值。这表明你是处理器正在等待的东西,但不是它等待的东西。例如,如果xint的转换等待浮点单元可用但没有单元可用,因为先前的指令正在执行添加完全不相关的数据,那么事实20%的样本似乎在(int) x中有误导性。

您运行一百万个粒子这一事实与某些数据访问一致,导致缓存抖动并降低性能。另一方面,添加更多余数操作(用于支持负坐标)这一事实使得索引计算看起来消耗更多时间反指示缓存问题。

然而,这些索引计算消耗程序的大部分时间是不常见的,除非程序几乎没有其他工作。

如果你能展示证明问题的自包含可编译代码,那可能会有所帮助。