总览
我有一个图像缓冲区,我需要将其转换为另一种格式。源图像缓冲区是四个通道,每个通道8位,分别是Alpha,Red,Green和Blue。目标缓冲区是三个通道,每个通道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的图像数据是真的,真的很糟糕。
运行在Intel处理器(Core 2及更高版本)上,因此存在一些我知道的流传输和数据处理命令,但不知道-也许在哪里可以找到专门的数据处理指令的指针会很好。
这将要进入OS X应用程序,并且我正在使用XCode4。如果汇编很轻松并且很明显,我可以沿着那条路走,但是在没有进行此设置之前,我还是很警惕花费太多时间。
伪代码很好-我不是在寻找完整的解决方案,而只是寻找算法和任何可能不会立即弄清楚的诡计的解释。

最佳答案

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

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

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

还要注意,所有这些覆盖输出末尾的所有1个(标量)或4个(SIMD)字节,如果有问题,则分别单独处理最后3个字节。

--- @彼得·科德斯

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

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,这些是每个设备的平均使用时间:

版本1:10.8630毫秒
版本2:11.3254毫秒
版本3:9.3163毫秒
版本4:9.3584毫秒

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

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

平均标准偏差
蛮力:18.01956毫秒| 1.22980毫秒(6.8%)
版本1:11.13120毫秒| 0.81076毫秒(7.3%)
版本2:11.27092毫秒| 0.66209毫秒(5.9%)
版本3:9.29184毫秒| 0.27851毫秒(3.0%)
版本4:9.40948毫秒| 0.32702毫秒(3.5%)

另外,如果有人需要,这是来自新测试的原始数据。对于每次迭代,都会随机生成一个32MB的数据集,并通过这四个函数运行。下面列出了每个函数的运行时间(以微秒为单位)。

蛮力: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
版本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
版本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
版本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
版本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

07-24 09:45
查看更多