使用 SIMD,我如何有条件地仅移动 alpha 通道值为 255 的像素?

Posted

技术标签:

【中文标题】使用 SIMD,我如何有条件地仅移动 alpha 通道值为 255 的像素?【英文标题】:Using SIMD, how do I conditionally move only the pixels with an alpha channel value of 255? 【发布时间】:2020-10-12 00:04:16 【问题描述】:

我目前正在使用 AVX2 内部函数对一些代码进行矢量化以存储 32 位像素数据。由于 AVX2 寄存器是 256 位的,我可以同时对 8 个像素进行操作。我目前的代码可以从一个缓冲区加载 8 个像素,然后将它们存储到另一个缓冲区:

// Load 256 bits (8 pixels) from memory into register YMMx           
BitmapOctoPixel = _mm256_load_si256((const __m256i*)((PIXEL32*)GameBitmap->Memory + BitmapOffset));

// adjust the colors

// As an example, the YMM0 register currently holds these pixels:
//        AARRGGBBAARRGGBB-AARRGGBBAARRGGBB-AARRGGBBAARRGGBB-AARRGGBBAARRGGBB
// YMM0 = FF33281EFF000000-FF33281E00FFFFFF-00FFFFFF00FFFFFF-00FFFFFF00FFFFFF

// store the result into the destination buffer
_mm256_store_si256((__m256i*)((PIXEL32*)gBackBuffer.Memory + MemoryOffset), BitmapOctoPixel);

现在我只想移动 Alpha 通道(“AA”组件)为 255 的像素。我不想进行 Alpha 混合。我只想将具有 0xFF 的像素存储为 alpha 值。

我想我可以使用掩码和_mm256_maskstore_epi32() 函数来做到这一点,但经过几个小时的尝试后我还没有弄明白。

谢谢

【问题讨论】:

【参考方案1】:

首先,请注意_mm256_maskstore_epi32 在 AMD Zen / Zen2 上的速度非常慢,例如 19 uop 和每 6 周期 1 次的吞吐量。 (https://uops.info/)。掩码加载很好,但掩码存储仅在英特尔硬件上有效。您可能希望与原始值混合并执行完整的向量存储。


maskstore 使用 32 位元素的高位作为存储与否的控制。 因此,当 alpha 正好 == 0xFF 时,您需要创建一个设置该位的向量。

方便地,8 位 alpha 已经在 32 位元素的顶部,因此它的高位是整个 32 位元素的控制位。根据整个 alpha 字节为0xFF,我们可以只使用 packed-8-bit 比较来将 alpha 通道的所有位(包括高位)设置为 0 或 1maskstore 根本不关心掩码中的其他位,因此像素其他部分的 8 位比较结果基本上是垃圾也没关系。


void store_opaque_only(void *dst, __m256i pixels)

// As an example, the YMM0 register currently holds these pixels:
//        AARRGGBBAARRGGBB-AARRGGBBAARRGGBB-AARRGGBBAARRGGBB-AARRGGBBAARRGGBB
// YMM0 = FF33281EFF000000-FF33281E00FFFFFF-00FFFFFF00FFFFFF-00FFFFFF00FFFFFF

    __m256i opaque = _mm256_cmpeq_epi8(pixels, _mm256_set1_epi8(-1));
    _mm256_maskstore_epi32(dst, opaque, pixels);

set1_epi8(-1) 而不是set1_epi32(0xFF000000) 使常量的创建成本更低:编译器可以通过将寄存器与自身进行比较来创建全1,而不是从内存中加载常量。 (Godbolt;当然这个函数会在实际用例中内联。)

# gcc10.2 -O3 -march=skylake
store_opaque_only:
    vpcmpeqd        ymm1, ymm1, ymm1           # all-ones
    vpcmpeqb        ymm1, ymm0, ymm1           # opaque =  pixels == -1
    vpmaskmovd      YMMWORD PTR [rdi], ymm1, ymm0
    ret

内联后,全1向量可以被提升出循环。


如果您不需要完全相等,例如alpha >= 0xF0,您可能必须在 vpcmpgtb _mm256_cmpgt_epi8 之前将范围转移到有符号(通过减去或异或 0x80)。调整后,您可以进行 dword 整数比较以创建 32 位掩码元素,因此您可以将其与 vpblendvb(整数字节混合)一起使用。

如果 alpha 位于 32 位元素中的不同位置,则在比较之前左移。

顺便说一句,如果您将像素存储回找到它们的位置,您还可以考虑将vblendvps 与常规存储之前的原始数据一起使用,而不是使用 maskstore。

没有 32 位粒度的整数混合,因此您必须 _mm256_castsi256_ps 让编译器对在 __m256i 变量上使用 _mm256_blendv_ps 感到满意。

在大多数 CPU 上,FP 混合将花费额外的一个或 2 个绕过延迟周期,但只要 OoO exec 可以隐藏该延迟,就不会产生吞吐量成本,这在您处理独立的像素向量时很可能发生。但是这样做可以节省指令而不是 vpxor / vpcmpgtd 来设置 vpblendvb

避免使用 maskstore 在 AMD 上非常好。

【讨论】:

谢谢。这很好用,完全符合我的要求。【参考方案2】:

我不确定这是否完全回答了您的问题,但这种比较将与__m256_maskstore_epi32() 兼容,我假设out_ptr 指向您要存储到的位置:

// As an example, the YMM0 register currently holds these pixels:
//        AARRGGBBAARRGGBB-AARRGGBBAARRGGBB-AARRGGBBAARRGGBB-AARRGGBBAARRGGBB
// YMM0 = FF33281EFF000000-FF33281E00FFFFFF-00FFFFFF00FFFFFF-00FFFFFF00FFFFFF

// compare every 8-bit value against 0xFF; for pixels that have this value in their alpha
// channel, the corresponding byte in alpha_mask will be 0xFF
__m256i mask = _mm256_cmpeq_epi8(BitmapOctoPixel, _mm256_set1_epi8(0xFF));
// now, you can use the masked store directly; the high bit in each 32-bit pixel is used
// to determine whether to do the store
__m256_maskstore_si256((__m256i *) out_ptr, mask, BitmapOctoPixel);

但是,这会在输出缓冲区中留下没有0xFF alpha 值的像素的间隙。那是你要的吗?还是要连续存储所有通过测试的像素?在这种情况下,您可能需要 AVX512 中的 _mm256_mask_compressstoreu_epi32() 效果,这需要在 AVX2 中进行更多的模拟工作。

【讨论】:

以上是关于使用 SIMD,我如何有条件地仅移动 alpha 通道值为 255 的像素?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以有条件地仅在 React 的输入标签中呈现占位符属性?

OPENGL:如何使用 blit 将 alpha 移动到红色通道

Cortex-M4 SIMD 比 Scalar 慢

使用 Mono.Simd SSE 指令进行流控制

如何使用 SIMD 指令截断值

如何让 VC 编译器使用 SIMD 更好地优化我的代码?