我感兴趣的是获得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;
}
但是由于我需要执行数百万次(它在多维数组的迭代器中使用),我想尽可能避免分支。要求:
实际上很容易得到错误的结果,所以这里有一个预期结果的例子:
有些人还担心优化它是没有意义的。我需要这个用于多维迭代器,其中超出范围的项被“虚拟数组”中的项目替换,该虚拟数组重复原始数组。所以如果我的数组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亿个欧几里德分区。
答案 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;
反汇编很短:
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
。