如何使用 AVX2 有效地连接两个向量? (VPALIGNR 的跨车道版本)

How to concatenate two vector efficiently using AVX2? (a lane-crossing version of VPALIGNR)

我实现了一个内联函数 (_mm256_concat_epi16)。它连接两个包含 16 位值的 AVX2 向量。它适用于前 8 个数字。如果我想将它用于向量的其余部分,我应该更改实现。但是在我的主程序中使用单个内联函数会更好。

问题是:有没有比我的更好的解决方案或任何建议使这个内联函数更通用,适用于 16 个值 而不是我的适用于 8 个值 的解决方案?我的解决方案连接了 2 个向量,但只解决了 16 种可能状态中的 8 种状态。

**编辑*我目前对这个问题的解决方案是使用未对齐的加载函数,它可以从内存中的任何部分读取。但是,当数据在寄存器中准备就绪时,重用它可能会更好。但是,它可能会导致端口 5 出现瓶颈,从而导致随机播放、置换等问题。但是吞吐量可能足够了(尚未测试)。

#include <stdio.h>
#include <x86intrin.h>

inline _mm256_print_epi16(__m256i a, char* name){
    short temp[16], i;
    _mm256_storeu_si256((__m256i *) &temp[0], a);
    for(i=0; i<16; i++)
        printf("%s[%d]=%4d , ",name,i+1,temp[i]);
    printf("\n");
}

inline __m256i _mm256_concat_epi16(__m256i a, __m256i  b, const int indx){
    return _mm256_alignr_epi8(_mm256_permute2x128_si256(a,b,0x21),a,indx*2);
}

int main()
{
    __m256i a = _mm256_setr_epi16(101,102,103,104,105,106,107,108,109,1010,1011,1012,1013,1014,1015,1016);_mm256_print_epi16(a, "a");
    __m256i b = _mm256_setr_epi16(201,202,203,204,205,206,207,208,209,2010,2011,2012,2013,2014,2015,2016);_mm256_print_epi16(b, "b");

    _mm256_print_epi16(_mm256_concat_epi16(a,b,8), "c");//numbers: 0-8
    return 0;
}

输出是:

// icc  -march=native -O3 -D _GNU_SOURCE -o "concat" "concat.c"
[fedora@localhost concatination]$ "./concat"
a[1]= 101 , a[2]= 102 , a[3]= 103 , a[4]= 104 , a[5]= 105 , a[6]= 106 , a[7]= 107 , a[8]= 108 , a[9]= 109 , a[10]=1010 , a[11]=1011 , a[12]=1012 , a[13]=1013 , a[14]=1014 , a[15]=1015 , a[16]=1016 , 
b[1]= 201 , b[2]= 202 , b[3]= 203 , b[4]= 204 , b[5]= 205 , b[6]= 206 , b[7]= 207 , b[8]= 208 , b[9]= 209 , b[10]=2010 , b[11]=2011 , b[12]=2012 , b[13]=2013 , b[14]=2014 , b[15]=2015 , b[16]=2016 , 
c[1]= 109 , c[2]=1010 , c[3]=1011 , c[4]=1012 , c[5]=1013 , c[6]=1014 , c[7]=1015 , c[8]=1016 , c[9]= 201 , c[10]= 202 , c[11]= 203 , c[12]= 204 , c[13]= 205 , c[14]= 206 , c[15]= 207 , c[16]= 208 , 

这个问题不可能给出一个笼统的答案。这是一个如此短的片段,最佳策略取决于周围的代码以及 CPU 你 运行 在做什么。

有时我们可以排除对任何 CPU 没有优势的事情,只是消耗更多相同的资源,但在考虑未对齐加载与随机播放之间的权衡时,情况并非如此。


在一个可能未对齐的输入数组的循环中,您最好不要使用未对齐的加载。特别是您的输入数组大部分时间都会在运行时对齐。如果不是,这是一个问题,那么如果可能的话,做一个未对齐的第一个向量,然后从第一个对齐边界对齐。 IE。到达主循环对齐边界的序言的常用技巧。但是对于多个指针,如果您的指针彼此未对齐,通常最好对齐您的存储指针,并执行未对齐的加载(根据英特尔的优化手册)。 (参见 Agner Fog's optimization guides and other links in the 标签 wiki。)

在最近的 Intel CPUs 上,跨越高速缓存行边界的矢量加载仍然具有相当不错的吞吐量,但这就是您可能考虑 ALU 策略或混合混洗和重叠的原因之一负载(在展开的循环中,您可以交替使用策略,这样您就不会在任何一个上遇到瓶颈)。


正如 Stephen Canon 在 _mm_alignr_epi8 (PALIGNR) equivalent in AVX2(一个可能的副本),如果你需要几个不同的偏移量 windows 到两个向量的相同连接中,那么两个存储 + 重复的未对齐加载是非常好的。在 Intel CPUs 上,只要 256b 未对齐加载不跨越高速缓存行边界(因此 alignas(64) 您的缓冲区),您将获得每时钟 2 个吞吐量。

但是,

Store/reload 对于单一用例来说并不是很好,因为对于没有完全包含在任一存储中的负载,存储转发失败。它的吞吐量仍然很便宜,但延迟很昂贵。另一个巨大的优势是它在使用运行时变量偏移量时非常高效。

如果延迟是一个问题,使用 ALU 洗牌可能会很好(尤其是在 Intel 上,通道交叉洗牌并不比通道内洗牌贵很多)。再一次,考虑/衡量你的循环瓶颈是什么,或者只是尝试 store/reload 与 ALU。


洗牌策略:

只有在编译时已知 indx 时,您当前的函数才能编译(因为 palignr 需要字节移位计数作为立即数)。

一样,您可以在编译时从不同的随机播放中进行选择,具体取决于 indx 值。他似乎在建议 CPP 宏,但那会很难看。

简单地使用 if(indx>=16) 或类似的东西要容易得多,这将优化掉。 (如果编译器拒绝使用明显的 "variable" 移位计数编译您的代码,您可以将 indx 设为模板参数。)Agner Fog 在他的 Vector Class Library (license=GPL), for functions like template <uint32_t d> static inline Vec8ui divide_by_ui(Vec8ui const & x).

中使用了它

相关:Emulating shifts on 32 bytes with AVX 根据轮班次数给出了不同洗牌策略的答案。但它只是试图模拟一个转变,而不是一个连续/车道交叉 palignr

vperm2i128 在 Intel 主流 CPUs 上很快(但仍然是交叉洗牌,所以 3c 延迟),但在 Ryzen 上很慢(8 微指令,3c latency/3c 吞吐量) .如果您正在为 Ryzen 进行调整,您会想要使用 if() 来计算 vextracti128 的组合以获得低通道上的高通道 and/or vinserti128。您可能还想使用单独的班次,然后 vpblendd 将结果放在一起。


设计正确的洗牌:

indx 确定每个通道的新字节需要从哪里来。让我们通过考虑 64 位元素来简化:

 hi |  lo
D C | B A    # a
H G | F E    # b

palignr(b,a i) forms (H G D C) >> i | (F E B A) >> i
But what we want is

D C | B A    # concatq(b,a,0): no-op.  return a;

E D | C B    # concatq(b,a,1):  applies to 16-bit element counts from 1..7
          low lane needs  hi(a).lo(a)
          high lane needs lo(b).hi(a)
        return palignr(swapmerge(a,b), a, 2*i).  (Where we use vperm2i128 to lane-swap+merge hi(a) and lo(b))
F E | D C    # concatq(b,a,2)
        special case of exactly half reg width: Just use vperm2i128.
        Or on Ryzen, `vextracti128` + `vinserti128`
G F | E D    # concatq(b,a,3): applies to 16-bit element counts from 9..15
        low  lane needs lo(b).hi(a)
        high lane needs hi(b).lo(b).  vperm2i128 -> palignr looks good
        return palignr(b, swapmerge(a,b), 2*i-16).

H G | F E    # concatq(b,a,4): no op: return b;

有趣的是,在 palignr 两种情况下都使用了 lo(b) | hi(a)。我们永远不需要 lo(a) | hi(b) 作为 palignr 输入。

这些设计说明直接导致此实现:

// UNTESTED
// clang refuses to compile this, but gcc works.

// in many cases won't be faster than simply using unaligned loads.
static inline __m256i lanecrossing_alignr_epi16(__m256i a, __m256i  b, unsigned int count) {
#endif
   if (count == 0)
     return a;
   else if (count <= 7)
     return _mm256_alignr_epi8(_mm256_permute2x128_si256(a,b,0x21),a,count*2);
   else if (count == 8)
      return _mm256_permute2x128_si256(a,b,0x21);
   else if (count > 8 && count <= 15)
     // clang chokes on the negative shift count even when this branch is not taken
     return _mm256_alignr_epi8(b,_mm256_permute2x128_si256(a,b,0x21),count*2 - 16);
   else if (count == 16)
     return b;
   else
     assert(0 && "out-of-bounds shift count");

// can't get this to work without C++ constexpr :/
//   else
//     static_assert(count <= 16, "out-of-bounds shift count");
}

我把它 on the Godbolt compiler explorer 与一些测试函数放在一起,这些函数用不同的常量移位计数内联它。 gcc6.3 将其编译为

test_alignr0:
    ret            # a was already in ymm0
test_alignr3:
    vperm2i128      ymm1, ymm0, ymm1, 33   # replaces b
    vpalignr        ymm0, ymm1, ymm0, 6
    ret
test_alignr8:
    vperm2i128      ymm0, ymm0, ymm1, 33
    ret
test_alignr11:
    vperm2i128      ymm0, ymm0, ymm1, 33   # replaces a
    vpalignr        ymm0, ymm1, ymm0, 6
    ret
test_alignr16:
    vmovdqa ymm0, ymm1
    ret

铿锵声。首先,对于 count*2 - 16 不使用 if/else 链的那个分支的计数,它说 error: argument should be a value from 0 to 255

此外,它迫不及待地想看到 alignr() 计数最终成为一个编译时常量:error: argument to '__builtin_ia32_palignr256' must be a constant integer,即使在内联之后也是如此。您可以通过将 count 设为模板参数来在 C++ 中解决该问题:

template<unsigned int count>
static inline __m256i lanecrossing_alignr_epi16(__m256i a, __m256i  b) {
   static_assert(count<=16, "out-of-bounds shift count");
   ...

在 C 中,您可以将其设为 CPP 宏而不是函数来处理它。

count*2 - 16 问题对于 clang 来说更难解决。您可以将轮班计数作为宏名称的一部分,例如 CONCAT256_EPI16_7。您可能会使用一些 CPP 技巧来分别执行 1..7 版本和 9..15 版本。 (Boost 有一些疯狂的 CPP 技巧。)


顺便说一句,你的打印功能很奇怪。它调用第一个元素 c[1] 而不是 c[0]。随机播放的矢量索引从 0 开始,所以它真的很混乱。