2 个 AVX-512 向量元素的交错合并 - C 内在

Posted

技术标签:

【中文标题】2 个 AVX-512 向量元素的交错合并 - C 内在【英文标题】:Interleaved merging of 2 AVX-512 vector elements - C intrinsic 【发布时间】:2020-09-10 03:04:00 【问题描述】:

我想以尽可能少的时钟周期数将 2 个 AVX-512 向量的元素合并到另外两个向量中。

具体问题如下:

// inputs
__m512i a = a0, a1, ..., a31; // 32x 16-bit int16_t integers
__m512i b = b0, b1, ..., b31; // 32x 16-bit int16_t integers

// desired output
__m512i A = a0 , b0 , a1 , b1 , ..., a15, b15;
__m512i B = a16, b16, a17, b17, ..., a31, b31;

天真的方法是将向量(a和b)复制到内存并通过直接索引创建向量(A和B),如下所示:

union U512i 
    __m512i vec;
    alignas(64) int16_t vals[32];
;

U512i ta =  a ;
U512i tb =  b 

U512i A = _mm512_set_epi16( tb.vals[15], ta.vals[15], ... tb.vals[0], ta.vals[0] );
U512i B = _mm512_set_epi16( tb.vals[31], ta.vals[31], ... tb.vals[16], ta.vals[16] );

我还需要进行类似的合并,但步幅不同,例如:

// inputs
__m512i a = a0, a1, ..., a31; // 32x 16-bit int16_t integers
__m512i b = b0, b1, ..., b31; // 32x 16-bit int16_t integers

// desired output
__m512i A = a0 , a1 , b0 , b1 , ..., a14, a15, b14, b15;
__m512i B = a16, a17, b16, b17, ..., a30, a31, b30, b31;

最适合解决此问题的 AVX-512 内在函数是什么?一些解释将不胜感激,因为我是 AVX-512 内在函数的新手。

感谢您的帮助!

【问题讨论】:

你看过_mm512_mask_blend_epi16和一些洗牌吗? vpermt2w 可以在每个输出的一条指令中执行此操作。或者在一些成本为 3 uop 的 CPU 上,vpunpcklwd + vpunpckhwd 然后用 2x 单 uop vpermt2d 修复该通道内交错,这些结果应该总共可以工作 4 个 shuffle uop 而不是 6 个。 保持pairs相邻的版本相当于32位元素粒度,所以可以只使用单uopvpermt2d @PeterCordes,感谢您的建议。我的 CPU (Skylake) 支持 vpermt2w。虽然 _mm512_mask_permutex2var_epi16 解决了我的问题,但它有点慢(7 个周期)。事实上,与幼稚的方式(通过转移到内存)相比,性能几乎保持不变。 vpermt2w 是 3 uop,在 SKX 上的 吞吐量 为每 2 个周期 1 个。是的,这并不理想,但是产生 A 和 B 的两个独立 shuffle 的延迟可能会重叠。 uops.info/…你确定像clang这样的编译器还没有把你正在做的事情编译成这样的洗牌吗?除非您的基准测试设计不佳,或者您的实际用例瓶颈在其他地方,或者您的编译器已经很好地优化了您的幼稚方式,否则这里应该有空间。 【参考方案1】:

感谢上面提到的 cmets,解决这个问题的一种方法是使用 vpermt2w 或内在的 _mm512_mask_permutex2var_epi16

在 Skylake-avx512 和 Ice Lake CPU (https://uops.info/) 上,vpermt2w 解码为 3 微指令(其中 2 微指令只能在端口 5 上运行)。总体而言,它有 7 个周期的延迟,每 2 个周期的吞吐量为 1 个。

使用vpermt2w的优化代码如下:

#include <immintrin.h>
#include <inttypes.h>

void foo(__m512i a, __m512i b) 

    __m512i A, B;
    __m512i idx1 = _mm512_set_epi16( 47, 15, 46, 14, 45, 13, 44, 12, 43, 11, 42, 10, 41, 9, 40, 8, 39, 7, 38, 6, 37, 5, 36, 4, 35, 3, 34, 2, 33, 1, 32, 0 );
    __m512i idx2 = _mm512_set_epi16(
        47 + 16, 15 + 16, 46 + 16, 14 + 16, 45 + 16, 13 + 16, 44 + 16, 12 + 16, 43 + 16, 11 + 16, 42 + 16, 10 + 16, 41 + 16, 9 + 16, 40 + 16, 8 + 16,
        39 + 16, 7 + 16, 38 + 16, 6 + 16, 37 + 16, 5 + 16, 36 + 16, 4 + 16, 35 + 16, 3 + 16, 34 + 16, 2 + 16, 33 + 16, 1 + 16, 32 + 16, 0 + 16 );

    A = _mm512_mask_permutex2var_epi16( a, 0xFFFFFFFF, idx1, b );
    B = _mm512_mask_permutex2var_epi16( a, 0xFFFFFFFF, idx2, b );

这里展示了简单的方法以供参考,但是对于不是编译时常量的输入向量,它使用 GCC 编译的效率非常

#include <immintrin.h>
#include <inttypes.h>

union U512i 
    __m512i vec;
    alignas(64) int16_t vals[32];
;

void foo(__m512i a, __m512i b) 

    __m512i A, B;

    U512i u_a =  a ;
    U512i u_b =  b ;
    A = _mm512_set_epi16 (
            u_b.vals[15], u_a.vals[15], u_b.vals[14], u_a.vals[14],
            u_b.vals[13], u_a.vals[13], u_b.vals[12], u_a.vals[12],
            u_b.vals[11], u_a.vals[11], u_b.vals[10], u_a.vals[10],
            u_b.vals[9], u_a.vals[9], u_b.vals[8], u_a.vals[8],
            u_b.vals[7], u_a.vals[7], u_b.vals[6], u_a.vals[6],
            u_b.vals[5], u_a.vals[5], u_b.vals[4], u_a.vals[4],
            u_b.vals[3], u_a.vals[3], u_b.vals[2], u_a.vals[2],
            u_b.vals[1], u_a.vals[1], u_b.vals[0], u_a.vals[0]
            );

    B = _mm512_set_epi16 (
            u_b.vals[31], u_a.vals[31], u_b.vals[30], u_a.vals[30],
            u_b.vals[29], u_a.vals[29], u_b.vals[28], u_a.vals[28],
            u_b.vals[27], u_a.vals[27], u_b.vals[26], u_a.vals[26],
            u_b.vals[25], u_a.vals[25], u_b.vals[24], u_a.vals[24],
            u_b.vals[23], u_a.vals[23], u_b.vals[22], u_a.vals[22],
            u_b.vals[21], u_a.vals[21], u_b.vals[20], u_a.vals[20],
            u_b.vals[19], u_a.vals[19], u_b.vals[18], u_a.vals[18],
            u_b.vals[17], u_a.vals[17], u_b.vals[16], u_a.vals[16]
            );


【讨论】:

vpermt2w 的延迟为 7 个周期,但您的用例具有指令级并行性。指令在您可以加起来的周期中没有单一的成本,这不是性能在乱序执行 CPU 上的工作方式。此外,它在 Ice Lake 上更快,仍然是 3 微秒,7 个周期延迟。 @PeterCordes。同意 ILP 和 CPI。根据this,延迟显示为“-”,你知道这是什么意思吗? 这意味着英特尔的内在函数指南并没有像往常一样详细地进行实际性能分析。它主要只有单指令指令的真实信息。或者可能是因为该内在函数可以编译为vpermi2wvpermt2w;内在函数指南也不会尝试显示与 asm 没有精确 1:1 映射的内在函数的性能信息。这个总是一个或另一个(除非不断传播将其删除或优化为其他内容),但这也许是英特尔留下他们的表格不完整的部分原因。 TL:DR:英特尔指南不是性能分析的好来源

以上是关于2 个 AVX-512 向量元素的交错合并 - C 内在的主要内容,如果未能解决你的问题,请参考以下文章

将一个向量交错和去交错成两个新向量

有没有办法用 AVX2 编写 _mm256_shldi_epi8(a,b,1) ? (向量之间每 8 位元素移位一位)

使用 AVX512 或 AVX2 计算所有压缩 32 位整数之和的最快方法

matlab - 如何合并/交错 2 个矩阵?

C++ 合并和排序 2 个向量

当使用带有 AVX-512 加载和存储的屏蔽寄存器时,是不是会因对屏蔽元素的无效访问而引发错误?