C 在_mm256_rsqrt_ps()中处理零

C 在_mm256_rsqrt_ps()中处理零,c,x86,sse,intrinsics,avx,C,X86,Sse,Intrinsics,Avx,考虑到\u mm256\u sqrt\u ps()相对较慢,并且我生成的值会立即被\u mm256\u floor\u ps()截断,环顾四周,似乎执行了以下操作: _mm256_mul_ps(_mm256_rsqrt_ps(eightFloats), eightFloats); 是获得额外性能和避免管道停滞的方法 不幸的是,对于零值,我当然会在计算1/sqrt(0)时遇到崩溃。最好的解决办法是什么?我已经尝试过这个方法(它有效而且更快),但是有没有更好的方法,或者

考虑到
\u mm256\u sqrt\u ps()
相对较慢,并且我生成的值会立即被
\u mm256\u floor\u ps()
截断,环顾四周,似乎执行了以下操作:

_mm256_mul_ps(_mm256_rsqrt_ps(eightFloats),
              eightFloats);
是获得额外性能和避免管道停滞的方法

不幸的是,对于零值,我当然会在计算
1/sqrt(0)
时遇到崩溃。最好的解决办法是什么?我已经尝试过这个方法(它有效而且更快),但是有没有更好的方法,或者在某些情况下我会遇到问题

_mm256_mul_ps(_mm256_rsqrt_ps(_mm256_max_ps(eightFloats,
                                            _mm256_set1_ps(0.1))),
              eightFloats);
我的代码是针对垂直应用程序的,因此我可以假设它将在Haswell CPU(i7-4810MQ)上运行,因此可以使用FMA/AVX2。原始代码约为:

float vals[MAX];
int sum = 0;
for (int i = 0; i < MAX; i++)
{
    int thisSqrt = (int) floor(sqrt(vals[i]));
    sum += min(thisSqrt, 0x3F);
}
float vals[MAX];
整数和=0;
对于(int i=0;i

vals
的所有值都应为整数值。(为什么一切都不只是
int
是一个不同的问题……

tl;dr:请参阅结尾,了解编译后应该可以使用的代码


为了解决
0.0
问题,您还可以将
0.0
的特殊情况输入与
0.0
的源代码进行FP比较。使用比较结果作为掩码,将
0*+无穷大
in
sqrt(x)=x*rsqrt(x)
)中产生的任何NaN归零。自动矢量化时,Clang会执行此操作。(但它将
blendps
与归零向量一起使用,而不是将比较掩码与
和nps
一起直接用于归零或保留元素。)

也可以使用
sqrt(x)~=recip(rsqrt(x))
<代码>rsqrt(0)=+Inf
<代码>往复(+Inf)=0
。然而,使用两个近似值会使相对误差增大,这是一个问题


你错过的东西是: 当输入为完全平方时,截断为整数(而不是四舍五入)需要精确的
sqrt
结果。如果25*rsqrt(25)的结果是4.999999或其他值(而不是5.00001),您将添加
4
而不是
5

即使使用Newton-Raphson迭代,
rsqrtps
,它仍可能给出
5.0-1ulp
。(1ulp=最后一位的一个单位=尾数的最低位)

此外:

  • 。请注意,我们关心的是吞吐量而不是延迟,因为我们在循环中使用它,没有做很多其他事情。sqrt不是循环携带的dep链的一部分,因此不同的迭代可以同时运行它们的sqrt计算

在执行
(x+offset)*近似值(x+offset)
之前,可以通过添加一个小常量来一举两得。足够大以克服1.5*2-12的最大相对误差,但足够小以不碰撞
sqrt_约(63*63-1+偏移量)
63
(最敏感的情况)

事实上,没有牛顿迭代,即使没有添加任何内容,我们也完蛋了<代码>约为(63*63-1)本身可能超过63.0
n=36
sqrt(n*n-1)+error
中的相对误差小于
sqrt(n*n)
时的最大值。GNU计算:

define f(n) { local x=sqrt(n*n-1); local e=x*1.5*2^(-12); print x; print e, x+e; }
; f(36)
35.98610843089316319413
~0.01317850650545403926 ~35.99928693739861723339
; f(37)
36.9864840178138587015
~0.01354485498699237990 ~37.00002887280085108140
您的源数据是否有任何属性意味着您不必担心它刚好低于一个大的完美正方形?例如,它总是完美正方形吗

您可以检查所有可能的输入值,因为重要域非常小(0..63*63的整数FP值),以查看Intel Haswell上的实际错误是否足够小,但这将是一个脆弱的优化,可能会使您的代码在AMD CPU上中断,甚至在未来的Intel CPU上中断。不幸的是,仅仅按照ISA规范编码就可以保证相对误差达到1.5*2-12,这需要更多的指令。我看不出有什么诀窍

如果你的上限更小(比如20),你可以只做
isqrt=static\u cast((x+0.5)*大约rsqrt(x+0.5))
。如果
20*20
,你会得到20,但是
20*20-1
总是19

; define test_approx_sqrt(x, off) { local s=x*x+off; local sq=s/sqrt(s); local sq_1=(s-1)/sqrt(s-1); local e=1.5*2^(-12); print sq, sq_1; print sq*e, sq_1*e; }
; test_approx_sqrt(20, 0.5)
~20.01249609618950056874 ~19.98749609130668473087   # (x+0.5)/sqrt(x+0.5)
 ~0.00732879495710064718  ~0.00731963968187500662   # relative error
注意
val*(x+/-err)=val*x+/-val*err
。IEEE FP mul生成的结果正确地四舍五入到0.5ulp,因此这应该适用于FP相对误差


无论如何,我想你需要一次牛顿-拉斐逊迭代。 最好的办法是在使用
rsqrt
进行近似sqrt之前,将
0.5
添加到输入中。这回避了0/0=NaN问题,将+/-误差范围全部推到整数切点的一侧(对于我们关心的范围内的数字)

FP min/max指令与FP add指令具有相同的性能,并且将处于关键路径上。使用add而不是max也解决了完美正方形的结果可能被忽略的问题


编译器输出:一个不错的起点 我从clang 3.7.1中获得了非常好的自动矢量化结果,它具有
sum\u int
,具有
-fno math errno-funsafe math optimizations
-ffinite math only
不是必需的(但即使使用完整的
-ffast math
,当使用
rsqrtps
时,clang也可以避免
sqrt(0)=NaN

sum\u fp
不会自动矢量化,即使使用完整的
-ffast math

然而,
clang
的版本遇到了与您的想法相同的问题:从rsqrt+NR截断不精确的结果,可能给出错误的整数。IDK如果这就是gcc没有自动矢量化的原因,因为它可以在不改变结果的情况下使用
sqrtps
进行大的加速。(至少,只要所有浮点数都在0和INT_MAX2之间,否则转换回整数将给出INT_MIN的“不确定”结果。(符号位设置,所有其他位清除)。这种情况下,
-ffast math
会中断程序,除非
; define test_approx_sqrt(x, off) { local s=x*x+off; local sq=s/sqrt(s); local sq_1=(s-1)/sqrt(s-1); local e=1.5*2^(-12); print sq, sq_1; print sq*e, sq_1*e; }
; test_approx_sqrt(20, 0.5)
~20.01249609618950056874 ~19.98749609130668473087   # (x+0.5)/sqrt(x+0.5)
 ~0.00732879495710064718  ~0.00731963968187500662   # relative error
// autovectorizes with clang, but has rounding problems.
// Note the use of sqrtf, and that floorf before truncating to int is redundant. (removed because clang doesn't optimize away the roundps)
int sum_int(float vals[]){
  int sum = 0;
  for (int i = 0; i < MAX; i++) {
    int thisSqrt = (int) sqrtf(vals[i]);
    sum += std::min(thisSqrt, 0x3F);
  }
  return sum;
}
#include <immintrin.h>
#define MAX 4096

// 2*sqrt(x) ~= 2*x*approx_rsqrt(x), with a Newton-Raphson iteration
// dividing by 2 is faster in the integer domain, so we don't do it
__m256 approx_2sqrt_ps256(__m256 x) {
    // clang / gcc usually use -3.0 and -0.5.  We could do the same by using fnmsub_ps (add 3 = subtract -3), so we can share constants
    __m256 three = _mm256_set1_ps(3.0f);
    //__m256 half = _mm256_set1_ps(0.5f);  // we omit the *0.5 step

    __m256 nr  = _mm256_rsqrt_ps( x );  // initial approximation for Newton-Raphson
    //           1/sqrt(x) ~=    nr  * (3 - x*nr * nr) * 0.5 = nr*(1.5 - x*0.5*nr*nr)
    // sqrt(x) = x/sqrt(x) ~= (x*nr) * (3 - x*nr * nr) * 0.5
    // 2*sqrt(x) ~= (x*nr) * (3 - x*nr * nr)
    __m256 xnr = _mm256_mul_ps( x, nr );

    __m256 three_minus_muls = _mm256_fnmadd_ps( xnr, nr, three );  // -(xnr*nr) + 3
    return _mm256_mul_ps( xnr, three_minus_muls );
}

// packed int32_t: correct results for inputs from 0 to well above 63*63
__m256i isqrt256_ps(__m256 x) {
    __m256 offset = _mm256_set1_ps(0.5f);    // or subtract -0.5, to maybe share constants with compiler-generated Newton iterations.
    __m256 xoff = _mm256_add_ps(x, offset);  // avoids 0*Inf = NaN, and rounding error before truncation
    __m256 approx_2sqrt_xoff = approx_2sqrt_ps256(xoff);
    __m256i i2sqrtx = _mm256_cvttps_epi32(approx_2sqrt_xoff);
    return _mm256_srli_epi32(i2sqrtx, 1);     // divide by 2 with truncation
    // alternatively, we could mask the low bit to zero and divide by two outside the loop, but that has no advantage unless port0 turns out to be the bottleneck
}

__m256i isqrt256_ps_simple_exact(__m256 x) {
    __m256 sqrt_x = _mm256_sqrt_ps(x);
    __m256i isqrtx = _mm256_cvttps_epi32(sqrt_x);
    return isqrtx;
}

int hsum_epi32_avx(__m256i x256){
    __m128i xhi = _mm256_extracti128_si256(x256, 1);
    __m128i xlo = _mm256_castsi256_si128(x256);
    __m128i x  = _mm_add_epi32(xlo, xhi);
    __m128i hl = _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2));
    hl  = _mm_add_epi32(hl, x);
    x   = _mm_shuffle_epi32(hl, _MM_SHUFFLE(2, 3, 0, 1));
    hl  = _mm_add_epi32(hl, x);
    return _mm_cvtsi128_si32(hl);
}

int sum_int_avx(float vals[]){
  __m256i sum = _mm256_setzero_si256();
  __m256i upperlimit = _mm256_set1_epi32(0x3F);

  for (int i = 0; i < MAX; i+=8) {
    __m256 v = _mm256_loadu_ps(vals+i);
    __m256i visqrt = isqrt256_ps(v);
    // assert visqrt == isqrt256_ps_simple_exact(v) or something
    visqrt = _mm256_min_epi32(visqrt, upperlimit);
    sum = _mm256_add_epi32(sum, visqrt);
  }
  return hsum_epi32_avx(sum);
}
sum_int_avx(float*):
    vpxor   ymm3, ymm3, ymm3
    xor     eax, eax
    vbroadcastss    ymm0, dword ptr [rip + .LCPI4_0]  ; set1(0.5)
    vbroadcastss    ymm1, dword ptr [rip + .LCPI4_1]  ; set1(3.0)
    vpbroadcastd    ymm2, dword ptr [rip + .LCPI4_2]  ; set1(63)
LBB4_1:                                               ; latencies
    vaddps      ymm4, ymm0, ymmword ptr [rdi + 4*rax] ; 3c
    vrsqrtps    ymm5, ymm4                            ; 7c
    vmulps      ymm4, ymm4, ymm5   ; x*nr             ; 5c
    vfnmadd213ps ymm5, ymm4, ymm1                     ; 5c
    vmulps      ymm4, ymm4, ymm5                      ; 5c
    vcvttps2dq  ymm4, ymm4                            ; 3c
    vpsrld      ymm4, ymm4, 1      ; 1c this would be a mulps (but not on the critical path) if we did this in the FP domain
    vpminsd     ymm4, ymm4, ymm2                      ; 1c
    vpaddd      ymm3, ymm4, ymm3                      ; 1c
    ; ... (those 9 insns repeated: loop unrolling)
    add     rax, 16
    cmp     rax, 4096
    jl      .LBB4_1
    ;... horizontal sum