【问题标题】:Fast method to copy memory with translation - ARGB to BGR使用翻译复制内存的快速方法 - ARGB 到 BGR
【发布时间】:2011-07-24 00:07:23
【问题描述】:

概述

我有一个需要转换为另一种格式的图像缓冲区。原始图像缓冲区是四个通道,每个通道 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 应用程序,并且我正在使用 XCode 4。如果组装是轻松的并且是显而易见的方法,那么我可以沿着这条路走下去,但之前没有在此设置上完成过我担心会花太多时间在里面。

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

【问题讨论】:

  • 编译器是否将 BGR 与 dword 对齐?
  • @marinara 不,它是字节对齐的。
  • 使用 GPU 应该没有意义,除非数据从那里进入系统。您应该能够用 CPU 使内存总线饱和。
  • 我没有玩过任何人的代码,但 AFAICT 没有人提到与 for(x = 0; x &lt; IMAGESIZE; x++) { dest[x].Red = orig[x].Red; } for(x = 0; x &lt; IMAGESIZE; x++) { dest[x].Green = orig[x].Green; } for(x = 0; x &lt; IMAGESIZE; x++) { dest[x].Blue = orig[x].Blue; } 等效的可能性。在这种情况下,更简单的循环是否超过了旋转?

标签: c x86 rgb sse micro-optimization


【解决方案1】:

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


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

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

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

--- @PeterCordes


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

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

如您所见,编译器在优化方面已经足够好,您无需编写程序集。此外,矢量函数在 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 17508 19844 17308 19844 17308 17499 1985/17403 17403 17499 1799 1799 版本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 11621 10793 11021 110943 10943 110281C1114401C10901C109441C109301C109301C10901C109444444401C101C109301C10936 版本 3:9036 9619 9341 8970 9453 9758 9043 10114 9243 9027 9163 9176 9168 9122 9514 9049 9161 9086 9064 9604 9178 9233 15601 9717 9 版本4:9339 10119 9846 9217 9526 9182 9145 10286 9051 9614 9249 9653 9799 9270 9173 9103 9132 9550 9147 9157 9199 9113 914699 9354

【讨论】:

  • 我对新版本的 GCC 的性能很感兴趣。 4.2 和 4.6 之间引入的一些优化令人印象深刻。另外,你能计算一下这些时间的标准差吗?
  • @liori 我添加了更多数据,包括标准偏差。抱歉,我现在没有比 4.2.1 更新的版本。以后有新版本后我会更新。
  • @MrE 这不是一个愚蠢的问题。我为提供的代码添加了一个数据集,它所用的时间大约是pshufb 的两倍。
  • @ughoavgfhw 已经证明它主要受内存限制。在其中一个循环中尝试显式预取以查看是否有帮助会很有趣。
  • @Camilo 编译器可以优化 C 代码,但不能优化内联汇编,因此汇编版本可能有不必要的填充,例如保存未使用的寄存器。两者的差值小于标准差,也可能是外界影响造成的。
【解决方案2】:

显而易见,使用 pshufb。

#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>

// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
    assert((uintptr_t)orig % 16 == 0);
    assert(imagesize % 4 == 0);
    __m128i mask = _mm_set_epi8(-128, -128, -128, -128, 13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3);
    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 16, dest += 12) {
        _mm_storeu_si128((__m128i *)dest, _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), mask));
    }
}

【讨论】:

  • +1 这几乎肯定是最佳的。有可能让编译器在不使用不可移植的内在函数的情况下生成相同或相似的代码......
  • 我们将不胜感激 _mm_set_epi8 的幻数解释。
  • @Daniel,看看我的回答。
【解决方案3】:

结合仅姿势者和 Jitamaro 的答案,如果您假设输入和输出是 16 字节对齐的,并且如果您一次处理 4 个像素,则可以使用 shuffle、mask、ands 和 ors 的组合来存储使用对齐的商店。主要思想是生成四个中间数据集,然后或它们与掩码一起选择相关的像素值并写出3个16字节的像素数据集。请注意,我根本没有编译或尝试运行它。

EDIT2:关于底层代码结构的更多细节:

使用 SSE2,您可以通过 16 字节对齐的 16 字节读取和写入获得更好的性能。由于您的 3 字节像素每 16 个像素只能对齐到 16 个字节,因此我们一次使用 shuffle 和 mask 以及 16 个输入像素的 ors 组合来批量处理 16 个像素。

从 LSB 到 MSB,输入如下所示,忽略特定组件:

s[0]: 0000 0000 0000 0000
s[1]: 1111 1111 1111 1111
s[2]: 2222 2222 2222 2222
s[3]: 3333 3333 3333 3333

输出如下所示:

d[0]: 000 000 000 000 111 1
d[1]:  11 111 111 222 222 22
d[2]:   2 222 333 333 333 333

因此,要生成这些输出,您需要执行以下操作(稍后我将指定实际的转换):

d[0]= combine_0(f_0_low(s[0]), f_0_high(s[1]))
d[1]= combine_1(f_1_low(s[1]), f_1_high(s[2]))
d[2]= combine_2(f_1_low(s[2]), f_1_high(s[3]))

现在,combine_&lt;x&gt; 应该是什么样子?如果我们假设 d 只是将 s 压缩在一起,我们可以将两个 s 与一个掩码和一个或连接起来:

combine_x(left, right)= (left & mask(x)) | (right & ~mask(x))

其中(1 表示选择左侧像素,0 表示选择右侧像素): 掩码(0)= 111 111 111 111 000 0 掩码 (1)= 11 111 111 000 000 00 掩码(2)= 1 111 000 000 000 000

但实际的转换(f_&lt;x&gt;_lowf_&lt;x&gt;_high)实际上并没有那么简单。由于我们正在从源像素中反转和删除字节,因此实际的转换是(为简洁起见,对于第一个目标):

d[0]= 
    s[0][0].Blue s[0][0].Green s[0][0].Red 
    s[0][1].Blue s[0][1].Green s[0][1].Red 
    s[0][2].Blue s[0][2].Green s[0][2].Red 
    s[0][3].Blue s[0][3].Green s[0][3].Red
    s[1][0].Blue s[1][0].Green s[1][0].Red
    s[1][1].Blue

如果您将上述内容转换为从源到目标的字节偏移量,您会得到: d[0]= &s[0]+3 &s[0]+2 &s[0]+1
&s[0]+7 &s[0]+6 &s[0]+5 &s[0]+11 &s[0]+10 &s[0]+9 &s[0]+15 &s[0]+14 &s[0]+13
&s[1]+3 &s[1]+2 &s[1]+1
&s[1]+7

(如果您查看所有 s[0] 偏移量,它们只会以相反的顺序匹配姿势者的 shuffle 掩码。)

现在,我们可以生成一个 shuffle 掩码来将每个源字节映射到一个目标字节(X 表示我们不关心那个值是什么):

f_0_low=  3 2 1  7 6 5  11 10 9  15 14 13  X X X  X
f_0_high= X X X  X X X   X  X X   X  X  X  3 2 1  7

f_1_low=    6 5  11 10 9  15 14 13  X X X   X X X  X  X
f_1_high=   X X   X  X X   X  X  X  3 2 1   7 6 5  11 10

f_2_low=      9  15 14 13  X  X  X  X X X   X  X  X  X  X  X
f_2_high=     X   X  X  X  3  2  1  7 6 5   11 10 9  15 14 13

我们可以通过查看我们为每个源像素使用的掩码来进一步优化这一点。如果您看一下我们用于 s[1] 的 shuffle 掩码:

f_0_high=  X  X  X  X  X  X  X  X  X  X  X  X  3  2  1  7
f_1_low=   6  5 11 10  9 15 14 13  X  X  X  X  X  X  X  X

由于两个 shuffle 蒙版不重叠,我们可以将它们组合起来,并简单地屏蔽掉 combine_ 中不相关的像素,我们已经这样做了!以下代码执行所有这些优化(另外它假设源地址和目标地址是 16 字节对齐的)。此外,掩码以 MSB->LSB 顺序写在代码中,以防您对排序感到困惑。

编辑:将存储更改为_mm_stream_si128,因为您可能会进行大量写入,而我们不一定要刷新缓存。另外,无论如何它都应该对齐,这样您就可以获得免费的性能!

#include <assert.h>
#include <inttypes.h>
#include <tmmintrin.h>

// needs:
// orig is 16-byte aligned
// imagesize is a multiple of 4
// dest has 4 trailing scratch bytes
void convert(uint8_t *orig, size_t imagesize, uint8_t *dest) {
    assert((uintptr_t)orig % 16 == 0);
    assert(imagesize % 16 == 0);

    __m128i shuf0 = _mm_set_epi8(
        -128, -128, -128, -128, // top 4 bytes are not used
        13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3); // bottom 12 go to the first pixel

    __m128i shuf1 = _mm_set_epi8(
        7, 1, 2, 3, // top 4 bytes go to the first pixel
    -128, -128, -128, -128, // unused
        13, 14, 15, 9, 10, 11, 5, 6); // bottom 8 go to second pixel

    __m128i shuf2 = _mm_set_epi8(
        10, 11, 5, 6, 7, 1, 2, 3, // top 8 go to second pixel
    -128, -128, -128, -128, // unused
        13, 14, 15, 9); // bottom 4 go to third pixel

    __m128i shuf3 = _mm_set_epi8(
        13, 14, 15, 9, 10, 11, 5, 6, 7, 1, 2, 3, // top 12 go to third pixel
        -128, -128, -128, -128); // unused

    __m128i mask0 = _mm_set_epi32(0, -1, -1, -1);
    __m128i mask1 = _mm_set_epi32(0,  0, -1, -1);
    __m128i mask2 = _mm_set_epi32(0,  0,  0, -1);

    uint8_t *end = orig + imagesize * 4;
    for (; orig != end; orig += 64, dest += 48) {
        __m128i a= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig), shuf0);
        __m128i b= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 1), shuf1);
        __m128i c= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 2), shuf2);
        __m128i d= _mm_shuffle_epi8(_mm_load_si128((__m128i *)orig + 3), shuf3);

        _mm_stream_si128((__m128i *)dest, _mm_or_si128(_mm_and_si128(a, mask0), _mm_andnot_si128(b, mask0));
        _mm_stream_si128((__m128i *)dest + 1, _mm_or_si128(_mm_and_si128(b, mask1), _mm_andnot_si128(c, mask1));
        _mm_stream_si128((__m128i *)dest + 2, _mm_or_si128(_mm_and_si128(c, mask2), _mm_andnot_si128(d, mask2));
    }
}

【讨论】:

  • 您有机会提供从 BGRA 到 RGB 的随机播放吗?我无法理解这一切是如何运作的。
【解决方案4】:

我来晚了,看来社区已经决定了poseur的pshufb-answer但分配了2000声望,这太慷慨了,我不得不试一试。

这是我的版本,没有特定于平台的内在函数或特定于机器的 asm,如果你像我一样进行位旋转 激活编译器优化(寄存器优化、循环展开):

#include "stdlib.h"
#include "stdio.h"
#include "time.h"

#define UInt8 unsigned char

#define IMAGESIZE (1920*1080) 
int main() {
    time_t  t0, t1;
    int frames;
    int frame; 
    typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
    typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;

    ARGB* orig = malloc(IMAGESIZE*sizeof(ARGB));
    if(!orig) {printf("nomem1");}
    BGR* dest = malloc(IMAGESIZE*sizeof(BGR));
    if(!dest) {printf("nomem2");}

    printf("to start original hit a key\n");
    getch();
    t0 = time(0);
    frames = 1200;
    for(frame = 0; frame<frames; frame++) {
        int x; for(x = 0; x < IMAGESIZE; x++) {
            dest[x].Red = orig[x].Red;
            dest[x].Green = orig[x].Green;
            dest[x].Blue = orig[x].Blue;
            x++;
        }
    }
    t1 = time(0);
    printf("finished original of %u frames in %u seconds\n", frames, t1-t0);

    // on my core 2 subnotebook the original took 16 sec 
    // (8 sec with compiler optimization -O3) so at 60 FPS 
    // (instead of the 1200) this would be faster than realtime 
    // (if you disregard any other rendering you have to do). 
    // However if you either want to do other/more processing 
    // OR want faster than realtime processing for e.g. a video-conversion 
    // program then this would have to be a lot faster still.

    printf("to start alternative hit a key\n");
    getch();
    t0 = time(0);
    frames = 1200;
    unsigned int* reader;
    unsigned int* end = reader+IMAGESIZE;
    unsigned int cur; // your question guarantees 32 bit cpu
    unsigned int next;
    unsigned int temp;
    unsigned int* writer;
    for(frame = 0; frame<frames; frame++) {
        reader = (void*)orig;
        writer = (void*)dest;
        next = *reader;
        reader++;
        while(reader<end) {
            cur = next;
            next = *reader;         
            // in the following the numbers are of course the bitmasks for 
            // 0-7 bits, 8-15 bits and 16-23 bits out of the 32
            temp = (cur&255)<<24 | (cur&65280)<<16|(cur&16711680)<<8|(next&255); 
            *writer = temp;
            reader++;
            writer++;
            cur = next;
            next = *reader;
            temp = (cur&65280)<<24|(cur&16711680)<<16|(next&255)<<8|(next&65280);
            *writer = temp;
            reader++;
            writer++;
            cur = next;
            next = *reader;
            temp = (cur&16711680)<<24|(next&255)<<16|(next&65280)<<8|(next&16711680);
            *writer = temp;
            reader++;
            writer++;
        }
    }
    t1 = time(0);
    printf("finished alternative of %u frames in %u seconds\n", frames, t1-t0);

    // on my core 2 subnotebook this alternative took 10 sec 
    // (4 sec with compiler optimization -O3)

}

结果如下(在我的核心 2 子笔记本上):

F:\>gcc b.c -o b.exe

F:\>b
to start original hit a key
finished original of 1200 frames in 16 seconds
to start alternative hit a key
finished alternative of 1200 frames in 10 seconds

F:\>gcc b.c -O3 -o b.exe

F:\>b
to start original hit a key
finished original of 1200 frames in 8 seconds
to start alternative hit a key
finished alternative of 1200 frames in 4 seconds

【讨论】:

  • 顺便说一句,1200帧当然是1920*1080像素的图片
【解决方案5】:

您想使用 Duff 的设备:http://en.wikipedia.org/wiki/Duff%27s_device。它也适用于 JavaScript。不过这篇文章读起来有点有趣http://lkml.indiana.edu/hypermail/linux/kernel/0008.2/0171.html。想象一下具有 512 KB 移动的 Duff 设备。

【讨论】:

  • Duff 的设备只是一种奇怪的 C 特有的展开循环的方式。要获得真正出色的性能,还需要更多的时间。
  • 我的 C 和程序集有点生疏了,但是当你必须用 CPU 移动所有东西时,展开一个循环总比什么都好。
  • @R:我不是经验丰富的 C 程序员。我正在设计网络应用程序。请你解释一下好吗?有什么好笑的?
【解决方案6】:

结合此处的快速转换功能之一,如果可以访问 Core 2,将转换拆分为线程可能是明智的,这些线程可以处理它们的第四个数据,如以下伪代码所示:

void bulk_bgrFromArgb(byte[] dest, byte[] src, int n)
{
       thread threads[] = {
           create_thread(bgrFromArgb, dest, src, n/4),
           create_thread(bgrFromArgb, dest+n/4, src+n/4, n/4),
           create_thread(bgrFromArgb, dest+n/2, src+n/2, n/4),
           create_thread(bgrFromArgb, dest+3*n/4, src+3*n/4, n/4),
       }
       join_threads(threads);
}

【讨论】:

  • 真的吗?我希望内存访问是这里的瓶颈,而不是 CPU 处理,所以使用额外的内核不会给你带来任何好处?
  • 每个额外的内核都附带一个 L1 缓存,因此虽然内存是瓶颈,但使用更多内核可能会为您购买一些额外的缓存来帮助缓解它。
  • 仅当您的线程运行在部分阵列已经在缓存中处于热状态的内核上时。就像你有工作线程只写了 src 的单独块,或者稍后将在他们刚刚写的 dest 的部分上做更多的工作,正确的线程与正确的块匹配,希望仍然在同一个 CPU 内核上运行。否则,这仅在单个 CPU 内核无法使内存带宽饱和时才有用(在大型 Xeon 上就是这种情况,每内核带宽很低,但在典型的现代四核桌面上则不然)。
【解决方案7】:

这个汇编函数应该可以,但是我不知道你是否想保留旧数据,这个函数会覆盖它。

代码适用于具有 intel 汇编风格的 MinGW GCC,您必须对其进行修改以适合您的编译器/汇编器。

extern "C" {
    int convertARGBtoBGR(uint buffer, uint size);
    __asm(
        ".globl _convertARGBtoBGR\n"
        "_convertARGBtoBGR:\n"
        "  push ebp\n"
        "  mov ebp, esp\n"
        "  sub esp, 4\n"
        "  mov esi, [ebp + 8]\n"
        "  mov edi, esi\n"
        "  mov ecx, [ebp + 12]\n"
        "  cld\n"
        "  convertARGBtoBGR_loop:\n"
        "    lodsd          ; load value from [esi] (4byte) to eax, increment esi by 4\n"
        "    bswap eax ; swap eax ( A R G B ) to ( B G R A )\n"
        "    stosd          ; store 4 bytes to [edi], increment  edi by 4\n"
        "    sub edi, 1; move edi 1 back down, next time we will write over A byte\n"
        "    loop convertARGBtoBGR_loop\n"
        "  leave\n"
        "  ret\n"
    );
}

你应该这样称呼它:

convertARGBtoBGR( &buffer, IMAGESIZE );

与您的 蛮力方法(至少/假设它已编译为注册)3 次读取和3 写操作。方法相同,但实现更高效。

【讨论】:

    【解决方案8】:

    您可以以 4 像素的块为单位,使用无符号长指针移动 32 位。试想一下,你可以通过移位和 OR/AND 来构造 4 个 32 位像素,3 个字代表 4 个 24 位像素,如下所示:

    //col0 col1 col2 col3
    //ARGB ARGB ARGB ARGB 32bits reading (4 pixels)
    //BGRB GRBG RBGR  32 bits writing (4 pixels)
    

    在所有现代 32/64 位处理器(桶式移位技术)中,移位操作总是由 1 个指令周期完成,因此它是构建这 3 个字以进行写入的最快方法,按位 AND 和 OR 也非常快。

    像这样:

    //assuming we have 4 ARGB1 ... ARGB4 pixels and  3 32 bits words,  W1, W2 and W3 to write
    // and *dest  its an unsigned long pointer for destination
    W1 = ((ARGB1 & 0x000f) << 24) | ((ARGB1 & 0x00f0) << 8) | ((ARGB1 & 0x0f00) >> 8) | (ARGB2 & 0x000f);
    *dest++ = W1;
    

    等等....循环中的下一个像素。

    您需要对不是 4 的倍数的图像进行一些调整,但我敢打赌这是所有方法中最快的方法,无需使用汇编程序。

    顺便说一句,忘记使用结构和索引访问,这些都是 SLOWER 移动数据的所有方式,只需查看已编译 C++ 程序的反汇编列表,您就会同意和我一起。

    【讨论】:

      【解决方案9】:
      typedef struct{ UInt8 Alpha; UInt8 Red; UInt8 Green; UInt8 Blue; } ARGB;
      typedef struct{ UInt8 Blue; UInt8 Green; UInt8 Red; } BGR;
      

      除了汇编或编译器内在函数之外,我可能会尝试执行以下操作,同时非常仔细地验证最终行为,因为其中一些(涉及联合的地方)可能依赖于编译器实现:

      union uARGB
      {
         struct ARGB argb;
         UInt32 x;
      };
      union uBGRA
      {
         struct 
         {
           BGR bgr;
           UInt8 Alpha;
         } bgra;
         UInt32 x;
      };
      

      然后对于您的代码内核,使用合适的循环展开:

      inline void argb2bgr(BGR* pbgr, ARGB* pargb)
      {
          uARGB* puargb = (uARGB*)pargb;
          uBGRA ubgra;
          ubgra.x = __byte_reverse_32(pargb->x);
          *pbgr = ubgra.bgra.bgr;
      }
      

      其中__byte_reverse_32() 假定存在一个编译器内在函数,它反转 32 位字的字节。

      总结底层方法:

      • 将 ARGB 结构视为 32 位整数
      • 反转 32 位整数
      • 将反转的 32 位整数视为 (BGR)A 结构
      • 让编译器复制 (BGR)A 结构的 (BGR) 部分

      【讨论】:

      • 我已经使用低级字符串处理指令(例如lodsdstosd)在汇编中实现了类似的方法。不幸的是,试验似乎没有用——建议的pshufb 解决方案得分更高。
      【解决方案10】:

      虽然你可以根据 CPU 使用情况使用一些技巧,

      This kind of operations can be done fasted with GPU.
      

      您似乎使用 C/C++...所以您的 GPU 编程 替代方案可能是(在 windows 平台上)

      暂时使用 GPU 进行这种数组运算,以加快计算速度。它们是为此而设计的。

      【讨论】:

      • 不要忘记从 CPU 总线访问 GPU 视频内存比在 CPU 内存映射中完成的移动操作要慢。要比主处理器更快,需要在显存中执行所有转换。
      【解决方案11】:

      我还没有看到任何人展示如何在 GPU 上执行此操作的示例。

      不久前,我写了一些与您的问题类似的东西。我从 video4linux2 相机接收到 YUV 格式的数据,并想在屏幕上将其绘制为灰度级(只是 Y 分量)。我还想用蓝色绘制太暗的区域,用红色绘制过饱和区域。

      我从 freeglut 发行版中的 smooth_opengl3.c 示例开始。

      数据作为 YUV 复制到纹理中,然后应用以下 GLSL 着色器程序。我敢肯定 GLSL 代码现在可以在所有的 mac 上运行,它会比所有 CPU 方法快得多。

      请注意,我没有关于如何取回数据的经验。理论上 glReadPixels 应该读回数据,但我从未测量过它的性能。

      OpenCL 可能是更简单的方法,但只有当我有支持它的笔记本时,我才会开始为此进行开发。

      (defparameter *vertex-shader*
      "void main(){
          gl_Position    = gl_ModelViewProjectionMatrix * gl_Vertex;
          gl_FrontColor  = gl_Color;
          gl_TexCoord[0] = gl_MultiTexCoord0;
      }
      ")
      
      (progn
       (defparameter *fragment-shader*
         "uniform sampler2D textureImage;
      void main()
      {
        vec4 q=texture2D( textureImage, gl_TexCoord[0].st);
        float v=q.z;
        if(int(gl_FragCoord.x)%2 == 0)
           v=q.x; 
        float x=0; // 1./255.;
        v-=.278431;
        v*=1.7;
        if(v>=(1.0-x))
          gl_FragColor = vec4(255,0,0,255);
        else if (v<=x)
          gl_FragColor = vec4(0,0,255,255);
        else
          gl_FragColor = vec4(v,v,v,255); 
      }
      ")
      

      【讨论】:

      • 它将比所有 CPU 方法都快得多。 不适用于小缓冲区,尤其是那些在 CPU 核心上的二级缓存中已经很热的缓冲区。但可以肯定的是,对于很多大型缓冲区来说,这可能是件好事,尤其是当它发生在 CPU 处理其他事情时。如果可以高效地将数据返回到 CPU,但 GPU -> CPU 传输通常不会超快。
      猜你喜欢
      • 2019-11-08
      • 2016-02-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多