X86 使用AVX2计算8个长整数的最小值

X86 使用AVX2计算8个长整数的最小值,x86,sse,simd,avx,avx2,X86,Sse,Simd,Avx,Avx2,我试图使用AVX2找到8长整数的最小值。我是SIMD编程的新手,不知道从哪里开始。我没有看到任何帖子/示例解释如何在AVX2中执行min和max。我知道由于256位限制,我不能超过4个长整数,但我可以通过三个步骤解决我的问题。此外,我也不知道如何将已经存在的正常长整数数组的数据加载到向量中,以便avx2 我知道这个过程背后的想法,这就是我想要实现的 long int nums = {1 , 2, 3 , 4 , 5 , 6 , 7, 8} a = min(1,2) ; b = min(3,4)

我试图使用
AVX2
找到8
长整数的最小值。我是
SIMD
编程的新手,不知道从哪里开始。我没有看到任何帖子/示例解释如何在
AVX2
中执行
min
max
。我知道由于
256位
限制,我不能超过4个
长整数
,但我可以通过三个步骤解决我的问题。此外,我也不知道如何将已经存在的正常
长整数数组的数据加载到
向量
中,以便
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以外的其他软件?(我使用的是
x86
系统)

有关x86优化等内容,请参阅上的链接。特别是英特尔的《本质论指南》和Agner Fog的东西

如果您总是正好有8个元素(64字节),这就简化了很多事情。向量化小东西时的一个主要挑战是,不要在处理未填充整个向量的剩余元素时添加太多启动/清理开销

AVX2没有压缩64位整数的最小/最大指令。只有8、16和32岁。这意味着您需要使用生成掩码的比较来模拟它(条件为false的元素为all-0s,条件为true的元素为all-1s,因此您可以使用此掩码将其他向量中的元素归零。)为了节省实际执行AND/ANDN和OR操作以将内容与掩码相结合的时间,有混合指令

AVX-512将为该操作带来巨大的加速。(支持进入(仅限xeon)Skylake)。它有一个
\u mm\u min\u epi64
。此操作还有一个库函数:
\uuuuu int64\umm512\ureduce\umin\uepi64(\uuuum512i a)
。我假设这个内在函数将发出一系列
vpminsq
指令。英特尔在其内部查找器中列出了它,但它只是一个英特尔库函数,而不是一条机器指令

这里有一个AVX2实现应该可以工作。我还没有测试它,但是编译后的输出看起来像是正确的指令序列。我可能在某个地方得到了一个相反的比较,所以检查一下

操作原理是:得到两个256b向量的元素最小值。将其拆分为两个128b向量,并得到该向量的元素最小值。然后将两个64b值的向量返回到GP寄存器,并执行最终的最小值。同时执行最大值,并与最小值交错

(哦,你在问题中提到了最小值/最大值,但现在我看到你实际上只是想要最小值。删除不需要的部分很简单,你可以将其更改为返回值,而不是通过指针/引用存储结果。标量版本可能更快;更好地在应用程序使用此操作的上下文中进行测试(不是一个独立的微基准)

#包括
#包括
int64_t输入[8]={1,2,3,};
#定义最小值(a,b)\
({uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu\
_a<\u b?\u a:\u b;})
#定义最大值(a,b)\
({{uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu\
___uuuu(b)b=(b)的类型\
_a>\u b?\u a:\u b;})
//把它放在可以内联的地方。你不想把结果存储到内存中
//或者让编译器在每次使用结束时生成VZEROUPPER。
void minmax64(int64_t输入[8],int64_t*minret,int64_t*maxret)
{
__m256i*in_vec=(u m256i*)输入;
__m256i v0=in_vec[0],v1=in_vec[1];//_mm256_loadu_si256是AVX的可选配置
__m256i gt=_mm256_cmpgt_epi64(v0,v1);//0xff..用于v0>v1.0的元素
__m256i minv=_mm256_blendv_epi8(v0,v1,gt);//从v1中获取字节,其中gt=0xff(即v0>v1)
__m256i maxv=_mm256_blendv_epi8(v1,v0,gt);//输入顺序颠倒
/*对于8、16或32b:不需要cmp/混合
最小值=_mm256_min_epi32(v0,v1);
maxv=_mm256_min_epi32(v0,v1);//一个insn更短,但更快(特别是延迟)
以及在具有128b向量的阶段,保持最小和最大候选,
您可以洗牌并重复以获得低位64,也可以选择再次获得低位32,
在提取到GP regs以完成比较之前。
*/
__m128i min0=_mm256_castsi256_si128(minv);//愚蠢的gcc 4.9.2将其编译为vmovdqa
__m128i min1=_mm256_extracti128_si256(minv,1);//extracti128(x,0)应该优化为零。
__m128i最大值=_mm256_castsi256_si128(最大值);
__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
int64_t tmp1=_mm_extract_epi64(min0,1);
*minret=min(tmp0,tmp1);//编译为64位GP寄存器的快速cmp/cmovg
tmp0=_mm_cvtsi128_si64(最大值为0);
tmp1=mm_extract_epi64(max0,1);
*maxret=min(tmp0,tmp1);
}
这可能比在GP寄存器中完成整个过程要快,也可能不快,因为64位加载是一个uop,
cmp
是一个uop,
cmovcc
只有2个uop(在英特尔上).Haswell可以为每个周期发出4个UOP。在到达比较树的底部之前,还有许多独立的工作要做,即使如此,cmp是1个周期延迟,cmov是2。如果您同时将工作交错为最小值和最大值,则有两个独立的依赖链(本例中为树)

vector版本的延迟比吞吐量高得多。如果您需要对多个独立的8个值集执行此操作,vector版本可能会做得很好。否则,5个周期的延迟