double diff, dsq = 0;
double *descr1, *descr2;
int i, d;
for (i = 0; i < d; ++i)
{
diff = descr1[i] - descr2[i];
dsq += diff * diff;
}
return dsq;
我想优化这部分代码,这需要花费大部分时间在我的程序中。 如果这个双倍乘法以优化的方式执行,我的程序可以非常快地运行。 是否有其他乘法方法而不是使用*运算符导致程序运行得更快? 非常感谢。
答案 0 :(得分:3)
这绝对属于Duff's Device。
这是我的实施,基于Duff的设备 (注意:只有经过轻微测试...... 必须在调试器中逐步完成以确保正确行为)
void fnc(void)
{
double dsq = 0.0;
double diff[8] = {0.0};
double descr1[115];
double descr2[115];
double* pD1 = descr1;
double* pD2 = descr2;
int d = 115;
//Fill with random data for testing
for(int i=0; i<d; ++i)
{
descr1[i] = (double)rand() / (double)rand();
descr2[i] = (double)rand() / (double)rand();
}
// Duff's Device: Step through this in a debugger, its AMAZING.
int c = (d + 7) / 8;
switch(d % 8) {
case 0: do { diff[0] = *pD1++ - *pD2++; diff[0] *= diff[0];
case 7: diff[7] = *pD1++ - *pD2++; diff[7] *= diff[7];
case 6: diff[6] = *pD1++ - *pD2++; diff[6] *= diff[6];
case 5: diff[5] = *pD1++ - *pD2++; diff[5] *= diff[5];
case 4: diff[4] = *pD1++ - *pD2++; diff[4] *= diff[4];
case 3: diff[3] = *pD1++ - *pD2++; diff[3] *= diff[3];
case 2: diff[2] = *pD1++ - *pD2++; diff[2] *= diff[2];
case 1: diff[1] = *pD1++ - *pD2++; diff[1] *= diff[1];
dsq += diff[0] + diff[1] + diff[2] + diff[3] + diff[4] + diff[5] + diff[6] + diff[7];
} while(--c > 0);
}
}
<小时/> 的说明强>
i
的值。
执行步骤大致如下:
Is i < d? ==> Yes
Do some math.
Is i < d? ==> Yes
Do some math.
Is i < d? ==> Yes
Do some math.
Is i < d? ==> Yes
Do some math.
您可以看到每个其他步骤都在检查i
。
使用Duff的设备,在检查计数器之前,您将获得八个操作(在这种情况下为c
)。
现在执行步骤大致如下:
Is c > 0? ==> Yes
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Is c > 0? ==> Yes
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Do some math.
Is c > 0? ==> Yes
[...]
换句话说,你花了大约8倍的CPU实际完成工作,而且检查你的柜台价值的时间要少得多。这是 BIG 胜利。
我怀疑你甚至可以将循环进一步展开到16或32次操作,以获得更大的胜利。这实际上取决于代码中d
的可能值。
请测试并分析此代码,并告诉我它是如何工作的 我强烈认为这将是一个很大的改进。
答案 1 :(得分:2)
您可以使用严格的别名规则帮助编译器:
double calc_ssq(double *restrict descr1, double *restrict descr2, size_t count)
{
double ssq;
ssq = 0.0;
for ( ;count; count--) {
double diff;
diff = *descr1++ - *descr2++;
ssq += diff * diff;
}
return ssq;
}
答案 2 :(得分:1)
如果你真的不需要计算的双精度,你可以尝试将它们转换为单精度,然后乘以。
我想,在32位处理器的情况下,单精度乘法将比双精度乘法更快,因为常规float
只需要一个处理器寄存器而double
需要两个。
我不确定施法会不会“吃掉”所有的速度提升,你将从单精度乘法中获得。
答案 3 :(得分:1)
如果d被2整除,我会尝试这样的事情:
for(i=0;i<d;i+=2)
{
diff0 = descr1[i] - descr2[i];
diff1 = descr1[i+1] - descr2[i+1];
dsq += diff0 * diff0 + diff1 * diff1;
}
这会向优化器提示可以交错六个操作。即使d是奇数,你也可以在每个向量的末尾附加一个0.0值(给出偶数个值),因为它对于给定的操作没有任何影响。
下一步可能是将向量附加为4可被整除,在迭代i + = 4之前进行4次减法,4次乘法和4次加法;
甚至可被8整除,允许向量完全符合64的缓存行大小。
浮点乘法只需要一个或两个时钟周期就可以完成加法和减法(根据Agner Fog)。因此,对于您的示例,减少迭代开销应该加快速度。
答案 4 :(得分:1)
总而言之,你有一个非常紧凑的循环来访问 lot 的数据。循环展开可能有助于隐藏延迟,但在现代硬件上,像这样的循环受内存带宽的限制,而不是计算能力。
因此,您拥有的唯一真正的优化希望是:a)使用float
数组而不是double
数组来减少从内存加载的数据量,并且b)避免尽可能多地调用此代码。
以下是一些数字:
你的内循环中有三个双重算术指令,大约是6个循环。这些需要16个字节的数据。在3 GHz处理器上,即8 GB / s的内存带宽。 DDR3-1066模块可提供8.5 GB / s的速率。所以,即使你使用SSE和东西,你也不会快得多,除非你转而使用float
。
答案 5 :(得分:1)
假设您使用的是现代Intel / AMD处理器(带有AVX),并且您希望保留相同的算法,则可以尝试以下代码。它使用AVX和OpenMP进行并行化。使用GCC foo.c -mavx -fopenmp -O3
进行编译。如果您不想使用OpenMP,只需注释掉两个#pragma
语句。
速度取决于数组大小和缓存大小。对于适合L1缓存的阵列,您可以预期大约6倍的加速(由于其开销,您应该禁用OpenMP)。升级将随着每个缓存级别而不断下降。当它到达系统内存时它仍然会得到提升(在我的两个核心常春藤网桥系统上运行超过10M双打(2 * 80MB)仍然超过70%)。
#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
#include <omp.h>
double foo_avx_omp(const double *descr1, const double *descr2, const int d) {
double diff, dsq = 0;
int i;
int range;
__m256d diff4_v1, diff4_v2, dsq4_v1, dsq4_v2, t1, l1, l2, l3, l4;
__m128d t2, t3;
range = (d-1) & -8; //round down to multiple of 8
#pragma omp parallel private(i,l1,l2,l3,l4,t1,t2,t3,dsq4_v1,dsq4_v2,diff4_v1,diff4_v2) \
reduction(+:dsq)
{
dsq4_v1 = _mm256_set1_pd(0.0);
dsq4_v2 = _mm256_set1_pd(0.0); //two sums to unroll the loop once
#pragma omp for
for(i=0; i<(range/8); i++) {
//load one cache line of descr1
l1 = _mm256_load_pd(&descr1[8*i]);
l3 = _mm256_load_pd(&descr1[8*i+4]);
//load one cache line of descr2
l2 = _mm256_load_pd(&descr2[8*i]);
l4 = _mm256_load_pd(&descr2[8*i+4]);
diff4_v1 = _mm256_sub_pd(l1, l2);
diff4_v2 = _mm256_sub_pd(l3, l4);
dsq4_v1 = _mm256_add_pd(dsq4_v1, _mm256_mul_pd(diff4_v1, diff4_v1));
dsq4_v2 = _mm256_add_pd(dsq4_v2, _mm256_mul_pd(diff4_v2, diff4_v2));
}
dsq4_v1 = _mm256_add_pd(dsq4_v1, dsq4_v2);
t1 = _mm256_hadd_pd(dsq4_v1,dsq4_v1);
t2 = _mm256_extractf128_pd(t1,1);
t3 = _mm_add_sd(_mm256_castpd256_pd128(t1),t2);
dsq += _mm_cvtsd_f64(t3);
}
//finish remaining elements if d was not a multiple of 8
for (i=range; i < d; ++i) {
diff = descr1[i] - descr2[i];
dsq += diff * diff;
}
return dsq;
}
double foo(double *descr1, double *descr2, int d) {
double diff, dsq = 0;
int i;
for (i = 0; i < d; ++i)
{
diff = descr1[i] - descr2[i];
dsq += diff * diff;
}
return dsq;
}
int main(void)
{
double result1, result2, result3, dtime;
double *descr1, *descr2;
const int n = 2000000;
int i;
int repeat = 1000;
descr1 = _mm_malloc(sizeof(double)*n, 64); //align to a cache line
descr2 = _mm_malloc(sizeof(double)*n, 64); //align to a cache line
for(i=0; i<n; i++) {
descr1[i] = 1.0*rand()/RAND_MAX;
descr2[i] = 1.0*rand()/RAND_MAX;
}
dtime = omp_get_wtime();
for(i=0; i<repeat; i++) {
result1 = foo(descr1, descr2, n);
}
dtime = omp_get_wtime() - dtime;
printf("foo %f, time %f\n", result1, dtime);
dtime = omp_get_wtime();
for(i=0; i<repeat; i++) {
result1 = foo_avx_omp(descr1, descr2, n);
}
dtime = omp_get_wtime() - dtime;
printf("foo_avx_omp %f, time %f\n", result1, dtime);
return 0;
}
答案 6 :(得分:1)
看起来你正在计算两个向量的均方误差。
使用BLAS,您将能够利用手动优化的代码,这些代码远比我们任何人编写的代码都高效。
答案 7 :(得分:0)
将descr1
和descr2
的两个双精度放入结构中,使它们在内存中彼此相邻。这样可以更好地使用缓存和访问内存。
同时使用register
diff
和dsq
答案 8 :(得分:0)
警告:以下未经测试的代码。
如果你的硬件和编译器都支持它们,你可能希望使用向量来平行一些操作。我在GCC 4.6.x编译器(x86-64 Ubuntu机器)上使用了类似以下的东西。如果使用不同的编译器/体系结构,某些语法可能略有不同或可能会有所不同。但是,我希望能够足够接近你的目标。
typedef double v2d_t __attribute__((vector_size (16)));
typedef union {
v2d_t vector;
double d[2];
} v2d_u;
v2d_u vdsq = (v2d_t) {0.0, 0.0}; /* sum of square of differences */
v2d_u vdiff; /* difference */
v2d_t * vdescr1; /* pointer to array of aligned vector of doubles */
v2d_t * vdescr2; /* pointer to array of aligned vector of doubles */
int i; /* index into array of aligned vector of doubles */
int d; /* # of elements in array */
/*
* ...
* Assuming that <d> is getting initialized appropriately somewhere
* ...
*/
for (i = 0; i < d; i++) {
vdiff.vector = vdescr1[i] - vdescr2[i];
vdsq.vector += vdiff.vector * vdiff.vector;
}
return vdsq.d[0] + vdsq.d[1];
以上内容可能会进一步调整以获得更好的性能。也许有些循环展开。或者,如果您可以利用256位向量(例如某些x86处理器上的YMMx)而不是此示例使用的128位向量,那么也可能加快速度(需要对代码进行一些调整)。
希望这有帮助。