如何用SSE3实现签名功能?

时间:2016-12-24 17:26:08

标签: x86 vectorization x86-64 sse simd

1)有没有办法使用具有以下特征的SSE3(无SSE4)有效实施sign function

  • 输入是一个浮点向量__m128
  • 输出也应为__m128,其值为[-1.0f,0.0f,1.0f]

我试过这个,但它没有用(虽然我认为应该):

inputVal = _mm_set_ps(-0.5, 0.5, 0.0, 3.0);
comp1 = _mm_cmpgt_ps(_mm_setzero_ps(), inputVal);
comp2 = _mm_cmpgt_ps(inputVal, _mm_setzero_ps());
comp1 = _mm_castsi128_ps(_mm_castps_si128(comp1));
comp2 = _mm_castsi128_ps(_mm_castps_si128(comp2));
signVal = _mm_sub_ps(comp1, comp2);

2)有没有办法创建“标志”功能(我不确定正确的名称)。即,如果A > B结果为1,则0__m128。结果应该是浮点数(__m128 greatherThanFlag = _mm_and_ps(_mm_cmpgt_ps(valA, valB), _mm_set1_ps(1.0f)); __m128 lessThanFlag = _mm_and_ps(_mm_cmplt_ps(valA, valB), _mm_set1_ps(1.0f)); ),就像它的输入一样。

更新:Cory Nelson的回答似乎在这里有效:

clrTextBk

4 个答案:

答案 0 :(得分:5)

首先想到的可能是最简单的:

__m128 sign(__m128 x)
{
    __m128 zero = _mm_setzero_ps();

    __m128 positive = _mm_and_ps(_mm_cmpgt_ps(x, zero), _mm_set1_ps(1.0f));
    __m128 negative = _mm_and_ps(_mm_cmplt_ps(x, zero), _mm_set1_ps(-1.0f));

    return _mm_or_ps(positive, negative);
}

或者,如果您错过了并且打算获得整数结果:

__m128i sign(__m128 x)
{
    __m128 zero = _mm_setzero_ps();

    __m128 positive = _mm_and_ps(_mm_cmpgt_ps(x, zero),
                                 _mm_castsi128_ps(_mm_set1_epi32(1)));
    __m128 negative = _mm_cmplt_ps(x, zero);

    return _mm_castps_si128(_mm_or_ps(positive, negative));
}

答案 1 :(得分:4)

如果sgn(-0.0f)可以产生-0.0f而不是+0.0f的输出,那么与@Cory Nelson的版本相比,您可以保存一两条指令。请参阅下面的传播NaN的版本。

  • 根据x != 0.0f
  • 的比较选择0.0或1.0
  • x的符号位复制到该位。

// return -0.0 for x=-0.0, otherwise the same as Cory's (except for NaN which neither handle well)
__m128 sgn_fast(__m128 x)
{
    __m128 negzero = _mm_set1_ps(-0.0f);

    // using _mm_setzero_ps() here might actually be better without AVX, since xor-zeroing is as cheap as a copy but starts a new dependency chain
    //__m128 nonzero = _mm_cmpneq_ps(x, negzero);  // -0.0 == 0.0 in IEEE floating point
    __m128 nonzero = _mm_cmpneq_ps(x, _mm_setzero_ps());

    __m128 x_signbit = _mm_and_ps(x, negzero);

    __m128 zeroone = _mm_and_ps(nonzero, _mm_set1_ps(1.0f));
    return _mm_or_ps(zeroone, x_signbit);
}

当输入为NaN时,根据NaN的符号,我认为它返回+/- 1.0f。 (当x为NaN时,_mm_cmpneq_ps()为真:请参阅the table on the CMPPD instruction)。

没有AVX,这比Cory的版本(with clang3.9 on the Godbolt compiler explorer)少两个指令。当内联到循环中时,内存源操作数可以是寄存器源操作数。 gcc使用更多指令,执行单独的MOVAPS加载并将其自身绘制到需要额外MOVAPS以将返回值转换为xmm0的角落。

    xorps   xmm1, xmm1
    cmpneqps        xmm1, xmm0
    andps   xmm0, xmmword ptr [rip + .LCPI0_0]    # x_signbit
    andps   xmm1, xmmword ptr [rip + .LCPI0_1]    # zeroone
    orps    xmm0, xmm1

关键路径延迟为cmpneqps + andps + orps,例如,在Intel Haswell上为3 + 1 + 1个周期。 Cory的版本需要并行运行两个cmpps指令才能实现延迟,这只能在Skylake上实现。其他CPU将产生资源冲突,导致额外的延迟周期。

传播NaN ,因此可能的输出为-1.0f-/+0.0f1.0fNaN,我们可以利用事实上,全1位模式是NaN。

  • _mm_cmpunord_ps(x,x)获取NaN掩码。 (或等效地,cmpneqps)
  • or将结果保留为未经修改或强制使用NaN。

// return -0.0 for x=-0.0.  Return -NaN for any NaN
__m128 sgn_fast_nanpropagating(__m128 x)
{
    __m128 negzero = _mm_set1_ps(-0.0f);
    __m128 nonzero = _mm_cmpneq_ps(x, _mm_setzero_ps());

    __m128 x_signbit = _mm_and_ps(x, negzero);
    __m128 nanmask   = _mm_cmpunord_ps(x,x);
    __m128 x_sign_or_nan = _mm_or_ps(x_signbit, nanmask);   // apply it here instead of to the final result for better ILP

    __m128 zeroone = _mm_and_ps(nonzero, _mm_set1_ps(1.0f));
    return _mm_or_ps(zeroone, x_sign_or_nan);
}

这有效地编译,并且几乎不会延长关键路径延迟。但是,在没有AVX的情况下复制寄存器需要更多的MOVAPS指令。

您可以使用SSE4.1 BLENDVPS执行一些有用的操作,但它不是所有CPU上最有效的指令。也很难避免将负零视为非零。

如果需要整数结果,可以使用SSSE3 _mm_sign_epi32(set1(1), x)获得-1,0或1输出。如果-0.0f -> -1过于草率,您可以通过与_mm_cmpneq_ps(x, _mm_setzero_ps())

的结果进行AND运算来解决这个问题。
// returns -1 for x = -0.0f
__m128i sgn_verysloppy_int_ssse3(__m128 x) {
  __m128i one = _mm_set1_epi32(1);
  __m128i sign = _mm_sign_epi32(one, _mm_castps_si128(x));
  return sign;
}

// correct results for all inputs
// NaN -> -1 or 1 according to its sign bit, never 0
__m128i sgn_int_ssse3(__m128 x) {
  __m128i one = _mm_set1_epi32(1);
  __m128i sign = _mm_sign_epi32(one, _mm_castps_si128(x));

  __m128  nonzero = _mm_cmpneq_ps(x, _mm_setzero_ps());
    return _mm_and_si128(sign, _mm_castps_si128(nonzero));
}

答案 2 :(得分:3)

如果您需要signum function float个向量,其结果是int32_t向量,并且您不关心NaN s,那么a基于以下理论,可以使用整数指令实现更高效的版本。

如果您使用浮点数并将这些位重新解释为带符号的二进制补码整数,则可以得到3种不同的情况(其中X是任意01,粗体MSB是符号位:

  • 0 X X X X X X X X X X X X X X 1> 0(或> 0.0f为浮动)
  • 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0== 0(或== 0.0f为浮动)
  • 1 X X X X X X X X X X X X X X X< 0(或<= 0.0f为浮动)

最后一种情况不明确,因为它可能是负零-0.0f的特殊浮点情况:

  • 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0== -0.0f == 0.0f为浮动

从这一点开始,浮点符号函数变为整数函数。

使用SSE3(不是SSSE3)提供的内在函数,可以实现为:

inline __m128i _mm_signum_ps(__m128 a)
{
    __m128i x = _mm_castps_si128(a);

    __m128i zero = _mm_setzero_si128();
    __m128i m0 = _mm_cmpgt_epi32(x, zero);
    __m128i m1 = _mm_cmplt_epi32(x, zero);
    __m128i m2 = _mm_cmpeq_epi32(x, _mm_set1_epi32(0x80000000));

    __m128i p = _mm_and_si128(m0, _mm_set1_epi32(+1));

    // Note that since (-1 == 0xFFFFFFFF) in two's complement,
    // n satisfies (n == m1), so the below line is strictly semantic
    // __m128i n = _mm_and_si128(m1, _mm_set1_epi32(-1));
    __m128i n = m1;

    return _mm_andnot_si128(m2, _mm_or_si128(p, n));
}

优化版本是

inline __m128i _mm_signum_ps(__m128 a)
{
    __m128i x = _mm_castps_si128(a);

    __m128i zr = _mm_setzero_si128();
    __m128i m0 = _mm_cmpeq_epi32(x, _mm_set1_epi32(0x80000000));
    __m128i mp = _mm_cmpgt_epi32(x, zr);
    __m128i mn = _mm_cmplt_epi32(x, zr);

    return _mm_or_si128(
      _mm_andnot_si128(m0, mn),
      _mm_and_si128(mp, _mm_set1_epi32(1))
    );
}

正如Peter在评论中所建议的,使用一个浮点比较_mm_cmplt_ps而不是两个整数比较_mm_cmplt_epi32 / _mm_cmpeq_epi32来处理-0.0f可以节省1个延迟,但它由于在浮点/整数域之间切换,可能会受到旁路延迟延迟的影响,因此最好坚持上面的仅整数实现。或不。由于您需要整数结果,因此您更有可能使用它并交换到整数域。所以:

inline __m128i _mm_signum_ps(__m128 a)
{
    __m128i x = _mm_castps_si128(a);
    __m128 zerops = _mm_setzero_ps();

    __m128i mn = _mm_castps_si128(_mm_cmplt_ps(a, zerops));
    __m128i mp = _mm_cmpgt_epi32(x, _mm_castps_si128(zerops));

    return _mm_or_si128(mn, _mm_and_si128(mp, _mm_set1_epi32(1)));
}

在clang 3.9中使用-march=x86-64 -msse3 -O3,这将编译为

_mm_signum_ps(float __vector(4)):                # @_mm_signum2_ps(float __vector(4))
        xorps   xmm1, xmm1                       # fp domain
        movaps  xmm2, xmm0                       # fp domain
        cmpltps xmm2, xmm1                       # fp domain
        pcmpgtd xmm0, xmm1                       # int domain
        psrld   xmm0, 31                         # int domain
        por     xmm0, xmm2                       # int domain
        ret

cmpltps以外,此处每条指令的延迟为1,吞吐量为<= 1。我认为这是一个非常有效的解决方案,可以使用SSSE3 _mm_sign_epi32进一步改进。

如果您需要浮点结果,最好完全保留在浮点域中(而不是在浮点/整数域之间交换),因此请使用Peter's solutions之一。

答案 3 :(得分:1)

你很接近,但你的代码不能正常工作,因为你试图仅使用强制转换将0 / -1 int转换为float。

试试这个(未经测试):

inputVal = _mm_set_ps(-0.5, 0.5, 0.0, 3.0);
comp1 = _mm_cmpgt_ps(_mm_setzero_ps(), inputVal);
comp2 = _mm_cmpgt_ps(inputVal, _mm_setzero_ps());
comp1 = _mm_cvtepi32_ps(_mm_castps_si128(comp1)); // 0/-1 => 0.0f/-1.0f
comp2 = _mm_cvtepi32_ps(_mm_castps_si128(comp2));
signVal = _mm_sub_ps(comp1, comp2);

话虽如此,我认为Cory's solution可能更有效率。