【问题标题】:Fast implementation of covariance of two 8-bit arrays快速实现两个 8 位数组的协方差
【发布时间】:2016-05-14 23:59:08
【问题描述】:

我需要比较大量相似的小尺寸(最大 200x200)图像。 所以我尝试实现 SSIM(结构相似性见https://en.wikipedia.org/wiki/Structural_similarity)算法。 SSIM 需要计算两个 8 位灰度图像的协方差。 一个简单的实现如下所示:

float SigmaXY(const uint8_t * x, const uint8_t * y, size_t size, float averageX, float averageY)
{
    float sum = 0;
    for(size_t i = 0; i < size; ++i)
        sum += (x[i] - averageX) * (y[i] - averageY);
    return sum / size;
}

但它的性能很差。 所以我希望通过使用 SIMD 或 CUDA 来改进它(听说可以做到)。 不幸的是,我没有这样做的经验。 它会是什么样子?我必须去哪里?

【问题讨论】:

  • 您是否允许您的编译器积极优化(例如,-funsafe-math-optimizations-Ofast 在 gcc 上)?如果你不这样做,编译器就不能对代码做很多事情,因为浮点数学不是关联的,它不能向量化。
  • 好点:-O3 -ffast-math -march=haswellclang auto-vectorizes the OP's scalar code a lot like Ermlg's earlier answer。它使用pmovzx 加载和 FMA,并利用 AVX2 来使用 256b 向量。该godbolt链接有一些我正在努力的改进,比如更有效的水平总和。

标签: c++ image-processing optimization sse simd


【解决方案1】:

算法有SIMD实现(我用的是SSE4.1):

#include <smmintrin.h>

template <int shift> inline __m128 SigmaXY(const __m128i & x, const __m128i & y, __m128 & averageX, __m128 & averageY)
{
    __m128 _x = _mm_cvtepi32_ps(_mm_cvtepu8_epi32(_mm_srli_si128(x, shift)));
    __m128 _y = _mm_cvtepi32_ps(_mm_cvtepu8_epi32(_mm_srli_si128(y, shift)));
    return _mm_mul_ps(_mm_sub_ps(_x, averageX), _mm_sub_ps(_y, averageY))
}

float SigmaXY(const uint8_t * x, const uint8_t * y, size_t size, float averageX, float averageY)
{
    float sum = 0;
    size_t i = 0, alignedSize = size/16*16;
    if(size >= 16)
    {
        __m128 sums = _mm_setzero_ps();
        __m128 avgX = _mm_set1_ps(averageX);
        __m128 avgY = _mm_set1_ps(averageY);
        for(; i < alignedSize; i += 16)
        {
            __m128i _x = _mm_loadu_si128((__m128i*)(x + i));
            __m128i _y = _mm_loadu_si128((__m128i*)(y + i));
            sums = _mm_add_ps(sums, SigmaXY<0>(_x, _y, avgX, avgY);
            sums = _mm_add_ps(sums, SigmaXY<4>(_x, _y, avgX, avgY);
            sums = _mm_add_ps(sums, SigmaXY<8>(_x, _y, avgX, avgY);
            sums = _mm_add_ps(sums, SigmaXY<12>(_x, _y, avgX, avgY);
        }
        float _sums[4];
        _mm_storeu_ps(_sums, sums);
        sum = _sums[0] + _sums[1] + _sums[2] + _sums[3];
    }
    for(; i < size; ++i)
        sum += (x[i] - averageX) * (y[i] - averageY);
    return sum / size;
}

希望对你有用。

【讨论】:

  • (我很久以前就开始了这个评论,在你发布另一个答案之前)。使用_mm_cvtepu8_epi32 (PMOVZX) 作为负载可能会更好,而不是移动完整矢量负载的结果,但这很难用内在函数来完成。当输入指针是 16B 对齐时,它应该没有太大区别。 gcc and clang make some fairly nice code out of this。 gcc 甚至会在可用时使用 FMA,将 SigmaXY&lt;shift&gt; 中的 mul_psadd_ps 组合到累加器中。
  • 使用多个累加器可能有助于允许更多独立工作的重叠:因为与计算独立 SigmaXY&lt;shift&gt; 结果的吞吐量相比,循环对 sums 的依赖并没有那么短。此外,水平和可以更有效地完成:stackoverflow.com/questions/6996764/…
  • @Peter 没关系,因为有一个不使用浮点数的最佳解决方案。
  • @ErmIg:同意。在您发布整数版本之前,我已经输入了大部分内容,但由于最佳水平添加而被侧向跟踪。我最终还是决定将它作为 cmets 发布,而不是仅仅放弃,因为任何从这个答案中学到任何东西的人都可能从看到这些 cmets 中受益。
【解决方案2】:

我还有另一个不错的解决方案!

首先我想提一些数学公式:

averageX = Sum(x[i])/size;
averageY = Sum(y[i])/size;

因此:

Sum((x[i] - averageX)*(y[i] - averageY))/size = 

Sum(x[i]*y[i])/size - Sum(x[i]*averageY)/size - 
Sum(averageX*y[i])/size + Sum(averageX*averageY)/size = 

Sum(x[i]*y[i])/size - averageY*Sum(x[i])/size - 
averageX*Sum(y[i])/size + averageX*averageY*Sum(1)/size =   

Sum(x[i]*y[i])/size - averageY*averageX - 
averageX*averageY + averageX*averageY = 

Sum(x[i]*y[i])/size - averageY*averageX;     

它允许修改我们的算法:

float SigmaXY(const uint8_t * x, const uint8_t * y, size_t size, float averageX, float averageY)
{
    uint32_t sum = 0; // If images will have size greater then 256x256 than you have to use uint64_t.
    for(size_t i = 0; i < size; ++i)
        sum += x[i]*y[i];
    return sum / size - averageY*averageX;
} 

只有在那之后我们才能使用 SIMD(我使用了 SSE2):

#include <emmintrin.h>

inline __m128i SigmaXY(__m128i x, __m128i y)
{
    __m128i lo = _mm_madd_epi16(_mm_unpacklo_epi8(x, _mm_setzero_si128()), _mm_unpacklo_epi8(y, _mm_setzero_si128()));
    __m128i hi = _mm_madd_epi16(_mm_unpackhi_epi8(y, _mm_setzero_si128()), _mm_unpackhi_epi8(y, _mm_setzero_si128()));
    return _mm_add_epi32(lo, hi);
}

float SigmaXY(const uint8_t * x, const uint8_t * y, size_t size, float averageX, float averageY)
{
    uint32_t sum = 0;
    size_t i = 0, alignedSize = size/16*16;
    if(size >= 16)
    {
        __m128i sums = _mm_setzero_si128();
        for(; i < alignedSize; i += 16)
        {
            __m128i _x = _mm_loadu_si128((__m128i*)(x + i));
            __m128i _y = _mm_loadu_si128((__m128i*)(y + i));
            sums = _mm_add_epi32(sums, SigmaXY(_x, _y));
        }
        uint32_t _sums[4];
        _mm_storeu_si128(_sums, sums);
        sum = _sums[0] + _sums[1] + _sums[2] + _sums[3];
    } 
    for(; i < size; ++i)
        sum += x[i]*y[i];
    return sum / size - averageY*averageX;
}

【讨论】:

  • 哇,在循环结束之前您不需要平均值,这很时髦。这意味着您也可以即时计算平均值。使用psadbw,或者使用_mm_add_epi16,同时为madd 解包。
  • 是的,当然,可以在同一个循环中计算协方差和一阶和二阶矩。
猜你喜欢
  • 1970-01-01
  • 2011-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-23
  • 1970-01-01
  • 2018-09-29
  • 1970-01-01
相关资源
最近更新 更多