我有以下代码,根据一个非常大的系列计算总和。
系列char *a
是一个char数组,只包含数字(0..9)。
我想问一下是否有可能让代码更快。它目前是分布式计算应用程序的瓶颈。
小型复制代码。不是实际的代码,而是更简化。
int top = 999999999;
char *a;
a = (char*) calloc(top+1, sizeof(char));
// ... fill a with initial values ...
for (int i=0; i<10; ++i) {
unsigned long long int sum = 0;
for (m = 1, k = top; m < k; ++m, --k) {
// Here is the bottle neck!!
sum += a[m]*a[k];
}
printf("%d\n", sum);
// ... Add something at the end of a, and increase top ...
}
我已尝试过以下操作:
使用-O3
(gcc编译器)优化代码。编译器行现在是:
gcc -c -Wall -fopenmp -Wno-unused-function -O3 -std=c99 -g0 -march=native -pipe -D_FILE_OFFSET_BITS=64 -m64 -fwhole-program -fprefetch-loop-arrays -funsafe-loop-optimizations -Wunsafe-loop-optimizations -fselective-scheduling -fselective-scheduling2 -fsel-sched-pipelining -fsel-sched-pipelining-outer-loops -fgcse-sm -fgcse-lm -fgcse-las -fmodulo-sched -fgcse-after-reload -fsee -DLIBDIVIDE_USE_SSE2 -DLIBDIVIDE_USE_SSE4_1 xxx.c -o xxx.o
使用GNU openMP将for循环拆分为多个核心
unsigned long long int halfway = (top>>1) + 1; // = top/2 + 1
// digits is defined as top+1
#pragma omp parallel // firstprivate/*shared*/(a, digits, halfway)
for (unsigned long long int m = 1; m < halfway; ++m) {
sum += a[m] * a[digits-m];
}
结果:更快,更快,但需要更多核心,我仍然希望加快速度。
在乘法之前将a[m]
投射到unsigned long long int
sum += (unsigned long long int)a[m] * a[k];
结果:性能提升很小。
使用乘法查找表,因为数组查找比实际乘法更快。
sum += multiply_lookup[a[m]][a[k]]; // a[m]*a[k];
结果:性能提升很小。
我试图找到一种减少操作的数学解决方案,但似乎没有什么可以通过数学方法进行优化。
我有以下优化建议:
I have read浮点数的乘法(asm fmul
)比整数的乘法(asm mul
)快得多。只是将int
更改为float
并没有帮助 - 但我认为如果使用MMX或SSE指令集完成工作,或者如果工作由FPU完成,代码可能会变得更加高效。虽然我有一些汇编知识,但我对这些主题一无所知。
但是,如果您有其他想法如何优化它,我很高兴听到它们。
更新一些其他信息:
top
会增加。top
达到数组限制时,a
将使用realloc()
增加100000字节。其他题外话题:您是否知道此总和的数学名称,该系列的元素对从外部乘以内部?
答案 0 :(得分:7)
您可以使用鲜为人知的PMADDUBSW(Multiply和Add Packed Signed和Unsigned Bytes)。签名/未签名的业务在这里无关紧要,无论如何,一切都在[0 .. 9]的区间内。添加是饱和的,但这并不重要,因为9 * 9只有81.内在函数是_mm_maddubs_epi16
。因为k
索引关闭了,所以你必须对它进行字节反转,这可以用PSHUFB(_mm_shuffle_epi8
)来完成。当索引在中间“相遇”时会发生令人讨厌的事情,你可以逐一完成这一部分。
这是一次尝试,只有轻微测试:
__m128i sum = _mm_setzero_si128();
int m, k;
for (m = 1, k = top - 15; m + 15 < k; m += 16, k -= 16) {
__m128i am = _mm_loadu_si128((__m128i*)(a + m));
__m128i ak = _mm_loadu_si128((__m128i*)(a + k));
ak = _mm_shuffle_epi8(ak, _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ,15));
sum = _mm_add_epi16(sum, _mm_maddubs_epi16(am, ak));
}
// could use phaddw, but I do this the long way to avoid overflow slightly longer
sum = _mm_add_epi32(_mm_unpacklo_epi16(sum, _mm_setzero_si128()),
_mm_unpackhi_epi16(sum, _mm_setzero_si128()));
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
int s = _mm_cvtsi128_si32(sum);
// this is for the "tail"
k += 15;
for (; m < k; ++m, --k)
s += a[m] * a[k];
我也忽略了溢出。您可以为(2 16 -1)/(2 * 81)= 404迭代执行此操作,但仍然绝对没有溢出。如果您需要更多,请定期将其添加到32位结果。
在快速基准测试中,这个速度大约是简单方法的7倍(在4770K上使用2KB随机数据进行测试,在每次运行中获得最佳运行效果)。
使用其他答案建议的指针可以进一步提高它的速度,大约是简单方法的9倍。有了索引,有一些奇怪的符号扩展正在进行。
int foobar(char* a, int top)
{
__m128i sum = _mm_setzero_si128();
char *m, *k;
for (m = a + 1, k = a + top - 15; m + 15 < k; m += 16, k -= 16) {
__m128i am = _mm_loadu_si128((__m128i*)(m));
__m128i ak = _mm_loadu_si128((__m128i*)(k));
ak = _mm_shuffle_epi8(ak, _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15));
sum = _mm_add_epi16(sum, _mm_maddubs_epi16(am, ak));
}
sum = _mm_add_epi32(_mm_unpacklo_epi16(sum, _mm_setzero_si128()),
_mm_unpackhi_epi16(sum, _mm_setzero_si128()));
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
int s = _mm_cvtsi128_si32(sum);
k += 15;
for (; m < k; ++m, --k)
s += *m * *k;
return s;
}
分成几部分,尽管有额外的逻辑,仍然是原版的9倍:
int foobar(char* a, int top)
{
int s = 0;
char *m, *k;
for (m = a + 1, k = a + top - 15; m + 15 < k;) {
__m128i sum = _mm_setzero_si128();
for (int i = 0; i < 404 && m + 15 < k; m += 16, k -= 16, ++i) {
__m128i am = _mm_loadu_si128((__m128i*)(m));
__m128i ak = _mm_loadu_si128((__m128i*)(k));
ak = _mm_shuffle_epi8(ak, _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ,15));
sum = _mm_add_epi16(sum, _mm_maddubs_epi16(am, ak));
}
sum = _mm_add_epi32(_mm_unpacklo_epi16(sum, _mm_setzero_si128()),
_mm_unpackhi_epi16(sum, _mm_setzero_si128()));
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
s += _mm_cvtsi128_si32(sum);
}
k += 15;
for (; m < k; ++m, --k)
s += *m * *k;
return s;
}
答案 1 :(得分:3)
这个循环,
for (m = 1, k = top; m < k; ++m, --k) {
// Here is the bottle neck!!
sum += a[m]*a[k];
}
可以从改为:
获益char *b = a + top;
a++;
for (; a < b; )
{
sum += ( *a++ ) * ( *b--);
}
通过删除[],您可以为每次访问数组保存很多算术运算。这使理论上的地址计算数减半:
4与++m --k
和a[m] a[k]
相对应
2 *a++ *b--
简单的指针增量更便宜,而且总体来说通常更快,因为数组访问并不总是使用[]完美优化。
希望这有帮助
答案 2 :(得分:2)
您要执行的操作称为discrete convolution,它会在乘以大数字时出现。您使用的天真算法的复杂度为O( n 2 ),但是O( n log n )使用discrete Fourier transform获得解决方案。
两个序列的离散卷积 c = a * b a = a 0 , a 1 ,..., a n - 1 和 b = b 0 , b 1 ,... , b n - 1 与 n 元素各自是2
c i =Σ max(0, i - n + 1)≤ j &lt; min( n , i + 1) a j < em> b i - j
如果我们假设 a i = b i = 0 i ∉{0,..., n - 1},然后我们可以简化这一点并求和所有整数 i :
c i =Σ j a j b i - j
请注意这是您要执行的操作: a = b = a[]
以及循环的i
次迭代,sum
只是 c i 。
离散卷积经过充分研究,并出现在与信号处理,组合和统计相关的许多数学问题中。幸运的是,它可以在超线性时间O( n log n )中计算,而不是天真的O( n 2 )。
我们可以将 b 扩展为 b N ,以便
b k = b k mod ñ,或
b N = b 0 , b < sub> 1 ,..., b n - 1 , b 0 ,..., b n - 1 , b 0 ,... < /子>
这称为 b b N 的循环扩展。离散卷积 a * b N 称为离散循环卷积 em> a 和 b 。
请注意, a 和 b 的非循环离散卷积可以通过在 a 中附加足够多的零来计算循环卷积。和 b 在进行卷积之前, b 的循环扩展不会改变结果。有关详细信息,请参阅this article。
离散傅里叶变换(DFT)将一系列样本 a 转换为采样信号的复频谱ℱ( a )。傅立叶变换是可逆的,并且可以使用各种fast Fourier transform(FFT)算法在超线性时间O( n log n )中进行计算。该变换在信号处理中具有许多应用。它具有有用的属性
a * b N =ℱ -1 (ℱ( a )×ℱ( b ))
也就是说,两个序列 a 和 b 的循环卷积是傅立叶变换下的逐元素乘法。由于逐元素乘法可以在线性时间内完成,因此可以在总时间O( n log n )中计算所需的结果。
FFTW项目提供高度优化的快速傅里叶变换实现。
傅立叶变换处理复数,并且要求浮点单元快速。结果可能有点不精确。据我所知,精确的方法可能有一些模数魔法(这是Schönhage-Strassen算法中用于大整数乘法的方法)但我不确定细节。