我尝试使用long ints
尝试找到8 AVX2
的分钟。我是SIMD
编程的绿色,我不知道从哪里开始。我没有看到任何说明如何在min
中执行max
和AVX2
的帖子/示例。我知道由于long ints
限制,我不能超过4 256 bit
,但我可以使用三个步骤解决我的问题。此外,我无法弄清楚如何将已存在的正常long int array
的数据加载到vectors
的{{1}}。
我知道这个过程背后的想法,这就是我想要实现的目标
avx2
有人可以帮助我解决如何让它发挥作用。最后一个long int nums = {1 , 2, 3 , 4 , 5 , 6 , 7, 8}
a = min(1,2) ; b = min(3,4) ; c = min(5,6) ; d = min(7,8)
x = min(a,b) ; y = min(c,d)
answer = min(x,y)
也是一个操作,在min
上执行此操作会更好吗?我应该使用CPU
以外的其他内容吗? (我在AVX2
系统上)
答案 0 :(得分:2)
对于x86优化等,请参阅https://stackoverflow.com/tags/x86/info上的链接。 ESP。英特尔的内在指南和Agner Fog的东西。
如果你总是有8个元素(64个字节),这会简化很多事情。向量化小东西时的一个主要挑战是不要添加太多启动/清理开销来处理不填充整个向量的剩余元素。
AVX2没有打包64位整数的最小/最大指令。只有8,16和32.这意味着您需要使用生成掩码的比较来模拟它(对于条件为false的元素,全0为全,0为真,因此您可以将此掩码清零元素在其他向量中。)为了节省实际执行AND / ANDN和OR操作以将事物与掩码组合在一起,有混合指令。
AVX-512 将为此操作带来更大的加速。 (支持进入(仅限xeon)Skylake)。它有一个_mm_min_epi64
。此操作还有一个库函数:__int64 _mm512_reduce_min_epi64 (__m512i a)
。我假设这个内在函数会发出一系列vpminsq
指令。英特尔在其内部查找器中列出了它,但它只是一个英特尔库函数,不是机器指令。
这是一个应该有效的AVX2实现。我还没有对它进行测试,但编译后的输出看起来像是正确的指令序列。我可能在那里得到了相反的比较,所以检查一下。
操作原理是:获取两个256b向量的元素min。将其拆分为两个128b向量并获得元素最小值。然后将两个64b值的向量带回GP寄存器并执行最后一分钟。 Max是在同一时间完成的,与min。交错。
(哎呀,你在你的问题中提到了min / max,但是现在我看到你实际上只是想要min。删除不需要的部分是微不足道的,你可以将它改为返回值而不是通过指针存储结果/ references。标量版本可能更快;更好地测试应用程序使用此操作的位置(不是独立的微基准测试)。)
#include <stdint.h>
#include <immintrin.h>
int64_t input[8] = { 1, 2, 3, };
#define min(a,b) \
({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); \
_a < _b ? _a : _b; })
#define max(a,b) \
({ __typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a > _b ? _a : _b; })
// put this where it can get inlined. You don't want to actually store the results to RAM
// or have the compiler-generated VZEROUPPER at the end for every use.
void minmax64(int64_t input[8], int64_t *minret, int64_t *maxret)
{
__m256i *in_vec = (__m256i*)input;
__m256i v0 = in_vec[0], v1=in_vec[1]; // _mm256_loadu_si256 is optional for AVX
__m256i gt = _mm256_cmpgt_epi64(v0, v1); // 0xff.. for elements where v0 > v1. 0 elsewhere
__m256i minv = _mm256_blendv_epi8(v0, v1, gt); // take bytes from v1 where gt=0xff (i.e. where v0>v1)
__m256i maxv = _mm256_blendv_epi8(v1, v0, gt); // input order reversed
/* for 8, 16, or 32b: cmp/blend isn't needed
minv = _mm256_min_epi32(v0,v1);
maxv = _mm256_min_epi32(v0,v1); // one insn shorter, but much faster (esp. latency)
And at the stage of having a 128b vectors holding the min and max candidates,
you'd shuffle and repeat to get the low 64, and optionally again for the low 32,
before extracting to GP regs to finish the comparisons.
*/
__m128i min0 = _mm256_castsi256_si128(minv); // stupid gcc 4.9.2 compiles this to a vmovdqa
__m128i min1 = _mm256_extracti128_si256(minv, 1); // extracti128(x, 0) should optimize away to nothing.
__m128i max0 = _mm256_castsi256_si128(maxv);
__m128i max1 = _mm256_extracti128_si256(maxv, 1);
__m128i gtmin = _mm_cmpgt_epi64(min0, min1);
__m128i gtmax = _mm_cmpgt_epi64(max0, max1);
min0 = _mm_blendv_epi8(min0, min1, gtmin);
max0 = _mm_blendv_epi8(max1, max0, gtmax);
int64_t tmp0 = _mm_cvtsi128_si64(min0); // tmp0 = max0.m128i_i64[0]; // MSVC only
int64_t tmp1 = _mm_extract_epi64(min0, 1);
*minret = min(tmp0, tmp1); // compiles to a quick cmp / cmovg of 64bit GP registers
tmp0 = _mm_cvtsi128_si64(max0);
tmp1 = _mm_extract_epi64(max0, 1);
*maxret = min(tmp0, tmp1);
}
这可能会或者可能不会比在GP寄存器中执行整个操作更快,因为64位负载是一个uop,cmp
是一个uop,cmovcc
只有2 uop(在Intel上)。 Haswell每循环可发出4个uop。在你到达比较树的底部之前,还有许多独立的工作要做,即便如此,cmp是1个周期的延迟,而cmov是2.如果你将工作交错了一分钟和最大值同时,有两个独立的依赖链(在这种情况下是树)。
矢量版本的延迟远远高于吞吐量。如果您需要对多个独立的8个值集合执行此操作,则矢量版本可能会很好。否则,pcmpgt*
的5周期延迟和blendv
的2周期延迟将受到伤害。如果还有其他可以同时进行的独立工作,那就没问题了。
如果你的整数较小,pmin*
(有符号或无符号,8,16或32b)是1个周期的延迟,每个周期2个吞吐量。对于16b无符号元素,甚至还有一个水平min指令,它给出了一个向量中8个min元素,就像user-number-guy所评论的那样。这样可以在将最小候选者缩小到适合一个向量之后,缩短整个分割/分钟缩小过程。