用于灰度到 ARGB 转换的 C++ SSE2 或 AVX2 内在函数

Posted

技术标签:

【中文标题】用于灰度到 ARGB 转换的 C++ SSE2 或 AVX2 内在函数【英文标题】:C++ SSE2 or AVX2 intrinsics for grayscale to ARGB conversion 【发布时间】:2016-11-08 22:04:54 【问题描述】:

我想知道是否要执行 SSE2/AVX2 整数指令或指令序列(或内在函数)以实现以下结果:

给定一行 8 字节像素的表格:

A = a, b, c, d, e, f, g, h

有没有办法将这些像素加载到包含 8 个 32 位 ARGB 像素的 YMM 寄存器中,以便将初始灰度值广播到每个对应的 32 位像素的其他 2 个字节?结果应该是这样的:(0 是 alpha 值)

B = 0aaa, 0bbb, 0ccc, 0ddd, 0eee, 0fff, 0ggg, 0hhh

我是矢量扩展的初学者,所以我什至不知道如何解决这个问题,或者是否有可能。

任何帮助将不胜感激。谢谢!

更新1

感谢您的回答。不过我还是有问题:

我把这个小例子放在一起,在 x64 上用 VS2015 编译。

int main()

    unsigned char* pixels = (unsigned char*)_aligned_malloc(64, 32);
    memset(pixels, 0, 64);

    for (unsigned char i = 0; i < 8; i++)
        pixels[i] = 0xaa + i;

    __m128i grayscalePix = _mm_load_si128((const __m128i*)pixels);
    __m256i rgba = _mm256_cvtepu8_epi32(grayscalePix);
    __m256i mulOperand = _mm256_set1_epi32(0x00010101);

    __m256i result = _mm256_mullo_epi32(rgba, mulOperand);

   _aligned_free(pixels);
    return 0;

问题是做了之后

__m256i rgba = mm256_cvtepu8_epi32(grayscalePix)

rgba 只设置了前四个双字。最后四个都是0。

英特尔开发人员手册说:

VPMOVZXBD ymm1, xmm2/m64 零扩展低 8 位中的 8 个压缩 8 位整数 xmm2/m64 字节到 8 个压缩的 32 位整数 ymm1.

我不确定这是预期的行为还是我仍然缺少某些东西。

谢谢。

【问题讨论】:

您的代码看起来不错。你确定你不只是测试错了吗?或者编译器没有优化部分/全部,因为结果未被使用? On Godbolt,我不得不使用-O0 让编译器保留向量操作。甚至 -Og-O1 也优化了除 malloc/free 之外的所有内容。尝试将向量存储到 uint32_t 数组中并使用 printf 或 C++ish 进行打印。 优化器不是问题,因为我在调试模式下测试它,但你仍然是对的 :) 显然,VS 调试器没有正确显示 _m256i 值。几乎感觉就像它在_m128i 边界处截断了它们。此外,寄存器窗口也没有太大帮助。在我将向量存储到内存并执行printf 之后,一切看起来都很好,所以我想谢谢你 :) 哦,哇,当您无法信任调试器时,情况就很糟糕了!当您使用结果时,调试器是否会变得更好? 我不再费心在调试器中查看_m256i 值了。当我需要测试我的代码的正确性时,我使用#ifdef _DEBUG 代码,我只是将所有内容复制到内存并在那里查看。 【参考方案1】:

更新:@chtz 的回答是一个更好的主意,使用廉价的 128->256 广播负载而不是 vpmovzx 来馈送 vpshufb,从而节省 shuffle 端口带宽。


按照 Mark 的建议从 PMOVZX 开始。

但在那之后,PSHUFB (_mm256_shuffle_epi8) 将比 PMULLD 快得多,只是它与 PMOVZX 竞争 shuffle 端口。 (而且它在车道上运行,所以你仍然需要 PMOVZX)。

因此,如果您只关心吞吐量,而不关心延迟,那么_mm256_mullo_epi32 很好。但是,如果延迟很重要,或者如果您的吞吐量瓶颈不是每个向量 2 个 shuffle-port 指令,那么 PSHUFB 复制每个像素内的字节应该是最好的。

实际上,即使是吞吐量,_mm256_mullo_epi32 在 HSW 和 BDW 上也很糟糕:对于 p0 来说是 2 uops(10c 延迟),所以对于一个端口来说是 2 uops。

在 SKL 上,p01 为 2 微秒(10c 延迟),因此它可以维持与 VPMOVZXBD 相同的每个时钟吞吐量。但这是一个额外的 1 uop,使其更有可能成为瓶颈。

(在所有支持 AVX2 的 Intel CPU 上,对于端口 5,VPSHUFB 为 1 uop,1c 延迟。)

【讨论】:

【参考方案2】:

您可以将打包的字节加载到寄存器中, call __m256i _mm256_cvtepu8_epi32 (__m128i a) 转换为 32 位值,然后乘以 0x00010101 将灰度复制到 R、G 和 B 中。

【讨论】:

pshufb 通常会比乘法更好。看我的回答。【参考方案3】:

您可以使用一个vbroadcasti128 和两个vpshufb 转换16 像素。广播不需要端口 5,如果它直接从内存加载,那么 shuffle 可以充分利用该端口(它仍然会在该端口或存储回内存时成为瓶颈)。

void gray2rgba(char const* input, char* output, size_t length)

    length &= size_t(-16); // lets just care about sizes multiples of 16 here ...

    __m256i shuflo = _mm256_setr_epi32(
        0x80000000, 0x80010101, 0x80020202, 0x80030303,
        0x80040404, 0x80050505, 0x80060606, 0x80070707
    );
    __m256i shufhi = _mm256_setr_epi32(
        0x80080808, 0x80090909, 0x800a0a0a, 0x800b0b0b,
        0x800c0c0c, 0x800d0d0d, 0x800e0e0e, 0x800f0f0f
    );

    for(size_t i=0; i<length; i+=16)
    
        __m256i in = _mm256_broadcastsi128_si256(*reinterpret_cast<const __m128i*>(input+i));
        __m256i out0 = _mm256_shuffle_epi8(in, shuflo);
        __m256i out1 = _mm256_shuffle_epi8(in, shufhi);
        _mm256_storeu_si256(reinterpret_cast<__m256i*>(output+4*i),    out0);
        _mm256_storeu_si256(reinterpret_cast<__m256i*>(output+4*i+32), out1);
    

Godbolt 演示:https://godbolt.org/z/dUx6GZ

【讨论】:

以上是关于用于灰度到 ARGB 转换的 C++ SSE2 或 AVX2 内在函数的主要内容,如果未能解决你的问题,请参考以下文章

Visual C++ (x64) 中的 SSE2 选项

如何将 Argb32 加载到特征矩阵中以获得最佳性能?

如何设置一个获取灰度图像并输出 ARGB 的图层,使其中一种灰度颜色透明?

如何将 A16B16G16R16F 转换为 ARGB32?

NEON:如何将 128 位 ARGB 转换为具有饱和度的 32 位 ARGB?

使用opencv将图像从BGR转换为ARGB