C中的快速欧几里德分裂

时间:2009-07-16 04:31:29

标签: c bit-manipulation micro-optimization

我感兴趣的是获得Euclidean除法的余数,即对于一对整数(i,n),找到r如:

i = k * n + r, 0 <= r < |k|

简单的解决方案是:

int euc(int i, int n)
{
    int r;

    r = i % n;
    if ( r < 0) {
        r += n;
    }
    return r;
}

但是由于我需要执行数百万次(它在多维数组的迭代器中使用),我想尽可能避免分支。要求:

  • 分支但更快也是可取的。
  • 只适用于正n的解决方案是可以接受的(但它必须适用于负i)。
  • n预先不知道,并且可以是任何值&gt; 0和&lt; MAX_INT

修改

实际上很容易得到错误的结果,所以这里有一个预期结果的例子:

  • euc(0,3)= 0
  • euc(1,3)= 1
  • euc(2,3)= 2
  • euc(3,3)= 0
  • euc(-1,3)= 2
  • euc(-2,3)= 1
  • euc(-3,3)= 0

有些人还担心优化它是没有意义的。我需要这个用于多维迭代器,其中超出范围的项被“虚拟数组”中的项目替换,该虚拟数组重复原始数组。所以如果我的数组x是[1,2,3,4],那么虚拟数组是[....,1,2,3,4,1,2,3,4,1,2,3,4, 1,2,3,4],例如,x [-2]是x 1等...

对于维d的nd数组,我需要每个点都有欧几里德分区。如果我需要在一个n ^ d数组与m ^ d内核之间进行相关,我需要n ^ d * m ^ d * d euclidean除法。对于100x100x100点的3d图像和5 * 5 * 5点的内核,已经有大约4亿个欧几里德分区。

12 个答案:

答案 0 :(得分:7)

编辑:没有乘法或分支。

int euc(int i, int n)
{
    int r;

    r = i % n;
    r += n & (-(r < 0));

    return r;
}

这是生成的代码。根据MSVC ++仪器分析器(我的测试)和OP的测试,它们的表现几乎相同。

; Original post
00401000  cdq              
00401001  idiv        eax,ecx 
00401003  mov         eax,edx 
00401005  test        eax,eax 
00401007  jge         euc+0Bh (40100Bh) 
00401009  add         eax,ecx 
0040100B  ret              

; Mine
00401020  cdq              
00401021  idiv        eax,ecx 
00401023  xor         eax,eax 
00401025  test        edx,edx 
00401027  setl        al   
0040102A  neg         eax  
0040102C  and         eax,ecx 
0040102E  add         eax,edx 
00401030  ret              

答案 1 :(得分:5)

我认为280Z28和克里斯托弗的装配高尔夫比我更好,并且处理随机存取。

但是,您实际上在做什么,似乎正在处理整个数组。显然,出于内存缓存的原因,您已经希望在可能的情况下按顺序执行此操作,因为避免缓存未命中比避免小分支要好很多倍。

在这种情况下,首先使用合适的边界检查,你可以在我称之为“破折号”的内循环中进行。检查下一个k增量不会导致任一阵列上最小维度的溢出,然后使用一个新的更多内部循环“破坏”k步骤,这只是每次将“物理”索引增加1做另一个idiv。您或编译器可以展开此循环,使用Duff的设备等。

如果内核很小,特别是如果它是固定大小的话,那么(或者它的多个具有合适的展开以偶尔减去而不是添加)可能是用于“破折号”长度的值。编译时常量短划线长度可能是最好的,因为那时你(或编译器)可以完全展开破折号循环并忽略连续条件。只要这不会使代码太大而不能快,它实际上用整数增量替换整个正模运算。

如果内核不是固定大小,但在最后一个维度上通常非常小,请考虑为最常见的大小设置不同版本的比较函数,并在每个版本中完全展开破折号循环。

另一种可能性是计算溢出将发生的下一个点(在任一阵列中),然后冲到该值。在dash循环中仍然有一个延续条件,但它只使用增量来尽可能长。

或者,如果您正在进行的操作是数字相等或其他一些简单操作(我不知道“相关性”是什么),您可以查看SIMD指令或其他任何内容,在这种情况下,短划线长度应该是您架构上最宽泛的单指令比较(或适当的SIMD操作)的多个。不过,这不是我所经历过的。

答案 2 :(得分:3)

没有分支,但有点小事:

int euc2(int i, int n)
{
    int r;
    r = i % n;
    r += (((unsigned int)r) >> 31) * n;
    return r;
}

没有乘法:

int euc2(int i, int n)
{
    int r;
    r = i % n;
    r += (r >> 31) & n;
    return r;
}

这给出了:

; _i$ = eax
; _n$ = ecx

cdq
idiv   ecx
mov eax, edx
sar eax, 31
and eax, ecx
add eax, edx

答案 3 :(得分:2)

整数乘法比除法快得多。对于具有已知N的大量调用,您可以通过乘以N的伪逆来将除以N替换。

我将在一个例子中说明这一点。取N = 29。然后计算一次伪逆2 ^ 16 / N:K = 2259(从2259.86 ...截断)。我假设我是正面的,我* K适合32位。

Quo = (I*K)>>16;   // replaces the division, Quo <= I/N
Mod = I - Quo*N;   // Mod >= I%N
while (Mod >= N) Mod -= N;  // compensate for the approximation

在我的例子中,我们取I = 753,我们得到Quo = 25和Mod = 28。 (无需补偿)

EDIT。

在你的3D卷积示例中,对i%n的大多数调用都是在0..n-1中的i,因此在大多数情况下,第一行如

if (i>=0 && i<n) return i;

将绕过昂贵且无用的白痴。

另外,如果你有足够的RAM,只需将所有维度对齐为2的幂并使用位操作(移位和)而不是分割。

编辑2。

我实际上是在10 ^ 9个电话上尝试过的。我%:2.93s,我的代码:1.38s。请记住它意味着对I的约束(I * K必须适合32位)。

另一个想法:如果你的值是x + dx,x在0..n-1和dx中很小,那么下面将涵盖所有情况:

if (i<0) return i+n; else if (i>=n) return i-n;
return i;

答案 4 :(得分:1)

int euc(int i, int n)
{
    return (i % n) + (((i % n) < 0) * n);
}

答案 5 :(得分:1)

我使用TSC在gcc -O3中计划每个人的提议(除了常数N的那个),他们都花了相同的时间(在1%之内)。

我的想法是((i%n)+ n)%n(没有分支),或(i +(n <&lt; 16))%n(显然对于大n或极端负i而言失败)将是更快,但他们都花了相同的时间。

答案 6 :(得分:1)

我真的很喜欢这个词:

r = ((i%n)+n)%n; 

反汇编很短:

r =((i%n)+ n)%n;

004135AC  mov         eax,dword ptr [i] 
004135AF  cdq              
004135B0  idiv        eax,dword ptr [n] 
004135B3  add         edx,dword ptr [n] 
004135B6  mov         eax,edx 
004135B8  cdq              
004135B9  idiv        eax,dword ptr [n] 
004135BC  mov         dword ptr [r],edx 

它没有跳转(2个idiv,可能很昂贵),它可以完全内联,避免了函数调用的开销。

您怎么看?

答案 7 :(得分:1)

如果你的范围足够低,可以创建一个查找表 - 两个昏暗的数组。 你也可以使用Inline函数,并通过查看生成的代码来确保它。

答案 8 :(得分:0)

如果你还可以保证我永远不会小于-n,你可以简单地在模数之前加上可选的加法。这样,你不需要分支,如果你不需要,模数会删除你添加的内容。

int euc(int i, int n)
{
    return (i + n) % n;
}

如果i小于-n,您仍然可以使用此方法。在这种情况下,您可能确切地知道您的值将在哪个范围内。因此,您可以将x * n添加到i,而不是将n添加到i,其中x是任何给出足够范围的整数。为了增加速度(在没有单周期乘法的处理器上),你可以左移而不是乘法。

答案 9 :(得分:0)

你在Eric Bainville的回答中说,大部分时间0 <= i < n和你有

if (i>=0 && i<n) return i;

作为euc()的第一行。

无论如何,当您进行比较时,您也可以使用它们:

int euc(int i, int n)
{
    if (n <= i)            return i % n;
    else if (i < 0)        return ((i + 1) % n) + n - 1;
    else /* 0 <= i < n */  return i;  // fastest possible response for common case
}

答案 10 :(得分:0)

如果您可以保证阵列的尺寸总是2的幂,那么您可以这样做:

r = (i & (n - 1));

如果您可以进一步保证您的尺寸来自给定的子集,您可以执行以下操作:

template<int n>
int euc(int i) {
    return (i & (n - 1));
}

int euc(int i, int n) {
    switch (n) {
        case 2: return euc<2>(i);
        case 4: return euc<4>(i);
    }
}

答案 11 :(得分:0)

如果右移不是算术,这里的Christopher's version会回退到Jason's

#include <limits.h>
static inline int euc(int i, int n)
{
    // check for arithmetic shift
    #if (-1 >> 1) == -1
        #define OFFSET ((i % n >> (sizeof(int) * CHAR_BIT - 1)) & n)
    #else
        #define OFFSET ((i % n < 0) * n)
    #endif

    return i % n + OFFSET;
}

后备版本应该较慢,因为它使用imul而不是and