最快50%缩放(A)RGB32图像使用sse intrinsic

Fastest 50% scaling of (A)RGB32 images using sse intrinsics

本文关键字:图像 sse intrinsic RGB32 缩放 最快      更新时间:2023-10-16

我想在c++中尽可能快地缩小图像。本文描述了如何有效地将32位rgb图像平均降低50%。它又快又好看。

我已经尝试使用sse intrinsic修改该方法。无论是否启用SSE,下面的代码都可以工作。然而,令人惊讶的是,加速几乎可以忽略不计。

有人能想到改进SSE代码的方法吗?创建变量shuffle1和shuffle2的两行似乎是两个候选(使用一些巧妙的移动或类似的方法)。

/*
 * Calculates the average of two rgb32 pixels.
 */
inline static uint32_t avg(uint32_t a, uint32_t b)
{
    return (((a^b) & 0xfefefefeUL) >> 1) + (a&b);
}
/*
 * Calculates the average of four rgb32 pixels.
 */
inline static uint32_t avg(const uint32_t a[2], const uint32_t b[2])
{
    return avg(avg(a[0], a[1]), avg(b[0], b[1]));
}
/*
 * Calculates the average of two rows of rgb32 pixels.
 */
void average2Rows(const uint32_t* src_row1, const uint32_t* src_row2, uint32_t* dst_row, int w)
{
#if !defined(__SSE)
        for (int x = w; x; --x, dst_row++, src_row1 += 2, src_row2 += 2)
            * dst_row = avg(src_row1, src_row2);
#else
        for (int x = w; x; x-=4, dst_row+=4, src_row1 += 8, src_row2 += 8)
        {
            __m128i left  = _mm_avg_epu8(_mm_load_si128((__m128i const*)src_row1), _mm_load_si128((__m128i const*)src_row2));
            __m128i right = _mm_avg_epu8(_mm_load_si128((__m128i const*)(src_row1+4)), _mm_load_si128((__m128i const*)(src_row2+4)));
            __m128i shuffle1 = _mm_set_epi32( right.m128i_u32[2], right.m128i_u32[0], left.m128i_u32[2], left.m128i_u32[0]);
            __m128i shuffle2 = _mm_set_epi32( right.m128i_u32[3], right.m128i_u32[1], left.m128i_u32[3], left.m128i_u32[1]);
            _mm_store_si128((__m128i *)dst_row, _mm_avg_epu8(shuffle1, shuffle2));
        }
#endif
}

在通用寄存器和SSE寄存器之间传输数据非常慢,因此您应该避免这样做:

__m128i shuffle1 = _mm_set_epi32( right.m128i_u32[2], right.m128i_u32[0], left.m128i_u32[2], left.m128i_u32[0]);
__m128i shuffle2 = _mm_set_epi32( right.m128i_u32[3], right.m128i_u32[1], left.m128i_u32[3], left.m128i_u32[1]);

在相应的Shuffle操作的帮助下对SSE寄存器中的值进行Shuffle。

这应该是你要找的:

__m128i t0 = _mm_unpacklo_epi32( left, right ); // right.m128i_u32[1] left.m128i_u32[1] right.m128i_u32[0] left.m128i_u32[0]
__m128i t1 = _mm_unpackhi_epi32( left, right ); // right.m128i_u32[3] left.m128i_u32[3] right.m128i_u32[2] left.m128i_u32[2]
__m128i shuffle1 = _mm_unpacklo_epi32( t0, t1 );    // right.m128i_u32[2] right.m128i_u32[0] left.m128i_u32[2] left.m128i_u32[0]
__m128i shuffle2 = _mm_unpackhi_epi32( t0, t1 );    // right.m128i_u32[3] right.m128i_u32[1] left.m128i_u32[3] left.m128i_u32[1]

主要问题是使用_mm_set_epi32来进行洗牌——不像大多数SSE内部函数,这并不直接映射到单个SSE指令——在这种情况下,它会在底层生成大量标量代码,并导致数据在内存、通用寄存器和SSE寄存器之间移动。请考虑使用适当的SSE shuffle特性。

第二个问题是,相对于加载和存储的数量,你所做的计算很少。这往往会导致代码带宽受限,而不是计算受限,即使使用理想的SSE代码,也可能看不到显著的性能改进。考虑在循环中组合更多操作,以便在缓存中对数据进行更多操作。

如果SSE本质没有什么区别,那么代码可能受到内存带宽的限制。

在你的代码中有很多的加载和存储,(_mm_set_epi32是一个加载以及明显的)没有太多的实际工作。

如果加载/存储控制了运行时间,那么再多花哨的指令也救不了你。在高度流水线化和重新排序指令的现代处理器上,它可能在使整个处理器在非SSE版本的代码中保持忙碌方面做得很好。

您可以通过多种方式验证这种情况。最简单的方法可能是将算法的实际吞吐量与内存的加载/存储速度进行比较。您可能还会注意到一些差异,不仅改变了实现,而且改变了输入的大小,当输入超过每一级处理器缓存的大小时,输入的大小会急剧增加。