对这个问题不可能给出一般性的答案。这是一个如此短的片段,最佳策略取决于周围的代码以及您正在运行的 CPU。
有时,我们可以排除那些对任何 CPU 都没有优势、只是消耗更多相同资源的事物,但在考虑未对齐负载与洗牌之间的权衡时,情况并非如此。
在可能未对齐的输入数组上的循环中,您可能最好使用未对齐的负载。特别是您的输入数组大多数时候都会在运行时对齐。如果不是,并且这是一个问题,那么如果可能的话,执行未对齐的第一个向量,然后从第一个对齐边界对齐。 IE。到达主循环对齐边界的序言的常用技巧。但对于多个指针,如果您的指针相对于彼此未对齐,通常最好对齐存储指针,并执行未对齐的加载(根据英特尔的优化手册)。 (看Agner Fog 的优化指南以及其他链接x86标记维基百科。)
在最新的 Intel CPU 上,跨越缓存行边界的矢量负载仍然具有相当好的吞吐量,但这就是您可能考虑 ALU 策略或混合洗牌和重叠负载的原因之一(在展开循环中,您可能会交替使用策略)这样你就不会在任何一个上遇到瓶颈)。
正如史蒂芬·卡农 (Stephen Canon) 在《AVX2 中的 _mm_alignr_epi8 (PALIGNR) 等效项(可能是重复的),如果您需要将多个不同的偏移窗口放入两个向量的同一串联中,那么两个存储+重复的未对齐加载非常好。在 Intel CPU 上,只要 256b 未对齐负载不跨越缓存行边界,您就可以获得每时钟 2 的吞吐量(因此alignas(64)
你的缓冲区)。
不过,存储/重新加载对于单一使用情况来说并不是很好,因为对于未完全包含在任一存储中的加载,存储转发失败。它对于吞吐量来说仍然很便宜,但对于延迟来说却很昂贵。另一个巨大的优势是它的运行时变量偏移量非常高效。
如果延迟是一个问题,那么使用 ALU shuffle 可能会很好(特别是在 Intel 上,其中跨车道 shuffle 并不比车道内贵很多)。再次考虑/测量循环瓶颈,或者尝试存储/重新加载与 ALU。
洗牌策略:
您当前的函数只能在以下情况下编译:indx
在编译时已知(因为palignr
需要字节移位计数作为立即数)。
As @穆罕默德建议,您可以在编译时从不同的洗牌中进行选择,具体取决于indx
价值。他似乎建议采用 CPP 宏,但那会很丑陋。
使用起来更加简单if(indx>=16)
或类似的东西,这将优化掉。 (你可以使indx
如果编译器拒绝使用明显“可变”的移位计数来编译您的代码,则为模板参数。)Agner Fog 在他的矢量类库(许可证=GPL),对于类似的功能.
有关的:使用 AVX 模拟 32 字节的移位根据轮班数,有不同的洗牌策略的答案。但它只是试图模拟换挡,而不是连续/车道交叉palignr
.
vperm2i128
在 Intel 主流 CPU 上速度很快(但仍然是通道交叉洗牌,因此 3c 延迟),但在 Ryzen 上速度较慢(8 uops,3c 延迟/3c 吞吐量)。如果您正在针对 Ryzen 进行调优,您需要使用if()
找出一个组合vextracti128
获得高车道和/或vinserti128
在一条低矮的车道上。您可能还想使用单独的轮班,然后vpblendd
结果在一起。
设计正确的洗牌:
The 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");
}
我把它在 Godbolt 编译器资源管理器上一些测试函数将其与不同的恒定移位计数内联。 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
哐当哽咽。首先,它说error: argument should be a value from 0 to 255
为了count*2 - 16
对于不使用该分支的计数if
/else
chain.
此外,它迫不及待地看到alignr()
count 最终成为一个编译时常量:error: argument to '__builtin_ia32_palignr256' must be a constant integer
,即使是在内联之后。你可以在 C++ 中通过以下方式解决这个问题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 宏而不是函数来处理该问题。
The count*2 - 16
对于 clang 来说问题更难解决。您可以将移位计数作为宏名称的一部分,例如 CONCAT256_EPI16_7。您可能可以使用一些 CPP 技巧来分别执行 1..7 版本和 9..15 版本。 (Boost 有一些疯狂的 CPP hack。)
顺便说一句,你的打印功能很奇怪。它调用第一个元素c[1]
代替c[0]
。向量索引从 0 开始进行洗牌,所以这真的很令人困惑。