【问题标题】:Fast 24-bit array -> 32-bit array conversion?快速 24 位数组 -> 32 位数组转换?
【发布时间】:2011-02-27 18:11:59
【问题描述】:

快速总结:

我有一个 24 位值的数组。有关如何将单个 24 位数组元素快速扩展为 32 位元素的任何建议?

详情:

我正在使用 DirectX 10 中的像素着色器实时处理传入的视频帧。一个绊脚石是我的帧来自具有 24 位像素(作为 YUV 或 RGB 图像)的捕获硬件,但 DX10 需要32 位像素纹理。因此,我必须先将 24 位值扩展为 32 位,然后才能将它们加载到 GPU 中。

我真的不在乎我将剩余的 8 位设置为什么,或者传入的 24 位在该 32 位值中的位置 - 我可以在像素着色器中修复所有这些问题。但我需要快速完成从 24 位到 32 位的转换真的

我对 SIMD SSE 操作不是很熟悉,但粗略地看,我似乎无法使用它们进行扩展,因为我的读取和写入大小不同。有什么建议?还是我被卡住了按顺序按摩这个数据集?

这感觉非常愚蠢 - 我正在使用像素着色器进行并行处理,但在此之前我必须执行逐像素的顺序操作。我一定遗漏了一些明显的东西......

【问题讨论】:

  • 你确定你不能配置你的视频源来给你额外的填充字节吗?
  • 很好,马蒂。这是非常不幸的,我同意。 :(

标签: c bitmap bit-manipulation sse simd


【解决方案1】:

下面的代码应该很快。它在每次迭代中复制 4 个像素,仅使用 32 位读/写指令。源指针和目标指针应对齐为 32 位。

uint32_t *src = ...;
uint32_t *dst = ...;

for (int i=0; i<num_pixels; i+=4) {
    uint32_t sa = src[0];
    uint32_t sb = src[1];
    uint32_t sc = src[2];

    dst[i+0] = sa;
    dst[i+1] = (sa>>24) | (sb<<8);
    dst[i+2] = (sb>>16) | (sc<<16);
    dst[i+3] = sc>>8;

    src += 3;
}

编辑:

这是一种使用 SSSE3 指令 PSHUFB 和 ALIGNR 的方法。该代码是使用编译器内在函数编写的,但如果需要,将其转换为汇编应该不难。它在每次迭代中复制 16 个像素。源指针和目标指针必须对齐到 16 个字节,否则会出错。如果它们没有对齐,您可以通过将 _mm_load_si128 替换为 _mm_loadu_si128 并将 _mm_store_si128 替换为 _mm_storeu_si128 来使其工作,但这会更慢。

#include <emmintrin.h>
#include <tmmintrin.h>

__m128i *src = ...;
__m128i *dst = ...;
__m128i mask = _mm_setr_epi8(0,1,2,-1, 3,4,5,-1, 6,7,8,-1, 9,10,11,-1);

for (int i=0; i<num_pixels; i+=16) {
    __m128i sa = _mm_load_si128(src);
    __m128i sb = _mm_load_si128(src+1);
    __m128i sc = _mm_load_si128(src+2);

    __m128i val = _mm_shuffle_epi8(sa, mask);
    _mm_store_si128(dst, val);
    val = _mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask);
    _mm_store_si128(dst+1, val);
    val = _mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask);
    _mm_store_si128(dst+2, val);
    val = _mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask);
    _mm_store_si128(dst+3, val);

    src += 3;
    dst += 4;
}

SSSE3(不要与 SSE3 混淆)需要相对较新的处理器:Core 2 或更新版本,我相信 AMD 还不支持它。仅使用 SSE2 指令执行此操作将需要更多操作,并且可能不值得。

【讨论】:

  • 哇——谢谢 interjay!很高兴知道我弄错了,并且 SIMD 操作可以满足我的需要。 SSSE3 样本太棒了!我可以完全控制运行此代码的平台,并且可以将硬件选择限制为支持 SSSE3 的 proc。
  • 我想知道...第一个解决方案对大小端处理器有影响吗?
  • @TheQuantumPhysicist 它假定一个小端处理器。同样的想法也适用于大端,但需要改变转变。
  • 在现代 x86(Nehalem 或特别是 Haswell 及更高版本)上,使用未对齐的负载而不是 palignr 会更好。 Haswell 和之后的版本只有一个 shuffle 端口,因此您的代码将在每个时钟 1 个 shuffle 而不是每个时钟 1 个 store 上成为瓶颈。您的代码看起来非常适合 Core2(尤其是第二代 Core2,pshufb 速度很快),但不适用于 shuffle 吞吐量低但负载吞吐量出色的 CPU。
  • AMD Zen 2 和 Intel Ice Lake 现在有 2 个 shuffle 端口,因此平衡很可能已经转向 palignr 用于更新的处理器。
【解决方案2】:

SSE3 很棒,但对于那些因任何原因无法使用它的人来说,这里是 x86 汇编器中的转换,真正由您手动优化。为了完整起见,我给出了两个方向的转换:RGB32->RGB24 和 RGB24->RGB32。

请注意,interjay 的 C 代码会在目标像素的 MSB(Alpha 通道)中留下垃圾。这在某些应用程序中可能无关紧要,但对我来说很重要,因此我的 RGB24->RGB32 代码将 MSB 强制为零。同样,我的 RGB32->RGB24 代码忽略了 MSB;如果源数据具有非零 alpha 通道,这可以避免垃圾输出。经基准验证,这些功能在性能方面几乎没有成本。

对于 RGB32->RGB24,我能够击败 VC++ 优化器约 20%。对于 RGB24->RGB32,增益微不足道。基准测试是在 i5 2500K 上完成的。我在这里省略了基准测试代码,但如果有人想要它,我会提供它。最重要的优化是尽快碰撞源指针(参见 ASAP 注释)。我最好的猜测是,这通过允许指令流水线更快地预取来增加并行性。除此之外,我只是重新排序了一些指令,以减少依赖关系并使用 bit-bashing 重叠内存访问。

void ConvRGB32ToRGB24(const UINT *Src, UINT *Dst, UINT Pixels)
{
#if !USE_ASM
    for (UINT i = 0; i < Pixels; i += 4) {
        UINT    sa = Src[i + 0] & 0xffffff;
        UINT    sb = Src[i + 1] & 0xffffff;
        UINT    sc = Src[i + 2] & 0xffffff;
        UINT    sd = Src[i + 3];
        Dst[0] = sa | (sb << 24);
        Dst[1] = (sb >> 8) | (sc << 16);
        Dst[2] = (sc >> 16) | (sd << 8);
        Dst += 3;
    }
#else
    __asm {
        mov     ecx, Pixels
        shr     ecx, 2              // 4 pixels at once
        jz      ConvRGB32ToRGB24_$2
        mov     esi, Src
        mov     edi, Dst
ConvRGB32ToRGB24_$1:
        mov     ebx, [esi + 4]      // sb
        and     ebx, 0ffffffh       // sb & 0xffffff
        mov     eax, [esi + 0]      // sa
        and     eax, 0ffffffh       // sa & 0xffffff
        mov     edx, ebx            // copy sb
        shl     ebx, 24             // sb << 24
        or      eax, ebx            // sa | (sb << 24)
        mov     [edi + 0], eax      // Dst[0]
        shr     edx, 8              // sb >> 8
        mov     eax, [esi + 8]      // sc
        and     eax, 0ffffffh       // sc & 0xffffff
        mov     ebx, eax            // copy sc
        shl     eax, 16             // sc << 16
        or      eax, edx            // (sb >> 8) | (sc << 16)
        mov     [edi + 4], eax      // Dst[1]
        shr     ebx, 16             // sc >> 16
        mov     eax, [esi + 12]     // sd
        add     esi, 16             // Src += 4 (ASAP)
        shl     eax, 8              // sd << 8
        or      eax, ebx            // (sc >> 16) | (sd << 8)
        mov     [edi + 8], eax      // Dst[2]
        add     edi, 12             // Dst += 3
        dec     ecx
        jnz     SHORT ConvRGB32ToRGB24_$1
ConvRGB32ToRGB24_$2:
    }
#endif
}

void ConvRGB24ToRGB32(const UINT *Src, UINT *Dst, UINT Pixels)
{
#if !USE_ASM
    for (UINT i = 0; i < Pixels; i += 4) {
        UINT    sa = Src[0];
        UINT    sb = Src[1];
        UINT    sc = Src[2];
        Dst[i + 0] = sa & 0xffffff;
        Dst[i + 1] = ((sa >> 24) | (sb << 8)) & 0xffffff;
        Dst[i + 2] = ((sb >> 16) | (sc << 16)) & 0xffffff;
        Dst[i + 3] = sc >> 8;
        Src += 3;
    }
#else
    __asm {
        mov     ecx, Pixels
        shr     ecx, 2              // 4 pixels at once
        jz      SHORT ConvRGB24ToRGB32_$2
        mov     esi, Src
        mov     edi, Dst
        push    ebp
ConvRGB24ToRGB32_$1:
        mov     ebx, [esi + 4]      // sb
        mov     edx, ebx            // copy sb
        mov     eax, [esi + 0]      // sa
        mov     ebp, eax            // copy sa
        and     ebx, 0ffffh         // sb & 0xffff
        shl     ebx, 8              // (sb & 0xffff) << 8
        and     eax, 0ffffffh       // sa & 0xffffff
        mov     [edi + 0], eax      // Dst[0]
        shr     ebp, 24             // sa >> 24
        or      ebx, ebp            // (sa >> 24) | ((sb & 0xffff) << 8)
        mov     [edi + 4], ebx      // Dst[1]
        shr     edx, 16             // sb >> 16
        mov     eax, [esi + 8]      // sc
        add     esi, 12             // Src += 12 (ASAP)
        mov     ebx, eax            // copy sc
        and     eax, 0ffh           // sc & 0xff
        shl     eax, 16             // (sc & 0xff) << 16
        or      eax, edx            // (sb >> 16) | ((sc & 0xff) << 16)
        mov     [edi + 8], eax      // Dst[2]
        shr     ebx, 8              // sc >> 8
        mov     [edi + 12], ebx     // Dst[3]
        add     edi, 16             // Dst += 16
        dec     ecx
        jnz     SHORT ConvRGB24ToRGB32_$1
        pop     ebp
ConvRGB24ToRGB32_$2:
    }
#endif
}

虽然我们正在研究它,但这里是实际 SSE3 汇编中的相同转换。这只适用于你有一个汇编器(FASM 是免费的)并且有一个支持 SSE3 的 CPU(可能但最好检查一下)。请注意,内在函数不一定会输出如此高效的东西,它完全取决于您使用的工具以及您正在编译的平台。在这里,很简单:所见即所得。此代码生成与上述 x86 代码相同的输出,并且速度提高了约 1.5 倍(在 i5 2500K 上)。

format MS COFF

section '.text' code readable executable

public _ConvRGB32ToRGB24SSE3

;   ebp + 8     Src (*RGB32, 16-byte aligned)
;   ebp + 12    Dst (*RGB24, 16-byte aligned)
;   ebp + 16    Pixels

_ConvRGB32ToRGB24SSE3:
    push    ebp
    mov     ebp, esp
    mov     eax, [ebp + 8]
    mov     edx, [ebp + 12]
    mov     ecx, [ebp + 16]
    shr     ecx, 4
    jz      done1
    movupd  xmm7, [mask1]

top1:
    movupd  xmm0, [eax + 0]     ; sa = Src[0]
    pshufb  xmm0, xmm7          ; sa = _mm_shuffle_epi8(sa, mask)
    movupd  xmm1, [eax + 16]    ; sb = Src[1]
    pshufb  xmm1, xmm7          ; sb = _mm_shuffle_epi8(sb, mask)
    movupd  xmm2, xmm1          ; sb1 = sb
    pslldq  xmm1, 12            ; sb = _mm_slli_si128(sb, 12)
    por     xmm0, xmm1          ; sa = _mm_or_si128(sa, sb)
    movupd  [edx + 0], xmm0     ; Dst[0] = sa
    psrldq  xmm2, 4             ; sb1 = _mm_srli_si128(sb1, 4)
    movupd  xmm0, [eax + 32]    ; sc = Src[2]
    pshufb  xmm0, xmm7          ; sc = _mm_shuffle_epi8(sc, mask)
    movupd  xmm1, xmm0          ; sc1 = sc
    pslldq  xmm0, 8             ; sc = _mm_slli_si128(sc, 8)
    por     xmm0, xmm2          ; sc = _mm_or_si128(sb1, sc)
    movupd  [edx + 16], xmm0    ; Dst[1] = sc
    psrldq  xmm1, 8             ; sc1 = _mm_srli_si128(sc1, 8)
    movupd  xmm0, [eax + 48]    ; sd = Src[3]
    pshufb  xmm0, xmm7          ; sd = _mm_shuffle_epi8(sd, mask)
    pslldq  xmm0, 4             ; sd = _mm_slli_si128(sd, 4)
    por     xmm0, xmm1          ; sd = _mm_or_si128(sc1, sd)
    movupd  [edx + 32], xmm0    ; Dst[2] = sd
    add     eax, 64
    add     edx, 48
    dec     ecx
    jnz     top1

done1:
    pop     ebp
    ret

public _ConvRGB24ToRGB32SSE3

;   ebp + 8     Src (*RGB24, 16-byte aligned)
;   ebp + 12    Dst (*RGB32, 16-byte aligned)
;   ebp + 16    Pixels

_ConvRGB24ToRGB32SSE3:
    push    ebp
    mov     ebp, esp
    mov     eax, [ebp + 8]
    mov     edx, [ebp + 12]
    mov     ecx, [ebp + 16]
    shr     ecx, 4
    jz      done2
    movupd  xmm7, [mask2]

top2:
    movupd  xmm0, [eax + 0]     ; sa = Src[0]
    movupd  xmm1, [eax + 16]    ; sb = Src[1]
    movupd  xmm2, [eax + 32]    ; sc = Src[2]
    movupd  xmm3, xmm0          ; sa1 = sa
    pshufb  xmm0, xmm7          ; sa = _mm_shuffle_epi8(sa, mask)
    movupd  [edx], xmm0         ; Dst[0] = sa
    movupd  xmm4, xmm1          ; sb1 = sb
    palignr xmm1, xmm3, 12      ; sb = _mm_alignr_epi8(sb, sa1, 12)
    pshufb  xmm1, xmm7          ; sb = _mm_shuffle_epi8(sb, mask);
    movupd  [edx + 16], xmm1    ; Dst[1] = sb
    movupd  xmm3, xmm2          ; sc1 = sc
    palignr xmm2, xmm4, 8       ; sc = _mm_alignr_epi8(sc, sb1, 8)
    pshufb  xmm2, xmm7          ; sc = _mm_shuffle_epi8(sc, mask)
    movupd  [edx + 32], xmm2    ; Dst[2] = sc
    palignr xmm3, xmm3, 4       ; sc1 = _mm_alignr_epi8(sc1, sc1, 4)
    pshufb  xmm3, xmm7          ; sc1 = _mm_shuffle_epi8(sc1, mask)
    movupd  [edx + 48], xmm3    ; Dst[3] = sc1
    add     eax, 48
    add     edx, 64
    dec     ecx
    jnz     top2

done2:
    pop     ebp
    ret

section '.data' data readable writeable align 16

label mask1 dqword 
    db  0,1,2,4, 5,6,8,9, 10,12,13,14, -1,-1,-1,-1
label mask2 dqword 
    db  0,1,2,-1, 3,4,5,-1, 6,7,8,-1, 9,10,11,-1

【讨论】:

    【解决方案3】:

    不同的输入/输出大小不是使用 simd 的障碍,只是减速带。您需要对数据进行分块,以便以完整的 simd 字(16 字节)读取和写入。

    在这种情况下,您将读取 3 个 SIMD 字(48 字节 == 16 rgb 像素),进行扩展,然后写入 4 个 SIMD 字。

    我只是说你可以使用 SIMD,我不是说你应该。中间部分,即扩展,仍然很棘手,因为在单词的不同部分有不统一的移位大小。

    【讨论】:

    • 谢谢 Mark - 很高兴知道我弄错了,并且 SIMD 操作可以满足我的需要。它可能不会更快,但值得我看看以确保。 :)
    【解决方案4】:

    SSE 4.1.ASM:

    PINSRD  XMM0,  DWORD PTR[ESI],   0
    PINSRD  XMM0,  DWORD PTR[ESI+3], 1
    PINSRD  XMM0,  DWORD PTR[ESI+6], 2
    PINSRD  XMM0,  DWORD PTR[ESI+9], 3
    PSLLD   XMM0,  8                    
    PSRLD   XMM0,  8
    MOVNTDQ [EDI], XMM1
    add     ESI,   12
    add     EDI,   16
    

    【讨论】:

    • 这大约是 interjays 答案的 4 倍。
    猜你喜欢
    • 2012-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-08
    • 1970-01-01
    • 2014-12-10
    • 1970-01-01
    相关资源
    最近更新 更多