在C(SIMD)中快速转换图像和Sobel滤波器优化

时间:2013-08-13 19:13:06

标签: c optimization sse simd

我想为我的一位朋友和我写过的光线追踪器实现真正(非常)快速Sobel operator(可以找到资源here)。以下是我到目前为止所得到的......

首先,假设图像是8位无符号整数数组中的灰度图像存储。

要编写真正的Sobel滤波器,我需要为每个像素计算Gx和Gy。由于原点旁边有6个像素,因此计算出这些数字中的每一个。但SIMD指令允许我处理16或甚至32(AVX)像素。希望运算符的内核具有一些不错的属性,因此我可以通过以下方式计算Gy:

  • 减去每个i和i + 2行,并将结果存储在其他图片(数组)的i + 1行中
  • 添加i,两次i + 1和i + 2列给出最终图片的i + 1列

我会做同样的事情(但转置)来计算Gx然后添加两张图片。

一些注意事项:

  • 我不关心内存分配,因为一切都会在开头分配。
  • 我可以处理将值除以4的溢出和签名问题(感谢_mm_srli_epi8) (uint8_t >> 2 - uint8_t >> 2) = int7_t //really store as int8_t
    int7_t + uint8_t << 1 >> 2 + int7_t = uint8_t
    //some precision is lost but I don't care

我面临的真正问题是从行到列。因为我无法将图片加载到SIMD寄存器中。我必须三次翻转图像至少不是吗?

一旦原始图片。然后我可以计算Gx和Gy的第一步,然后翻转结果图片以计算第二步。

所以,这是我的问题:

  • 这种实现是个好主意吗?
  • 有没有办法比dumb算法更快地转置数组? (我不这么认为)
  • 哪里会出现瓶颈? (任何猜测?:P)

2 个答案:

答案 0 :(得分:8)

我认为transpose / 2-pass不适合优化Sobel Operator代码。 Sobel Operator不是计算功能,因此浪费内存访问以进行转置/ 2遍访问对于这种情况并不好。我写了一些Sobel Operator测试代码,看看SSE能有多快。此代码不处理第一个和最后一个边缘像素,并使用FPU计算sqrt()值。

Sobel运营商需要14个乘法,1个平方根,11个加法,2个/最大值,12个读取访问和1个写入访问运算符。这意味着如果优化代码,则可以在20~30周期内处理组件。

FloatSobel()函数占用2113044个CPU周期来处理256 * 256个图像处理32.76个周期/分量。我将此示例代码转换为SSE。

void FPUSobel()
{
    BYTE* image_0 = g_image + g_image_width * 0;
    BYTE* image_1 = g_image + g_image_width * 1;
    BYTE* image_2 = g_image + g_image_width * 2;
    DWORD* screen = g_screen + g_screen_width*1;

    for(int y=1; y<g_image_height-1; ++y)
    {
        for(int x=1; x<g_image_width-1; ++x)
        {
            float gx =  image_0[x-1] * (+1.0f) + 
                        image_0[x+1] * (-1.0f) +
                        image_1[x-1] * (+2.0f) + 
                        image_1[x+1] * (-2.0f) +
                        image_2[x-1] * (+1.0f) + 
                        image_2[x+1] * (-1.0f);

            float gy =  image_0[x-1] * (+1.0f) + 
                        image_0[x+0] * (+2.0f) + 
                        image_0[x+1] * (+1.0f) +
                        image_2[x-1] * (-1.0f) + 
                        image_2[x+0] * (-2.0f) + 
                        image_2[x+1] * (-1.0f);


            int result = (int)min(255.0f, max(0.0f, sqrtf(gx * gx + gy * gy)));

            screen[x] = 0x01010101 * result;
        }
        image_0 += g_image_width;
        image_1 += g_image_width;
        image_2 += g_image_width;
        screen += g_screen_width;
    }
}

SseSobel()函数占用613220 CPU周期来处理相同的256 * 256图像。它花费了9.51个周期/组件,比FPUSobel()快了3.4倍。有一些空间需要优化,但它不会超过4倍,因为它使用了4路SIMD。

此函数使用SoA方法一次处理4个像素。在大多数阵列或图像数据中,SoA优于AoS,因为您必须转置/随机播放才能使用AoS。并且SoA更容易将常见的C代码更改为SSE代码。

void SseSobel()
{
    BYTE* image_0 = g_image + g_image_width * 0;
    BYTE* image_1 = g_image + g_image_width * 1;
    BYTE* image_2 = g_image + g_image_width * 2;
    DWORD* screen = g_screen + g_screen_width*1;

    __m128 const_p_one = _mm_set1_ps(+1.0f);
    __m128 const_p_two = _mm_set1_ps(+2.0f);
    __m128 const_n_one = _mm_set1_ps(-1.0f);
    __m128 const_n_two = _mm_set1_ps(-2.0f);

    for(int y=1; y<g_image_height-1; ++y)
    {
        for(int x=1; x<g_image_width-1; x+=4)
        {
            // load 16 components. (0~6 will be used)
            __m128i current_0 = _mm_unpacklo_epi8(_mm_loadu_si128((__m128i*)(image_0+x-1)), _mm_setzero_si128());
            __m128i current_1 = _mm_unpacklo_epi8(_mm_loadu_si128((__m128i*)(image_1+x-1)), _mm_setzero_si128());
            __m128i current_2 = _mm_unpacklo_epi8(_mm_loadu_si128((__m128i*)(image_2+x-1)), _mm_setzero_si128());

            // image_00 = { image_0[x-1], image_0[x+0], image_0[x+1], image_0[x+2] }
            __m128 image_00 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(current_0, _mm_setzero_si128()));
            // image_01 = { image_0[x+0], image_0[x+1], image_0[x+2], image_0[x+3] }
            __m128 image_01 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(_mm_srli_si128(current_0, 2), _mm_setzero_si128()));
            // image_02 = { image_0[x+1], image_0[x+2], image_0[x+3], image_0[x+4] }
            __m128 image_02 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(_mm_srli_si128(current_0, 4), _mm_setzero_si128()));
            __m128 image_10 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(current_1, _mm_setzero_si128()));
            __m128 image_12 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(_mm_srli_si128(current_1, 4), _mm_setzero_si128()));
            __m128 image_20 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(current_2, _mm_setzero_si128()));
            __m128 image_21 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(_mm_srli_si128(current_2, 2), _mm_setzero_si128()));
            __m128 image_22 = _mm_cvtepi32_ps(_mm_unpacklo_epi16(_mm_srli_si128(current_2, 4), _mm_setzero_si128()));

            __m128 gx = _mm_add_ps( _mm_mul_ps(image_00,const_p_one),
                        _mm_add_ps( _mm_mul_ps(image_02,const_n_one),
                        _mm_add_ps( _mm_mul_ps(image_10,const_p_two),
                        _mm_add_ps( _mm_mul_ps(image_12,const_n_two),
                        _mm_add_ps( _mm_mul_ps(image_20,const_p_one),
                                    _mm_mul_ps(image_22,const_n_one))))));

            __m128 gy = _mm_add_ps( _mm_mul_ps(image_00,const_p_one), 
                        _mm_add_ps( _mm_mul_ps(image_01,const_p_two), 
                        _mm_add_ps( _mm_mul_ps(image_02,const_p_one),
                        _mm_add_ps( _mm_mul_ps(image_20,const_n_one), 
                        _mm_add_ps( _mm_mul_ps(image_21,const_n_two), 
                                    _mm_mul_ps(image_22,const_n_one))))));

            __m128 result = _mm_min_ps( _mm_set1_ps(255.0f), 
                            _mm_max_ps( _mm_set1_ps(0.0f), 
                                        _mm_sqrt_ps(_mm_add_ps(_mm_mul_ps(gx, gx), _mm_mul_ps(gy,gy))) ));

            __m128i pack_32 = _mm_cvtps_epi32(result); //R32,G32,B32,A32
            __m128i pack_16 = _mm_packs_epi32(pack_32, pack_32); //R16,G16,B16,A16,R16,G16,B16,A16
            __m128i pack_8 = _mm_packus_epi16(pack_16, pack_16); //RGBA,RGBA,RGBA,RGBA
            __m128i unpack_2 = _mm_unpacklo_epi8(pack_8, pack_8); //RRGG,BBAA,RRGG,BBAA
            __m128i unpack_4 = _mm_unpacklo_epi8(unpack_2, unpack_2); //RRRR,GGGG,BBBB,AAAA

            _mm_storeu_si128((__m128i*)(screen+x),unpack_4);
        }
        image_0 += g_image_width;
        image_1 += g_image_width;
        image_2 += g_image_width;
        screen += g_screen_width;
    }
}

答案 1 :(得分:2)

对于@zupet答案中的代码:
与其乘以一个(const_p_one),我什么都不做。编译器可能无法对其进行优化。
与其乘以2,我将自己加。比整数整数的mul快。但是对于FP,它基本上只是避免了需要另一个向量常量。 Haswell的FP增加吞吐量比FP mul差,但Skylake和Zen处于平衡状态。

与其乘以-1.0,而与_mm_xor_ps-0.0求反,以翻转符号位。

我将独立并排并行地计算pos和neg项,而不是依次计算(为了更好的流水线),且相同的arithm和sub仅在末尾。等等等等...还有很多改进有待解决

使用AVX + FMA,_mm_fma_ps可以更快。