如何优化这段代码?

时间:2013-09-08 10:35:29

标签: c performance optimization statistics profile

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;

我想优化这部分代码,这需要花费大部分时间在我的程序中。 如果这个双倍乘法以优化的方式执行,我的程序可以非常快地运行。 是否有其他乘法方法而不是使用*运算符导致程序运行得更快? 非常感谢。

9 个答案:

答案 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)

descr1descr2的两个双精度放入结构中,使它们在内存中彼此相邻。这样可以更好地使用缓存和访问内存。

同时使用register diffdsq

答案 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位向量,那么也可能加快速度(需要对代码进行一些调整)。

希望这有帮助。