SSE / AVX:基于每个元素的最小和最大绝对值,从两个__m256浮点向量中进行选择

时间:2018-09-19 22:50:08

标签: sse intrinsics avx avx512

我正在寻找

的有效AVX(AVX512)实现
// Given
float u[8];
float v[8];

// Compute
float a[8];
float b[8];

//  Such that
for ( int i = 0; i < 8; ++i )
{
    a[i] = fabs(u[i]) >= fabs(v[i]) ? u[i] : v[i];
    b[i] = fabs(u[i]) <  fabs(v[i]) ? u[i] : v[i];
}

即,我需要根据auv的{​​{1}}中选择元素方式,并根据{{1 }},其中mask是元素级的。

2 个答案:

答案 0 :(得分:5)

前几天我也遇到了同样的问题。我想出的解决方案(仅使用AVX)是:

// take the absolute value of u and v
__m256 sign_bit = _mm256_set1_ps(-0.0f);
__m256 u_abs = _mm256_andnot_ps(sign_bit, u);
__m256 v_abs = _mm256_andnot_ps(sign_bit, v);
// get a mask indicating the indices for which abs(u[i]) >= abs(v[i])
__m256 u_ge_v = _mm256_cmp_ps(u_abs, v_abs, _CMP_GE_OS);
// use the mask to select the appropriate elements into a and b, flipping the argument
// order for b to invert the sense of the mask
__m256 a = _mm256_blendv_ps(u, v, u_ge_v);
__m256 b = _mm256_blendv_ps(v, u, u_ge_v);

等效的AVX512为:

// take the absolute value of u and v
__m512 sign_bit = _mm512_set1_ps(-0.0f);
__m512 u_abs = _mm512_andnot_ps(sign_bit, u);
__m512 v_abs = _mm512_andnot_ps(sign_bit, v);
// get a mask indicating the indices for which abs(u[i]) >= abs(v[i])
__mmask16 u_ge_v = _mm512_cmp_ps_mask(u_abs, v_abs, _CMP_GE_OS);
// use the mask to select the appropriate elements into a and b, flipping the argument
// order for b to invert the sense of the mask
__m512 a = _mm512_mask_blend_ps(u_ge_v, u, v);
__m512 b = _mm512_mask_blend_ps(u_ge_v, v, u);

正如彼得·科德斯(Peter Cordes)在上述评论中所建议的那样,还有其他一些方法,例如,取绝对值后接一个最小值/最大值,然后重新插入符号位,但是我找不到比该值更短/更低的延迟的东西。此指令序列。

答案 1 :(得分:3)

clang使用@RequestBody和必要的-ffast-math限定词https://godbolt.org/z/NMvN1u对向量进行自动矢量化的工作相当合理。然后将两个输入都吸收到ABS中,一次,__restrict在具有相同掩码的原始输入上进行两次比较,而其他源以相反的顺序得到最小值和最大值。

这几乎是我在检查编译器所做的事情并查看其输出以确认我尚未想到的细节之前所考虑的问题。我没有比这更聪明的了。我认为我们不能避免ab和b分别使用abs()。没有vblendvps比较谓词可以比较幅度并且忽略符号位。

cmpps

对于AVX512,您将执行相同的操作,只是将其与掩码而不是其他矢量进行比较。

// untested: I *might* have reversed min/max, but I think this is right.
#include <immintrin.h>
// returns min_abs
__m256 minmax_abs(__m256 u, __m256 v,  __m256 *max_result) {
    const __m256 signbits = _mm256_set1_ps(-0.0f);
    __m256 abs_u = _mm256_andnot_ps(signbits, u);
    __m256 abs_v = _mm256_andnot_ps(signbits, v);  // strip the sign bit

    __m256 maxabs_is_v = _mm256_cmp_ps(abs_u, abs_v, _CMP_LT_OS);  // u < v

    *max_result = _mm256_blendv_ps(v, u, maxabs_is_v);
    return        _mm256_blendv_ps(u, v, maxabs_is_v);
}

Clang以一种有趣的方式(Godbolt)编译return语句:

// returns min_abs
__m512 minmax_abs512(__m512 u, __m512 v,  __m512 *max_result) {
    const __m512 absmask = _mm512_castsi512_ps(_mm512_set1_epi32(0x7fffffff));
    __m512 abs_u = _mm512_and_ps(absmask, u);
    __m512 abs_v = _mm512_and_ps(absmask, v);  // strip the sign bit

    __mmask16 maxabs_is_v = _mm512_cmp_ps_mask(abs_u, abs_v, _CMP_LT_OS);  // u < v

    *max_result = _mm512_mask_blend_ps(maxabs_is_v, v, u);
    return        _mm512_mask_blend_ps(maxabs_is_v, u, v);
}

clang注意到.LCPI2_0: .long 2147483647 # 0x7fffffff minmax_abs512(float __vector(16), float __vector(16), float __vector(16)*): # @minmax_abs512(float __vector(16), float __vector(16), float __vector(16)*) vbroadcastss zmm2, dword ptr [rip + .LCPI2_0] vandps zmm3, zmm0, zmm2 vandps zmm2, zmm1, zmm2 vcmpltps k1, zmm3, zmm2 vblendmps zmm2 {k1}, zmm1, zmm0 vmovaps zmmword ptr [rdi], zmm2 ## store the blend result vmovaps zmm0 {k1}, zmm1 ## interesting choice: blend merge-masking ret 已经使用了混合输入之一,并没有使用另一个vblendmps,而是使用带有常规向量zmm0的合并掩码。对于512位vmovaps(端口0或端口5的单联指令),Skylake-AVX512的优势为零,但是如果Agner Fog's instruction tables是正确的,vblendmps仅在端口上运行0或5,但带掩码的256位或128位vblendmps x/y/zmm可以在p0 / p1 / p5的任何一个上运行。

这两者都是单周期/单周期延迟,这与基于2 ups掩码 vector 的AVX2 vmovaps x/ymm{k}, x/ymm不同。 (因此,即使对于256位向量,AVX512还是一个优势)。不幸的是,当使用vblendvps进行编译时,gcc,clang或ICC都没有将_mm256_cmp_ps转换为_mm256_cmp_ps_mask并将AVX2内在函数优化为AVX512指令。)

-march=skylake-avx512来制作s/512/256/的版本,该版本将AVX512用于256位向量。


Gcc更进一步,并对

进行了可疑的“优化”
minmax_abs512

而不是使用一个混合指令。 (我一直认为我看到的是一个存储区,然后是一个被屏蔽的存储区,但是没有,两个编译器都没有这种混合方式。)