我已经实现了内联函数(_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 ,
答案 0 :(得分:7)
对这个问题给出一般答案是不可能的。这是一个很短的片段,最好的策略取决于周围的代码和你正在运行的CPU。
有时我们可以排除在任何CPU上都没有优势但只消耗更多相同资源的事情,但在考虑未对齐负载与混洗之间的权衡时则不是这样。
在可能未对齐的输入数组上的循环中,您可能最好使用未对齐的加载。特别是您的输入数组将在大多数时间在运行时对齐。如果不是,并且这是一个问题,那么如果可能的话,做一个未对齐的第一个向量,然后从第一个对齐边界对齐。即一个序言的常用技巧,可以到达主循环的对齐边界。但是使用多个指针时,如果指针相对于彼此未对齐,通常最好对齐商店指针,并进行未对齐的加载(根据英特尔的优化手册)。 (请参阅Agner Fog's optimization guides代码wiki中的x86和其他链接。)
在最近的英特尔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开始,所以它真的很混乱。