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

时间:2017-07-21 19:53:57

标签: c simd intrinsics avx avx2

我已经实现了内联函数(_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 , 

1 个答案:

答案 0 :(得分:7)

对这个问题给出一般答案是不可能的。这是一个很短的片段,最好的策略取决于周围的代码和你正在运行的CPU。

有时我们可以排除在任何CPU上都没有优势但只消耗更多相同资源的事情,但在考虑未对齐负载与混洗之间的权衡时则不是这样。

在可能未对齐的输入数组上的循环中,您可能最好使用未对齐的加载。特别是您的输入数组将在大多数时间在运行时对齐。如果不是,并且这是一个问题,那么如果可能的话,做一个未对齐的第一个向量,然后从第一个对齐边界对齐。即一个序言的常用技巧,可以到达主循环的对齐边界。但是使用多个指针时,如果指针相对于彼此未对齐,通常最好对齐商店指针,并进行未对齐的加载(根据英特尔的优化手册)。 (请参阅Agner Fog's optimization guides代码wiki中的和其他链接。)

在最近的英特尔CPU上,跨越缓存行边界的向量加载仍具有相当好的吞吐量,但这是您考虑ALU策略或混合重载和重叠加载的一个原因(在展开的循环中)可能会采取替代策略,这样你就不会出现任何瓶颈。)

斯蒂芬·佳能指出 _mm_alignr_epi8 (PALIGNR) equivalent in AVX2(可能与此重复),如果您需要将几个不同的偏移窗口放入两个向量的相同级联中,那么两个存储+重复的未对齐加载非常好。在Intel CPU上,只要不跨越缓存行边界(因此alignas(64)缓冲区),就可以获得256b未对齐负载的每时钟2个吞吐量。

但是,对于一次性用例,存储/重新加载并不是很好,因为存储转发失败,因为任何一个存储中都没有完全包含负载。它的吞吐量仍然很低,但延迟很昂贵。另一个巨大的优势是它具有运行时变量偏移的效率。

如果延迟是一个问题,使用ALU shuffle可能会很好(尤其是在英特尔,其中交叉路口的shuffle并不比在车道上昂贵得多)。再次,考虑/衡量你的循环瓶颈,或者只是尝试存储/重新加载ALU。

随机播放策略

当前函数只能在编译时知道indx时编译(因为palignr需要将字节移位计数作为立即数)。

作为@Mohammad suggested,您可以在编译时从不同的shuffle中选择,具体取决于indx值。他似乎在暗示一个CPP宏观,但那将是丑陋的。

更容易简单地使用if(indx>=16)或类似的东西,这将优化。 (如果编译器拒绝使用明显的“可变”移位计数编译代码,则可以使indx成为模板参数。)Agner Fog在他的Vector Class Library(license = GPL)中使用此函数, template <uint32_t d> static inline Vec8ui divide_by_ui(Vec8ui const & x)

相关:Emulating shifts on 32 bytes with AVX根据班次计数有不同的随机播放策略。但它只是试图模仿一个转变,而不是一个连接/车道交叉palignr

vperm2i128在英特尔主流CPU上运行速度很快(但仍然是一个跨越通道的混乱,因此3c延迟),但在Ryzen上缓慢(8个uop,3c延迟/ 3c吞吐量)。如果您正在为Ryzen进行调整,那么您需要使用if()找出vextracti128组合以获得高通道和/或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;

有趣的是,在lo(b) | hi(a)个案例中都使用了palignr。我们永远不需要lo(a) | hi(b)作为帮助。

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

// 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

clang choke on it 。首先,对于不使用error: argument should be a value from 0 to 255 / count*2 - 16链的那个分支的计数,if表示else

此外,它不能等待并看到alignr()计数最终成为编译时常量:error: argument to '__builtin_ia32_palignr256' must be a constant integer,即使它是在内联之后。您可以通过使count成为模板参数来解决这个问题:

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宏而不是一个处理它的函数。

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

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