8

我正在尝试找到一种用于 RGB8 到 RGB32 图像转换的程序集优化方法。

源是 8 位灰度图像,目标应该是 32 位灰度图像 (​​BGRA),第 4 通道 (alpha) 被忽略。源地址不保证 16 字节对齐,Count 是 16 的倍数,目标地址是 16 字节对齐。

  • 输入:8位单通道灰度图像
  • 输出:32 位 BGRA(忽略 alpha 通道)
  • COUNT:图像大小是 16 的倍数
  • CPU:x86-32(允许 SSE2/SSE3)

这是我优化的汇编代码。有没有更快的转换方式?

void ConvertGreyToRgb32Assembler(__m128i* Source, __m128i* Destination, unsigned int Count) {
    static unsigned int __declspec(align(64)) Masks[] = {
        0x80000000, 0x80010101, 0x80020202, 0x80030303,
        0x80040404, 0x80050505, 0x80060606, 0x80070707,
        0x80080808, 0x80090909, 0x800a0a0a, 0x800b0b0b,
        0x800c0c0c, 0x800d0d0d, 0x800e0e0e, 0x800f0f0f
    };

    __asm {
        mov esi, Source
        mov edi, Destination
        mov edx, Count
        xor ecx, ecx
        movdqa xmm4, xmmword ptr [Masks + 0]
        movdqa xmm5, xmmword ptr [Masks + 16]
        movdqa xmm6, xmmword ptr [Masks + 32]
        movdqa xmm7, xmmword ptr [Masks + 48]
l1:
        movdqu xmm0, xmmword ptr [esi + ecx]
        movdqa xmm1, xmm0
        movdqa xmm2, xmm0
        movdqa xmm3, xmm0
        pshufb xmm0, xmm4
        pshufb xmm1, xmm5
        pshufb xmm2, xmm6
        pshufb xmm3, xmm7
        movntdq [edi + 0], xmm0
        movntdq [edi + 16], xmm1
        movntdq [edi + 32], xmm2
        movntdq [edi + 48], xmm3
        add edi, 64
        add ecx, 16
        cmp ecx, edx
        jb l1
    }
}

还有另一种使用几个 PUNPCKLBW 和 PUNPCKHBW 的方法,但这似乎有点慢。

更新:这是基本的非优化算法:

BGRA* Destination = ...
unsigned char* Source ...
for (unsigned int i = 0; i < Size; i++) {
    Destination[i].Blue = Source[i];
    Destination[i].Green = Source[i];
    Destination[i].Red = Source[i]; 
}

PS:我还尝试将 C 代码与 MS VS2008 SSE 编译器内在函数一起使用。事实证明,编译器产生了大量不必要的内存移动,导致代码比纯汇编慢 10-20%。

更新 2:这是仅使用内部函数的相同代码。

void ConvertGreyToRgb32Assembler(__m128i* Source, __m128i* Destination, unsigned int Count) {
    static const unsigned int __declspec(align(64)) Masks[] = {
        0x80000000, 0x80010101, 0x80020202, 0x80030303,
        0x80040404, 0x80050505, 0x80060606, 0x80070707,
        0x80080808, 0x80090909, 0x800a0a0a, 0x800b0b0b,
        0x800c0c0c, 0x800d0d0d, 0x800e0e0e, 0x800f0f0f
    };

    register __m128i m0 = _mm_load_si128((__m128i*) (Masks + 0));
    register __m128i m1 = _mm_load_si128((__m128i*) (Masks + 4));
    register __m128i m2 = _mm_load_si128((__m128i*) (Masks + 8));
    register __m128i m3 = _mm_load_si128((__m128i*) (Masks + 12));

    for (unsigned int i = 0; i < Count / 16; i++) {
        __m128i r0 = _mm_load_si128(Source + i);

        _mm_stream_si128(Destination + (i * 4) + 0, _mm_shuffle_epi8(r0, m0));
        _mm_stream_si128(Destination + (i * 4) + 1, _mm_shuffle_epi8(r0, m1));
        _mm_stream_si128(Destination + (i * 4) + 2, _mm_shuffle_epi8(r0, m2));
        _mm_stream_si128(Destination + (i * 4) + 3, _mm_shuffle_epi8(r0, m3));
    }
}

更新3:这是编译器生成的代码(美化)(Visual Studio 2012,所有优化):

    push ebp 
    mov ebp, esp 
    mov edx, dword ptr [ebp+8]
    movdqa xmm1, xmmword ptr ds:[Masks + 0]
    movdqa xmm2, xmmword ptr ds:[Masks + 16]
    movdqa xmm3, xmmword ptr ds:[Masks + 32]
    movdqa xmm4, xmmword ptr ds:[Masks + 48]
    push esi 
    test ecx, ecx 
    je l2
    lea esi, [ecx-1] 
    shr esi, 4 
    inc esi
l1:
    mov ecx, edx 
    movdqu xmm0, xmmword ptr [ecx] 
    mov ecx, eax 
    movdqa xmm5, xmm0 
    pshufb xmm5, xmm1 
    movdqa xmmword ptr [ecx], xmm5 
    movdqa xmm5, xmm0 
    pshufb xmm5, xmm2 
    movdqa xmmword ptr [eax+10h], xmm5 
    movdqa xmm5, xmm0
    pshufb xmm5, xmm3
    movdqa xmmword ptr [eax+20h], xmm5 
    lea ecx, [eax+30h]
    add edx, 10h 
    add eax, 40h 
    dec esi 
    pshufb xmm0, xmm4 
    movdqa xmmword ptr [ecx], xmm0 
    jne l1
l2:
    pop esi
    pop ebp
    ret

似乎交错movdqapshufb更快一些。

更新 4:这似乎是最佳的手动优化代码:

   __asm {
        mov esi, Source
        mov edi, Destination
        mov ecx, Count
        movdqu xmm0, xmmword ptr [esi]
        movdqa xmm4, xmmword ptr [Masks + 0]
        movdqa xmm5, xmmword ptr [Masks + 16]
        movdqa xmm6, xmmword ptr [Masks + 32]
        movdqa xmm7, xmmword ptr [Masks + 48]
l1:
        dec ecx
        lea edi, [ edi + 64 ]
        lea esi, [ esi + 16 ]
        movdqa xmm1, xmm0
        movdqa xmm2, xmm0
        movdqa xmm3, xmm0
        pshufb xmm0, xmm4
        movdqa [edi - 64], xmm0
        pshufb xmm1, xmm5
        movdqa [edi - 48], xmm1
        pshufb xmm2, xmm6
        movdqa [edi - 32], xmm2
        pshufb xmm3, xmm7
        movdqa [edi - 16], xmm3
        movdqu xmm0, xmmword ptr [esi]
        ja l1
    }

更新 5:此转换算法使用punpck指令。但是,此转换例程比使用掩码和pushfb.

for (unsigned int i = 0; i < Count; i += 16) {
    register __m128i r0 = _mm_load_si128(Source++);
    register __m128i r1 = _mm_unpackhi_epi8(r0, r0);
    register __m128i r2 = _mm_unpacklo_epi8(r0, r0);
    register __m128i r3 = _mm_unpackhi_epi8(r1, r1);
    register __m128i r4 = _mm_unpacklo_epi8(r1, r1);
    register __m128i r5 = _mm_unpackhi_epi8(r2, r2);
    register __m128i r6 = _mm_unpacklo_epi8(r2, r2);

    _mm_store_si128(Destination++, r6);
    _mm_store_si128(Destination++, r5);
    _mm_store_si128(Destination++, r4);
    _mm_store_si128(Destination++, r3);
}

更新 6:为了完整起见,这是从 32 位转换回 8 位灰度图像的逆方法。

static void ConvertRgb32ToGrey(const __m128i* Source, __m128i* Destination, unsigned int Count) {
    static const unsigned char __declspec(align(64)) Masks[] = {
        0x00, 0x04, 0x08, 0x0c, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
        0x80, 0x80, 0x80, 0x80, 0x00, 0x04, 0x08, 0x0c, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
        0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x04, 0x08, 0x0c, 0x80, 0x80, 0x80, 0x80,
        0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x04, 0x08, 0x0c,
    };

    register __m128i m0 = _mm_load_si128((__m128i*) (Masks + 0));
    register __m128i m1 = _mm_load_si128((__m128i*) (Masks + 16));
    register __m128i m2 = _mm_load_si128((__m128i*) (Masks + 32));
    register __m128i m3 = _mm_load_si128((__m128i*) (Masks + 48));

    for (unsigned int i = 0; i < Count / 64; i++) {
        __m128i a = _mm_load_si128(Source + (i * 4) + 0);
        __m128i b = _mm_load_si128(Source + (i * 4) + 1);
        __m128i c = _mm_load_si128(Source + (i * 4) + 2);
        __m128i d = _mm_load_si128(Source + (i * 4) + 3);

        a = _mm_shuffle_epi8(a, m0);
        b = _mm_shuffle_epi8(b, m1);
        c = _mm_shuffle_epi8(c, m2);
        d = _mm_shuffle_epi8(d, m3);

        __m128i e = _mm_or_si128(a, b);
        __m128i f = _mm_or_si128(c, d);
        __m128i g = _mm_or_si128(e, f);

        _mm_stream_si128(Destination + i, g);

    }
}
4

1 回答 1

6

会尝试:

    __asm {
        mov esi, 来源
        mov edi, 目的地
        mov ecx, 计数
        movdqu xmm0, xmmword ptr [esi]
        movdqa xmm4, xmmword ptr [掩码 + 0]
        movdqa xmm5, xmmword ptr [掩码 + 16]
        movdqa xmm6, xmmword ptr [掩码 + 32]
        movdqa xmm7, xmmword ptr [掩码 + 48]
l1:
        十二月
        lea edi, [ edi + 64 ]
        莉亚 esi,[ esi + 16 ]
        movdqa xmm1, xmm0
        movdqa xmm2, xmm0
        movdqa xmm3, xmm0
        pshufb xmm0, xmm4
        pshufb xmm1, xmm5
        pshufb xmm2, xmm6
        pshufb xmm3, xmm7
        movntdq [edi - 64], xmm0
        movntdq [edi - 48], xmm1
        movntdq [edi - 32], xmm2
        movntdq [edi - 16], xmm3
        movdqu xmm0, xmmword ptr [esi]
        ja l1
    }

虽然还没有对它进行基准测试;这些变化背后的假设:

  1. 延迟可以在movdqu xmm0,...循环中稍微隐藏一些(您的代码的负载xmm0直接跟在使用该寄存器中的值的指令之后)
  2. add两个 regs 上的opscmp并不是真的都是必要的;可以使用地址生成 ( ) 和/lea的隐式零测试。这样,就不会因为//上的操作而导致依赖关系,因为循环中唯一的 ALU 操作正在递减循环计数器。decjaEFLAGSecxesiedi

最后,在任何情况下这都可能是加载/存储绑定,因此算术是“免费游戏”;因此,即使有给定的论点,我也预计不会有什么不同。

如果输入很大,那么去掉“未对齐的头/尾”是有意义的,即为第一个/最后一个[0..15]字节做一个 duff 的设备,主循环使用movdqa.

编辑:

通过(GCC 4.7.1)运行您的内在源代码gcc -msse4.2 -O8 -c会提供以下程序集:

部分.text的反汇编:

0000000000000000 <ConvertGreyToRgb32Assembler>:
   0: 85 d2 测试 edx,edx
   2: 74 76 je 7a <ConvertGreyToRgb32Assembler+0x7a>
   4: 66 0f 6f 2d 00 00 00 00 movdqa xmm5,XMMWORD PTR [rip+0x0]
        # c <ConvertGreyToRgb32Assembler+0xc>
   c: 48 89 f8 mov rax,rdi
   f: 66 0f 6f 25 00 00 00 00 movdqa xmm4,XMMWORD PTR [rip+0x0]
        # 17 <ConvertGreyToRgb32Assembler+0x17>
  17: 66 0f 6f 1d 00 00 00 00 movdqa xmm3,XMMWORD PTR [rip+0x0]
        # 1f <ConvertGreyToRgb32Assembler+0x1f>
  1f: 66 0f 6f 15 00 00 00 00 movdqa xmm2,XMMWORD PTR [rip+0x0]
        # 27 <ConvertGreyToRgb32Assembler+0x27>
  27: 66 0f 1f 84 00 00 00 00 00 nop WORD PTR [rax+rax*1+0x0]
  30: f3 0f 6f 00 movdqu xmm0,XMMWORD PTR [rax]
  34: 48 89 f1 mov rcx,rsi
  37: 48 83 c0 10 添加 rax,0x10
  3b: 66 0f 6f c8 movdqa xmm1,xmm0
  3f: 66 0f 38 00 cd pshufb xmm1,xmm5
  44: 66 0f e7 0e movntdq XMMWORD PTR [rsi],xmm1
  48: 66 0f 6f c8 movdqa xmm1,xmm0
  4c: 66 0f 38 00 cc pshufb xmm1,xmm4
  51: 66 0f e7 4e 10 movntdq XMMWORD PTR [rsi+0x10],xmm1
  56: 66 0f 6f c8 movdqa xmm1,xmm0
  5a: 66 0f 38 00 c2 pshufb xmm0,xm​​m2
  5f: 66 0f 38 00 cb pshufb xmm1,xmm3
  64: 66 0f e7 4e 20 movntdq XMMWORD PTR [rsi+0x20],xmm1
  69: 66 0f e7 41 30 movntdq XMMWORD PTR [rcx+0x30],xmm0
  6e: 89 c1 mov ecx,eax
  70: 29 f9 子 ecx,edi
  72: 48 83 c6 40 添加 rsi,0x40
  76: 39 ca cmp edx,ecx
  78: 77 b6 ja 30 <ConvertGreyToRgb32Assembler+0x30>
  7a: f3 c3 repz ret

这让我非常强烈地想起了你最初的汇编代码。如果 MSVC 创建的东西比这更糟糕,我会说这是您使用的编译器(版本)中的错误/限制。

于 2012-08-17T12:29:51.197 回答