c - 通过翻译复制内存的快速方法 - ARGB 到 BGR

标签 c x86 rgb sse micro-optimization

概览

我有一个图像缓冲区,需要将其转换为另一种格式。原始图像缓冲区是四个 channel ,每个 channel 8 位,Alpha、红色、绿色和蓝色。目标缓冲区是三个 channel ,每个 channel 8 位,蓝色、绿色和红色。

所以暴力破解的方法是:

// Assume a 32 x 32 pixel image
#define IMAGESIZE (32*32)

typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

ARGB orig[IMAGESIZE];
BGR  dest[IMAGESIZE];

for(x = 0; x < IMAGESIZE; x++)
{
     dest[x].Red = orig[x].Red;
     dest[x].Green = orig[x].Green;
     dest[x].Blue = orig[x].Blue;
}

但是,我需要比循环和三字节副本提供的速度更快的速度。考虑到我在 32 位计算机上运行,​​我希望可以使用一些技巧来减少内存读写次数。

附加信息

每张图片都是至少 4 像素的倍数。所以我们可以寻址 16 个 ARGB 字节并将它们移动到每个循环的 12 个 RGB 字节中。也许这个事实可以用来加快速度,尤其是当它很好地落入 32 位边界时。

我可以访问 OpenCL - 虽然这需要将整个缓冲区移动到 GPU 内存中,然后将结果移回,但事实上 OpenCL 可以同时处理图像的许多部分,而且大内存块移动实际上非常有效可能使这成为一个值得探索的地方。

虽然我在上面给出了小缓冲区的例子,但我确实在移动高清视频 (1920x1080) 并且有时会更大,大部分更小,缓冲区,所以虽然 32x32 的情况可能微不足道,但复制 8.3MB 的图像数据字节by byte 真的非常糟糕。

在 Intel 处理器(Core 2 及更高版本)上运行,因此存在流媒体和数据处理命令,我知道存在这些命令,但不知道 - 也许关于在哪里寻找专门数据处理指令的指针会很好。

这将进入 OS X 应用程序,我正在使用 XCode 4。如果组装是无痛的并且是显而易见的方式,我可以沿着这条路前进,但之前没有在这个设置上完成它我担心在其中投入太多时间。

伪代码很好 - 我不是在寻找完整的解决方案,只是在寻找算法和对任何可能无法立即清楚的技巧的解释。

最佳答案

我写了 4 个不同的版本,它们通过交换字节来工作。我使用带有 -O3 -mssse3 的 gcc 4.2.1 编译它们,在 32MB 的随机数据上运行它们 10 次并找到平均值。


编者注:最初的内联汇编使用了不安全的约束,例如修改仅输入操作数,而不告诉编译器 the side effect on memory pointed-to by pointer inputs in registers .显然这对于​​基准测试来说工作正常。我修复了约束以确保所有调用者都安全。这不应该影响基准数字,只确保周围的代码对所有调用者都是安全的。具有更高内存带宽的现代 CPU 应该看到 SIMD 比一次 4 字节标量有更大的加速,但最大的好处是当数据在缓存中很热时(在较小的 block 中工作,或在较小的总大小上工作)。

在 2020 年,最好的选择是使用可移植的 _mm_loadu_si128 内部函数版本,它将编译为等效的 asm 循环:https://gcc.gnu.org/wiki/DontUseInlineAsm .

另请注意,所有这些都会覆盖输出末尾后的 1(标量)或 4(SIMD)字节,因此如果出现问题,请单独覆盖最后 3 个字节。

--- @PeterCordes


第一个版本使用 C 循环分别转换每个像素,使用 OSSwapInt32 函数(使用 -O3 编译为 bswap 指令>).

void swap1(ARGB *orig, BGR *dest, unsigned imageSize) {
    unsigned x;
    for(x = 0; x < imageSize; x++) {
        *((uint32_t*)(((uint8_t*)dest)+x*3)) = OSSwapInt32(((uint32_t*)orig)[x]);
        // warning: strict-aliasing UB.  Use memcpy for unaligned loads/stores
    }
}

第二种方法执行相同的操作,但使用内联汇编循环而不是 C 循环。

void swap2(ARGB *orig, BGR *dest, unsigned imageSize) {
    asm volatile ( // has to be volatile because the output is a side effect on pointed-to memory
        "0:\n\t"                   // do {
        "movl   (%1),%%eax\n\t"
        "bswapl %%eax\n\t"
        "movl   %%eax,(%0)\n\t"    // copy a dword byte-reversed
        "add    $4,%1\n\t"         // orig += 4 bytes
        "add    $3,%0\n\t"         // dest += 3 bytes
        "dec    %2\n\t"
        "jnz    0b"                // }while(--imageSize)
        : "+r" (dest), "+r" (orig), "+r" (imageSize)
        : // no pure inputs; the asm modifies and dereferences the inputs to use them as read/write outputs.
        : "flags", "eax", "memory"
    );
}

第三个版本是just a poseur's answer的修改版.我将内置函数转换为 GCC 等效函数并使用 lddqu 内置函数,这样输入参数就不需要对齐了。 (编者注:只有 P4 从 lddqu 中受益;使用 movdqu 没问题,但没有缺点。)

typedef char v16qi __attribute__ ((vector_size (16)));
void swap3(uint8_t *orig, uint8_t *dest, size_t imagesize) {
    v16qi mask = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};
    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 16, dest += 12) {
        __builtin_ia32_storedqu(dest,__builtin_ia32_pshufb128(__builtin_ia32_lddqu(orig),mask));
    }
}

最后,第四个版本是与第三个版本等效的内联汇编。

void swap2_2(uint8_t *orig, uint8_t *dest, size_t imagesize) {
    static const int8_t mask[16] = {3,2,1,7,6,5,11,10,9,15,14,13,0xFF,0xFF,0xFF,0XFF};
    asm volatile (
        "lddqu  %3,%%xmm1\n\t"
        "0:\n\t"
        "lddqu  (%1),%%xmm0\n\t"
        "pshufb %%xmm1,%%xmm0\n\t"
        "movdqu %%xmm0,(%0)\n\t"
        "add    $16,%1\n\t"
        "add    $12,%0\n\t"
        "sub    $4,%2\n\t"
        "jnz    0b"
        : "+r" (dest), "+r" (orig), "+r" (imagesize)
        : "m" (mask)  // whole array as a memory operand.  "x" would get the compiler to load it
        : "flags", "xmm0", "xmm1", "memory"
    );
}

(这些都是 compile fine with GCC9.3,但 clang10 不知道 __builtin_ia32_pshufb128;使用 _mm_shuffle_epi8。)

在我的 2010 MacBook Pro、2.4 Ghz i5(Westmere/Arrandale)、4GB RAM 上,这些是每个的平均时间:

Version 1: 10.8630 milliseconds
Version 2: 11.3254 milliseconds
Version 3:  9.3163 milliseconds
Version 4:  9.3584 milliseconds

如您所见,编译器在优化方面足够好,您无需编写汇编。此外, vector 函数在 32MB 数据上仅快 1.5 毫秒,因此如果您想支持不支持 SSSE3 的最早的 Intel mac,它不会造成太大伤害。

编辑:liori 要求提供标准差信息。不幸的是,我没有保存数据点,所以我运行了另一个 25 次迭代的测试。

              Average    | Standard Deviation
Brute force: 18.01956 ms | 1.22980 ms (6.8%)
Version 1:   11.13120 ms | 0.81076 ms (7.3%)
Version 2:   11.27092 ms | 0.66209 ms (5.9%)
Version 3:    9.29184 ms | 0.27851 ms (3.0%)
Version 4:    9.40948 ms | 0.32702 ms (3.5%)

此外,这是新测试的原始数据,以备不时之需。对于每次迭代,随机生成一个 32MB 的数据集并运行四个函数。下面列出了每个函数的运行时间(以微秒为单位)。

Brute force: 22173 18344 17458 17277 17508 19844 17093 17116 19758 17395 18393 17075 17499 19023 19875 17203 16996 17442 17458 17073 17043 18567 17285 17746 17845
Version 1:   10508 11042 13432 11892 12577 10587 11281 11912 12500 10601 10551 10444 11655 10421 11285 10554 10334 10452 10490 10554 10419 11458 11682 11048 10601
Version 2:   10623 12797 13173 11130 11218 11433 11621 10793 11026 10635 11042 11328 12782 10943 10693 10755 11547 11028 10972 10811 11152 11143 11240 10952 10936
Version 3:    9036  9619  9341  8970  9453  9758  9043 10114  9243  9027  9163  9176  9168  9122  9514  9049  9161  9086  9064  9604  9178  9233  9301  9717  9156
Version 4:    9339 10119  9846  9217  9526  9182  9145 10286  9051  9614  9249  9653  9799  9270  9173  9103  9132  9550  9147  9157  9199  9113  9699  9354  9314

关于c - 通过翻译复制内存的快速方法 - ARGB 到 BGR,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6804101/

相关文章:

c - Pthreads:如果只允许在一行代码上使用互斥锁,如何确保线程安全

assembly - 链接描述文件未按预期跳过字节

linux - 当错误与内存访问无关时,是否会显示 `segfaults` 错误列表?

c++ - 如何知道返回地址在栈上的位置c/c++

C 编程。将字符串 www.as.com 转换为 3www2as3com0

c - 在包含字符串的 typedef 结构中使用指针

java - 处理 - 颜色循环

java - Eclipse 上的 java 随机彩色打印

html - 或多或少正确地从 RGB 转换为 CMYK

c - struct数组的数据结构