【问题标题】:Fast vectorized conversion from RGB to BGRA从 RGB 到 BGRA 的快速矢量化转换
【发布时间】:2011-11-03 21:35:13
【问题描述】:

在对之前关于将 RGB 转换为 RGBA 和 ARGB 转换为 BGR 的一些问题的后续行动中,我想通过 SSE 加快 RGB 到 BGRA 的转换.假设一台 32 位机器,并且想使用 intrinsics。我很难将源缓冲区和目标缓冲区对齐以使用 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

【问题讨论】:

  • 您是否在为您的编译器使用优化标志(哪个?)?编译器通常会更好地优化代码,不会引入错误。您收集了哪些基准数据?
  • 不是 SSE 答案,但您是否尝试过展开循环 4 次以使输入始终从对齐的地址开始?然后,您可以一次读取输入一个机器字,而不是按字节读取,并对源像素的每个相对位置进行专门的移位和屏蔽。正如 Dana 所提到的,值得看看编译器在高优化级别上的表现如何(检查生成的汇编代码,除了基准测试),但我怀疑它是否会足够积极地展开循环根据in的对齐方式全部自行拆分入口点。
  • 好问题。它只是 GCC4.6 的“O2”(不是 O3)。我的基准案例是 10K 迭代运行,512 作为“宽度”跨度。感谢您的精彩回复!

标签: c opengl sse simd vectorization


【解决方案1】:

我不完全了解您的要求,我急切地等待您的问题得到适当的答复。与此同时,我提出了平均快 8% 到 10% 的 am 实现。我正在运行 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。另一个动机是限制算术运算的数量。

【讨论】:

  • 杰克不用担心!如果您能澄清您可能不理解的部分,我可以尝试详细说明。 :)
  • 使用 SSE 是什么意思?我认为这意味着指示编译器使用特定的优化技术,如果是这种情况,可能根本不值得手动调整代码。你还说你想使用内在函数,你是什么意思?但是,我对并行化有很好的了解。
  • 哦。我指的是使用 SSE2/3 或 SSSEE 的矢量化特性。主要是填充/遮罩操作,因为我已经看到了其他图像转换的优雅解决方案。现在,我知道 GCC4.x 有几个在这里有帮助的编译标志,但我不确定哪个和/或它是否更好。也许您的专业知识在这里会有所帮助。
  • 好的,我更接近理解了。不抱歉,我不是 gcc 专家。
【解决方案2】:

这是使用 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 展开,但仍然没有比字节加载 + 或 godbolt.org/z/Ei9C_d 做得更好。在 Sandybridge 系列 CPU 上,gcc 的版本每 3 个时钟周期最多可以运行 4 个字节,如果竞争超线程则更少,前端瓶颈在每个时钟 4 微秒。那是垃圾。很难想象这个pshufb 版本至少不会快 3 倍,而且更容易取决于内存带宽。
  • 嗯,不过,看起来像是一些错过的优化。使用 palignr / _mm_alignr_epi8 从 3 个对齐的负载中获取四个 9 字节的窗口,而不是使用 AND/AND/OR 进行合并。或使用movsdpunpcklqdq 合并高/低半部分,或合并低半部分。或者特别是在 Haswell 及更高版本上(每个时钟 1 次随机播放),只需执行四个未对齐的加载。 Nehalem / K10 及更高版本具有高效的未对齐负载。 (但在 Skylake 之前,页面拆分仍然很糟糕。)
  • @PeterCordes:是的,你是对的——tweak the scalar code to get 4 byte loads 当然可以,但它看起来仍然不快。我不确定我要比较的内存带宽是多少,7 年是很长的时间。 palignr 优化看起来不错,我可以试试看。
  • 哦,我忘了这也是将字节顺序反转为 BGRA,而不仅仅是 SSE2 convert packed RGB to RGBA pixels (add a 4th 0xFF byte after every 3 bytes)。使用像__builtin_bswap32(in) | 0xFF000000 这样的倒序函数来获得mov + bswap + OR + mov。 (但这仍然是 4 uops,不计算 pointers +=3*unroll+=4 * unroll 的任何循环开销,因此我们只能通过巨大的展开来接近每个时钟 1 个 DWORD 存储)或在 Atom/Silvermont(但不是 Haswell)上,movbe可以保存一个uop。
  • @PeterCordes:palign 变化实际上最终导致了轻微的悲观化,我不确定具体原因。 godbolt.org/z/Y3-Dbh
【解决方案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 是一个 unsigned char*。

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。

【讨论】:

  • 除此之外,您可能希望查看 Apple 最近的 vImage 转换 API...,特别是诸如“vImageConvert_RGB888toARGB8888”之类的例程,用于将 24 位 RGB 转换为 32 位 ARGB(或 BGRA)。 developer.apple.com/library/mac/documentation/Performance/…
  • FWIW 我无法复制该结果 - 在 i5-6200U (Skylake) 上使用 gcc 6.3.0 使用 -mssse3 -O3 进行测试,PixelFix 和 1.07ms 的每个 (1920x1080) 图像得到 1.57 毫秒rgb_to_bgrx_sse 的每张图片。
【解决方案4】:

嗯...使用 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_RGB888toARGB888,线程化(在较新的硬件上)


  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 毫秒

需要我多说吗?

【讨论】:

  • 一个后续...使用上面的单线程 128 位向量代码“rgb_to_bgrx_sse”给出了相同大小的 I/O 缓冲区在 11 毫秒范围内的结果。 vImage 是明显的赢家。
猜你喜欢
  • 1970-01-01
  • 2012-07-26
  • 2020-08-19
  • 1970-01-01
  • 1970-01-01
  • 2011-06-19
  • 2015-05-24
  • 2010-09-17
相关资源
最近更新 更多