快速加权平均值和方差10个箱

时间:2015-08-05 13:28:56

标签: c++ optimization sse variance

我想加快我的部分代码,但我不认为有更好的方法可以进行以下计算:

float invSum = 1.0f / float(sum);

for (int i = 0; i < numBins; ++i)
{
    histVec[i] *= invSum;
}

for (int i = 0; i < numBins; ++i)
{
    float midPoint = (float)i*binSize + binOffset;
    float f = histVec[i];
    fmean += f * midPoint;
}

for (int i = 0; i < numBins; ++i)
{
    float midPoint = (float)i*binSize + binOffset;
    float f = histVec[i];
    float diff = midPoint - fmean;
    var += f * hwk::sqr(diff);
}
for循环中的

numBins通常为10,但这段代码经常被调用(频率为每秒80帧,每帧至少调用8次)

我尝试使用一些SSE方法,但它只是略微加快了这段代码。我想我可以避免计算两次midPoint,但我不知道如何。有没有更好的方法来计算fmean和var?

这是SSE代码:

// make hist contain a multiple of 4 valid values
    for (int i = numBins; i < ((numBins + 3) & ~3); i++) 
        hist[i] = 0;

// find sum of bins in inHist
    __m128i iSum4 = _mm_set1_epi32(0);
    for (int i = 0; i < numBins; i += 4) 
    {
        __m128i a = *((__m128i *) &inHist[i]);
        iSum4 = _mm_add_epi32(iSum4, a);
    }

    int iSum = iSum4.m128i_i32[0] + iSum4.m128i_i32[1] + iSum4.m128i_i32[2] + iSum4.m128i_i32[3];

    //float stdevB, meanB;

    if (iSum == 0.0f)
    {
        stdev = 0.0;
        mean = 0.0;
    }
    else
    {   
        // Set histVec to normalised values in inHist
        __m128 invSum = _mm_set1_ps(1.0f / float(iSum));
        for (int i = 0; i < numBins; i += 4) 
        {
            __m128i a = *((__m128i *) &inHist[i]);
            __m128  b = _mm_cvtepi32_ps(a);
            __m128  c = _mm_mul_ps(b, invSum);
            _mm_store_ps(&histVec[i], c);
        }

        float binSize = 256.0f / (float)numBins;
        float halfBinSize = binSize * 0.5f;
        float binOffset = halfBinSize;

        __m128 binSizeMask = _mm_set1_ps(binSize);
        __m128 binOffsetMask = _mm_set1_ps(binOffset);
        __m128 fmean4 = _mm_set1_ps(0.0f);
        for (int i = 0; i < numBins; i += 4)
        {
            __m128i idx4 = _mm_set_epi32(i + 3, i + 2, i + 1, i);
            __m128  idx_m128 = _mm_cvtepi32_ps(idx4);
            __m128  histVec4 = _mm_load_ps(&histVec[i]);
            __m128  midPoint4 = _mm_add_ps(_mm_mul_ps(idx_m128, binSizeMask), binOffsetMask);
            fmean4 = _mm_add_ps(fmean4, _mm_mul_ps(histVec4, midPoint4));
        }
        fmean4 = _mm_hadd_ps(fmean4, fmean4); // 01 23 01 23
        fmean4 = _mm_hadd_ps(fmean4, fmean4); // 0123 0123 0123 0123

        float fmean = fmean4.m128_f32[0]; 

        //fmean4 = _mm_set1_ps(fmean);
        __m128 var4 = _mm_set1_ps(0.0f);
        for (int i = 0; i < numBins; i+=4)
        {
            __m128i idx4 = _mm_set_epi32(i + 3, i + 2, i + 1, i);
            __m128  idx_m128 = _mm_cvtepi32_ps(idx4);
            __m128  histVec4 = _mm_load_ps(&histVec[i]);
            __m128  midPoint4 = _mm_add_ps(_mm_mul_ps(idx_m128, binSizeMask), binOffsetMask);
            __m128  diff4 = _mm_sub_ps(midPoint4, fmean4);
            var4 = _mm_add_ps(var4, _mm_mul_ps(histVec4, _mm_mul_ps(diff4, diff4)));
        }

        var4 = _mm_hadd_ps(var4, var4); // 01 23 01 23
        var4 = _mm_hadd_ps(var4, var4); // 0123 0123 0123 0123
        float var = var4.m128_f32[0]; 

        stdev = sqrt(var);
        mean = fmean;
    }

我可能做错了,因为我没有像我期待的那样有很多改进。 SSE代码中是否存在可能会降低流程速度的内容?

(编者按:此问题的SSE部分最初被问为https://stackoverflow.com/questions/31837817/foor-loop-optimisation-sse-comparison,该副本已作为副本关闭。)

4 个答案:

答案 0 :(得分:5)

我只是意识到你的数据数组是以int数组开头的,因为你的代码中没有声明。我可以在SSE版本中看到你以整数开头,并且稍后只存储它的浮点版本。

保持一切整数将让我们用简单的ivec = _mm_add_epi32(ivec, _mm_set1_epi32(4)); Aki Suihkonen的答案做循环计数器向量有一些转换,应该让它更好地优化。特别是,自动矢量化器应该能够在没有-ffast-math的情况下做得更多。事实上,它确实很好。你可以用内在函数做得更好,尤其是保存一些向量32位乘法并缩短依赖链。

我的旧答案,基于只是尝试优化您编写的代码,假设FP输入

您可以使用the algorithm @Jason linked to将所有3个循环合并为一个循环。但是,这可能不是有利可图的,因为它涉及一个分工。对于少量的垃圾箱,可能只是循环多次。

首先阅读http://agner.org/optimize/处的指南。他的优化装配指南中的一些技巧将加速您的SSE尝试(我为您编辑了这个问题)。

  • 尽可能合并你的循环,这样你就可以在每次加载/存储数据时做更多的工作。

  • 多个累加器,用于隐藏循环携带的依赖链的延迟。 (即使FP添加在最近的Intel CPU上也需要3个周期。)这不适用于像你这样的真正短阵列。

  • 代替每次迭代的int-&gt; float转换,使用float循环计数器以及int循环计数器。 (在每次迭代时添加_mm_set1_ps(4.0f)的向量。)_mm_set...带有变量args是可能的循环中要避免的东西。它需要几条指令(特别是每个arg到setr必须单独计算。)

gcc -O3设法自动向量化第一个循环,而不是其他循环。使用-O3 -ffast-math,它会自动向量化更多。 -ffast-math允许它以与代码指定的顺序不同的顺序执行FP操作。例如将数组加到向量的4个元素中,最后只合并4个累加器。

告诉gcc输入指针对齐16允许gcc自动向量化,开销少很多(对于未对齐的部分没有标量循环)。

// return mean
float fpstats(float histVec[], float sum, float binSize, float binOffset, long numBins, float *variance_p)
{
    numBins += 3;
    numBins &= ~3;  // round up to multiple of 4.  This is just a quick hack to make the code fast and simple.
    histVec = (float*)__builtin_assume_aligned(histVec, 16);

    float invSum = 1.0f / float(sum);
    float var = 0, fmean = 0;

    for (int i = 0; i < numBins; ++i)
    {
        histVec[i] *= invSum;
        float midPoint = (float)i*binSize + binOffset;
        float f = histVec[i];
        fmean += f * midPoint;
    }

    for (int i = 0; i < numBins; ++i)
    {
        float midPoint = (float)i*binSize + binOffset;
        float f = histVec[i];
        float diff = midPoint - fmean;
//        var += f * hwk::sqr(diff);
        var += f * (diff * diff);
    }
    *variance_p = var;
    return fmean;
}

gcc为第二个循环生成一些奇怪的代码。

        # broadcasting fmean after the 1st loop
        subss   %xmm0, %xmm2    # fmean, D.2466
        shufps  $0, %xmm2, %xmm2        # vect_cst_.16
.L5: ## top of 2nd loop 
        movdqa  %xmm3, %xmm5    # vect_vec_iv_.8, vect_vec_iv_.8
        cvtdq2ps        %xmm3, %xmm3    # vect_vec_iv_.8, vect__32.9
        movq    %rcx, %rsi      # D.2465, D.2467
        addq    $1, %rcx        #, D.2465
        mulps   %xmm1, %xmm3    # vect_cst_.11, vect__33.10
        salq    $4, %rsi        #, D.2467
        paddd   %xmm7, %xmm5    # vect_cst_.7, vect_vec_iv_.8
        addps   %xmm2, %xmm3    # vect_cst_.16, vect_diff_39.15
        mulps   %xmm3, %xmm3    # vect_diff_39.15, vect_powmult_53.17
        mulps   (%rdi,%rsi), %xmm3      # MEM[base: histVec_10, index: _107, offset: 0B], vect__41.18
        addps   %xmm3, %xmm4    # vect__41.18, vect_var_42.19
        cmpq    %rcx, %rax      # D.2465, bnd.26
        ja      .L8     #,   ### <--- This is insane.
        haddps  %xmm4, %xmm4    # vect_var_42.19, tmp160
        haddps  %xmm4, %xmm4    # tmp160, vect_var_42.21
.L2:
        movss   %xmm4, (%rdx)   # var, *variance_p_44(D)
        ret
        .p2align 4,,10
        .p2align 3
.L8:
        movdqa  %xmm5, %xmm3    # vect_vec_iv_.8, vect_vec_iv_.8
        jmp     .L5     #

因此,gcc不是每次迭代都跳回到顶部,而是决定跳转到复制寄存器,然后无条件地jmp回到循环的顶部。 uop循环缓冲区可以消除这种愚蠢的前端开销,但gcc应该已经构造了循环,因此它不会在每次迭代时复制xmm5-&gt; xmm3然后xmm3-&gt; xmm5,因为这很愚蠢。应该让条件跳转到循环的顶部。

另请注意gcc用于获取循环计数器的浮动版本的技术:以1 2 3 4的整数向量开始,并添加set1_epi32(4)。将其用作打包int-&gt; float cvtdq2ps的输入。在Intel HW上,该指令在FP-add端口上运行,并且具有3个周期延迟,与打包FP add相同。 gcc prob。最好只添加set1_ps(4.0)的向量,即使这会创建一个3循环循环的依赖链,而不是1循环向量int add,每次迭代都会有3个周期的转换分支。 / p>

小迭代次数

你说这通常会用在10个箱子上吗?只需10个分档的专用版本可以通过避免所有循环开销并将所有内容保存在寄存器中来实现大幅加速。

如果问题的大小很小,您可以将FP权重放在内存中,而不是每次都使用整数 - >浮点转换重新计算它们。

此外,相对于垂直操作量,10个分区意味着很多水平操作,因为你只有2个半矢量的数据。

如果10个确实很常见,请专门为其设计一个版本。如果16岁以下是常见的,请专门针对该版本。 (他们可以而且应该共享const float weights[] = { 0.0f, 1.0f, 2.0f, ...};数组。)

您可能希望将内在函数用于专门的小问题版本,而不是自动矢量化。

在您的数组中有用数据结束后进行零填充可能仍然是您的专业版本中的一个好主意。但是,您可以使用movq指令加载最后2个浮点数并清除向量寄存器的高64位。 (__m128i _mm_cvtsi64_si128 (__int64 a))。把它投到__m128,你很高兴。

答案 1 :(得分:3)

正如peterchen所提到的,这些操作对于当前的桌面处理器来说非常简单。该函数是线性的,即O(n)。 numBins的典型大小是多少?如果它相当大(例如,超过1000000),并行化将有所帮助。使用像OpenMP这样的库可能很简单。如果numBins开始接近MAXINT,您可以将GPGPU视为一个选项(CUDA / OpenCL)。

考虑到这一点,您应该尝试分析您的应用程序。很可能,如果存在性能限制,则不在此方法中。 Michael Abrash对高性能代码的定义&#34;在确定是否/何时进行优化方面对我有很大帮助:

  

在我们创建高性能代码之前,我们必须了解高性能是什么。创建高性能软件的目标(并非总能实现)是使软件能够如此快速地执行其指定任务,以便就用户而言立即响应。换句话说,理想情况下,高性能代码应该运行得如此之快,以至于代码中的任何进一步改进都是毫无意义的。请注意,上述定义最重要的是没有说明尽可能快地制作软件。

参考: The Graphics Programming Black Book

答案 2 :(得分:2)

要计算的整体功能是

std = sqrt(SUM_i { hist[i]/sum * (midpoint_i - mean_midpoint)^2 })

使用身份

Var (aX + b) = Var (X) * a^2

可以大大降低整体操作的复杂性

1)箱子的中点不需要偏移b
2)无需通过bin宽度

的bin数组元素进行预分频

3)无需使用和的倒数

来标准化直方图条目

优化计算如下

float calcVariance(int histBin[], float binWidth)
{
     int i;
     int sum = 0;
     int mid = 0;
     int var = 0;
     for (i = 0; i < 10; i++)
     {
          sum += histBin[i];
          mid += i*histBin[i];
     }

     float inv_sum = 1.0f / (float)sum;
     float mid_sum = mid * inv_sum;

     for (i = 0; i < 10; i++)
     {
         int diff = i * sum - mid;  // because mid is prescaled by sum
         var += histBin[i] * diff * diff;
     }

     return sqrt(float(var) / (float)(sum * sum * sum)) * binWidth;
}

如果是float histBin[],则需要进行细微更改;

另外,我将histBin大小填充到4的倍数,以便更好地进行矢量化。

修改

使用内循环中的浮点数计算此方法的另一种方法:

 float inv_sum = 1.0f / (float)sum;
 float mid_sum = mid * inv_sum;
 float var = 0.0f;

 for (i = 0; i < 10; i++)
 {
      float diff = (float)i - mid_sum;
      var += (float)histBin[i] * diff * diff;
 }
 return sqrt(var * inv_sum) * binWidth;

答案 3 :(得分:1)

仅对全局结果执行缩放,并尽可能长时间保持整数。

使用Σ(X-m)²/N = ΣX²/N - m²在单个循环中对所有计算进行分组。

// Accumulate the histogram
int mean= 0, var= 0;
for (int i = 0; i < numBins; ++i)
{
    mean+= i * histVec[i];
    var+= i * i * histVec[i];
}

// Compute the reduced mean and variance
float fmean= (float(mean) / sum);
float fvar= float(var) / sum - fmean * fmean;

// Rescale
fmean= fmean * binSize + binOffset;
fvar= fvar * binSize * binSize;

所需的整数类型取决于区间中的最大值。循环的SSE优化可以利用_mm_madd_epi16指令。

如果bin的数量很小,则考虑完全展开循环。预先计算表格中的i向量。

幸运的是,数据符合16位,总和为32位,积累是通过类似

的方式完成的。
    static short I[16]= { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0 };
    static short I2[16]= { 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 0, 0, 0, 0, 0, 0 };

    // First group
    __m128i i= _mm_load_si128((__m128i*)&I[0]);
    __m128i i2= _mm_load_si128((__m128i*)&I2[0]);
    __m128i h= _mm_load_si128((__m128i*)&inHist[0]);

    __m128i mean= _mm_madd_epi16(i, h);
    __m128i var= _mm_madd_epi16(i2, h);

    // Second group
    i= _mm_load_si128((__m128i*)&I[8]);
    i2= _mm_load_si128((__m128i*)&I2[8]);
    h= _mm_load_si128((__m128i*)&inHist[8]);

    mean= _mm_add_epi32(mean, _mm_madd_epi16(i, h));
    var= _mm_add_epi32(var, _mm_madd_epi16(i2, h));

注意:未选中