快速矢量化的RGB到BGRA的转换

6
在之前关于将RGB转换为RGBA和ARGB转换为BGR的一些问题的跟进中,我希望能够通过SSE加速RGB到BGRA的转换。假设使用32位机器,并且想要使用内置函数。我遇到了困难,无法对齐源缓冲区和目标缓冲区以使用128位寄存器,并寻求其他精明的矢量化解决方案。
需要进行矢量化的例程如下...
    void RGB8ToBGRX8(int w, const void *in, void *out)
    {
        int i;
        int width = w;
        const unsigned char *src= (const unsigned char*) in;
        unsigned int *dst= (unsigned int*) out;
        unsigned int invalue, outvalue;

        for (i=0; i<width; i++, src+=3, dst++)
        {
                invalue = src[0];
                outvalue = (invalue<<16);
                invalue = src[1];
                outvalue |= (invalue<<8);
                invalue = src[2];
                outvalue |= (invalue);
                *dst = outvalue | 0xff000000;
        }
      }

这个例程主要用于处理大型纹理(512KB),因此如果我可以并行化某些操作,可能有利于一次处理更多的像素。当然,我需要进行性能分析。 :)

编辑:

我的编译参数...

gcc -O2 main.c

1
你是否在使用编译器的优化标志(哪一个)?编译器通常会更好地优化代码,而不会引入不正确性。你收集了哪些基准数据? - Dana the Sane
不是SSE的答案,但你尝试过将循环展开4次,使得输入始终从对齐的地址开始吗?这样,您可以一次读取一台机器字而不是逐个字节读取输入,并为源像素的每个相对位置使用专用的移位和掩码。正如Dana所提到的那样,值得看看编译器在高优化级别下的表现(除了基准测试之外还要检查生成的汇编代码),但我怀疑它是否足够积极地展开循环并根据'in'的对齐拆分入口点。 - hmakholm left over Monica
好问题。使用GCC4.6,它只是“O2”(而不是O3)。我的基准测试案例是一个10K迭代运行,512作为“宽度”跨度。感谢您的回复! - Rev316
4个回答

11

这是使用SSSE3内部函数执行所请求操作的示例。输入和输出指针必须16字节对齐,并且它一次处理16个像素块。

#include <tmmintrin.h>

/* in and out must be 16-byte aligned */
void rgb_to_bgrx_sse(unsigned w, const void *in, void *out)
{
    const __m128i *in_vec = in;
    __m128i *out_vec = out;

    w /= 16;

    while (w-- > 0) {
        /*             0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15
         * in_vec[0]   Ra Ga Ba Rb Gb Bb Rc Gc Bc Rd Gd Bd Re Ge Be Rf
         * in_vec[1]   Gf Bf Rg Gg Bg Rh Gh Bh Ri Gi Bi Rj Gj Bj Rk Gk
         * in_vec[2]   Bk Rl Gl Bl Rm Gm Bm Rn Gn Bn Ro Go Bo Rp Gp Bp
         */
        __m128i in1, in2, in3;
        __m128i out;

        in1 = in_vec[0];

        out = _mm_shuffle_epi8(in1,
            _mm_set_epi8(0xff, 9, 10, 11, 0xff, 6, 7, 8, 0xff, 3, 4, 5, 0xff, 0, 1, 2));
        out = _mm_or_si128(out,
            _mm_set_epi8(0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0));
        out_vec[0] = out;

        in2 = in_vec[1];

        in1 = _mm_and_si128(in1,
            _mm_set_epi8(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0));
        out = _mm_and_si128(in2,
            _mm_set_epi8(0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff));
        out = _mm_or_si128(out, in1);
        out = _mm_shuffle_epi8(out,
            _mm_set_epi8(0xff, 5, 6, 7, 0xff, 2, 3, 4, 0xff, 15, 0, 1, 0xff, 12, 13, 14));
        out = _mm_or_si128(out,
            _mm_set_epi8(0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0));
        out_vec[1] = out;

        in3 = in_vec[2];
        in_vec += 3;

        in2 = _mm_and_si128(in2,
            _mm_set_epi8(0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0));
        out = _mm_and_si128(in3,
            _mm_set_epi8(0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff));
        out = _mm_or_si128(out, in2);
        out = _mm_shuffle_epi8(out,
            _mm_set_epi8(0xff, 1, 2, 3, 0xff, 14, 15, 0, 0xff, 11, 12, 13, 0xff, 8, 9, 10));
        out = _mm_or_si128(out,
            _mm_set_epi8(0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0));
        out_vec[2] = out;

        out = _mm_shuffle_epi8(in3,
            _mm_set_epi8(0xff, 13, 14, 15, 0xff, 10, 11, 12, 0xff, 7, 8, 9, 0xff, 4, 5, 6));
        out = _mm_or_si128(out,
            _mm_set_epi8(0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0, 0xff, 0, 0, 0));
        out_vec[3] = out;

        out_vec += 4;
    }
}

即使是gcc8.2的-O3优化也不能将OP的版本优化为4字节加载。ICC和clang的-O3会展开循环,但仍然不如字节加载+OR https://godbolt.org/z/Ei9C_d。在Sandybridge系列CPU上,gcc的版本最多每3个时钟周期存储4个字节,或者如果与超线程竞争,则在前端瓶颈处每个时钟周期4个uops以下。那就是垃圾。很难想象这个`pshufb`版本不至少快3倍,而且根据内存带宽的不同,可能更快。 - Peter Cordes
@PeterCordes:是的,你说得对 - 当然可以调整标量代码以获得4字节加载,但它看起来仍然不够快。我不确定我当时比较的内存带宽是多少,毕竟7年时间很长了。palignr优化看起来不错,我可能会尝试一下。 - caf
哦,我忘记了这也是将字节顺序反转为BGRA,而不仅仅是SSE2将打包的RGB转换为RGBA像素(在每3个字节后添加第4个0xFF字节)。使用类似于__builtin_bswap32(in) | 0xFF000000的端序反转函数来获得mov + bswap + OR + mov。(但是这仍然是4个uop,不包括pointers +=3*unroll+=4 * unroll的任何循环开销,因此我们只能通过巨大的展开接近每个时钟1 DWORD存储)或者在Atom/Silvermont上(但不是Haswell),movbe可以节省一个uop。 - Peter Cordes
@PeterCordes:palign的更改实际上最终会导致轻微的悲观化,我不确定具体原因是什么。https://godbolt.org/z/Y3-Dbh - caf
如果你使用的是Haswell/Skylake,那么每个时钟周期可能会有1次洗牌吞吐量。在这里,未对齐的加载应该更好。我没有仔细查看gcc如何编译原始代码中的and/and/or混合操作,但也许它比3个微操作更好。 - Peter Cordes
显示剩余5条评论

3

在我的个人实践中,我发现以下实现方式可以将BGR-24转换为ARGB-32获得最佳结果。

这段代码的执行速度约为8.8毫秒,而上述128位矢量化代码每张图片的执行时间为14.5毫秒。

void PixelFix(u_int32_t *buff,unsigned char *diskmem)
{
    int i,j;
    int picptr, srcptr;
    int w = 1920;
    int h = 1080;

    for (j=0; j<h; j++) {
        for (i=0; i<w; i++) {
            buff[picptr++]=(diskmem[srcptr]<<24) | (diskmem[srcptr+1]<<16) | diskmem[srcptr+2]<<8 | 0xff;
            srcptr+=3;
        }
    }
}

以前我使用了这个程序(每张图片大约需要13.2毫秒)。在这里,buff是一个无符号字符指针。

for (j=0; j<h; j++) {
    int srcptr = (h-j-1)*w*3;  // remove if you don't want vertical flipping
    for (i=0; i<w; i++) {
        buff[picptr+3]=diskmem[srcptr++]; // b
        buff[picptr+2]=diskmem[srcptr++]; // g
        buff[picptr+1]=diskmem[srcptr++]; // r
        buff[picptr+0]=255;               // a
        picptr+=4;
    }
}

我使用的是2012年MacMini 2.6ghz/i7。


除此之外,有人可能希望研究一下苹果最近的vImage转换API...,特别是像“vImageConvert_RGB888toARGB8888”这样的例程,用于将24位RGB转换为32位ARGB(或BGRA)。https://developer.apple.com/library/mac/documentation/Performance/Reference/vImage_conversion/Reference/reference.html#//apple_ref/c/func/vImageConvert_RGB888toARGB8888 - zzyzy
就我个人而言,我无法复制那个结果 - 在i5-6200U(Skylake)上进行测试,使用gcc 6.3.0和-mssse3 -O3,对于PixelFix每个(1920x1080)图像需要1.57毫秒,对于rgb_to_bgrx_sse每个图像需要1.07毫秒。 - caf

3

使用vImageConvert_RGB888toARGB8888非常非常快(速度提高了15倍)。

以上PixelFix代码(每张图片约6毫秒,现在在更新的硬件上)


  1. 6.373520毫秒
  2. 6.383363毫秒
  3. 6.413560毫秒
  4. 6.278606毫秒
  5. 6.293607毫秒
  6. 6.368118毫秒
  7. 6.338904毫秒
  8. 6.389385毫秒
  9. 6.365495毫秒

使用vImageConvert_RGB888toARGB8888进行线程处理(在更新的硬件上)


  1. 0.563649毫秒
  2. 0.400387毫秒
  3. 0.375198毫秒
  4. 0.360898毫秒
  5. 0.391278毫秒
  6. 0.396797毫秒
  7. 0.405534毫秒
  8. 0.386495毫秒
  9. 0.367621毫秒

还需要我说什么吗?


1
一个后续的问题...使用单线程128位向量代码"rgb_to_bgrx_sse",对于相同大小的I/O缓冲区,结果在11毫秒范围内。vImage在这里是明显的赢家。 - zzyzy

1

我不完全了解您所要求的内容,急切地等待您的问题的详细回答。同时,我已经想出了一种实现方法,平均速度大约比原来快8至10%。我正在运行Win7 64位操作系统,使用VS2010编译C++代码,并选择快速选项进行发布。

#pragma pack(push, 1)
    struct RGB {
        unsigned char r, g, b;
    };
    
    struct BGRA {
        unsigned char b, g, r, a;
    };
#pragma pack(pop)

    void RGB8ToBGRX8(int width, const void* in, void* out)
    {
        const RGB* src = (const RGB*)in;
        BGRA* dst = (BGRA*)out; 
        do {        
            dst->r = src->r;
            dst->g = src->g;
            dst->b = src->b;
            dst->a = 0xFF;
            src++;
            dst++;
        } while (--width);
    }

我使用结构体的动机是为了让编译器尽可能高效地推进指针src和dst。另一个动机是限制算术运算的数量。


没问题,杰克!如果你能澄清你可能不理解的部分,我可以尝试解释一下。 :) - Rev316
你提到使用 SSE 是什么意思?我认为这意味着指示编译器使用特定的优化技术,如果是这种情况,也许根本不值得手动调整代码。你还说你想使用内部函数,那是什么意思?然而,我对并行化有很好的掌握。 - Jack
哦,我指的是使用SSE2/3或SSSEE进行矢量化计算时的内在机制。主要涉及填充/掩码操作,因为我已经看到其他图像转换采用了优雅的解决方案。现在,我知道GCC4.x有几个编译标志可以帮助解决这个问题,但我不确定哪一个更好。也许你的专业知识能够提供帮助。 - Rev316
好的,我更接近理解了。不好意思,我不是gcc的专家。 - Jack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接