【问题标题】:How to implement sign function with SSE3?如何用 SSE3 实现签名功能?
【发布时间】:2017-05-09 23:37:30
【问题描述】:

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),就像它的输入一样。

更新:看来 Cory Nelson 的回答在这里会起作用:

__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));

【问题讨论】:

  • 对于第 2 部分,您为什么需要 1.0f 标志,而不仅仅是来自 _mm_cmpgt_ps 的 AND 掩码?例如要有条件地添加某些内容,请将具有比较结果的操作数屏蔽为比较为假的零元素。全零位 = 0.0f,这是加法恒等式。或者,如果您只想制作 0 或不变的内容,并且使用蒙版。
  • @PeterCordes,我不确定我是否理解你(我对位的经验并不丰富 :-))。你会为第 2 部分写一个解决方案吗?效率越高越好:-)。
  • 你可以用(A>B)?1.f:0.f查看g++生成的代码来获得一些想法......(cmpltps+andps)
  • /facepalm.我担心你会因为那样可怕的事情而想要它。使用 FP 乘法和加法是实现condition ? a : b 的一种非常慢的方法。如果您没有 SSE4 BLENDPS,请使用通常的 ANDPS/ANDNPS/ORPS 序列根据比较结果进行混合。见Branchless “select” (cond ? a : b) section in this blog post
  • 如果你想否定一些东西,把掩码变成一个只设置符号位(有条件地)的向量,然后用它进行异或来翻转另一个向量的符号位。 (请记住,与0 进行异或没有效果,它是身份值。与1 进行异或有点翻转。有关使用布尔值操作符号位的更多信息,另请参阅SSE absolute value

标签: x86 vectorization x86-64 sse simd


【解决方案1】:

您已经接近了,但您的代码无法正常工作,因为您试图仅使用强制转换将 0/-1 int 转换为浮点数。

试试这个(未经测试):

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 可能更有效。

【讨论】:

  • 它说“不存在从 _m128 到 _mm128i 的合适的用户定义转换。
  • 啊 - 一些编译器需要强制转换(顺便说一句,您使用的是哪个编译器?) - 我会尽快修复它。
  • 我正在使用 VS 2015。
  • @Zboson:我认为它是 gcc 的早期版本(最高 4.something),或者带有-flax-vector-conversions 的 gcc 的更新版本。
  • @Zboson:是的,我倾向于将很多这些东西隔离到自定义宏或内联函数中,否则它往往会变得冗长且不可读,
【解决方案2】:

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

__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));
}

【讨论】:

  • 您可以只使用一个_mm_cmpneq_ps(x, zero) 制作一个稍微草率的版本,然后复制符号位。请参阅我的答案,了解当输入为 -0.0 时返回 -0.0 的版本,否则对于非 NaN 输入返回相同的内容。
  • 等离子的整数版本指出,全1比较结果已经是整数-1。编译器有望优化 _mm_and_psset1_epi32(-1)。有趣的是,您可以通过逻辑右移将比较结果转换为整数 0 或 1,因此您也不需要那里的常量。 (但是那个演员很可能有一个旁路延迟)。无论如何,您应该在整数版本中使用_mm_and_si128_mm_or_si128
  • @PeterCordes 整数版本始终将其保持在浮点域中,以避免重新解释寄存器时可能出现的流水线停顿。
  • 是的,但是当这个函数的结果被用作整数向量时,会有一些绕过延迟的额外延迟。或者不是,因为英特尔 SnB 系列上的一些指令似乎可以在整数或 FP 数据上没有额外延迟的情况下使用。例如我认为 Agner Fog 说像 _mm_or_ps 这样的布尔运算在其输出用作整数运算的输入时没有额外的延迟。对于您的 FP 版本,两个比较结果都通过 ANDPS,最好仍然使用 _mm_and_ps,然后在这些结果上使用 _mm_or_si128,免费提供整数域结果。
  • 整数域布尔运算可以在 Intel CPU 上的任何端口上运行,但 FP 域布尔运算只能在 Skylake 之前的端口 5 上运行,因此整数域布尔运算具有更好的吞吐量。见my answer here。另外,嗯,我看到orps 与整数输入一起使用时没有额外的延迟,但我不确定 SnB 系列的其他方式。 por 用于andps 的结果时可能会有额外的延迟。我认为cmpps 在 FP 域中产生其 0/-1 结果是对的。
【解决方案3】:

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

如果您采用浮点数并将这些位重新解释为有符号 二进制补码 整数,则可以得到 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 在 cmets 中建议的那样,使用一个浮点比较 _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 之一。

【讨论】:

  • 我认为如果你想要一个没有 SSSE3 psignd 的整数结果,仍然使用 cmpps 而不是特殊情况 -0.0 可能会更快。不过,使用移位而不是(或之后)比较是一个巧妙的技巧,并且可以节省一些整数常量。
  • 不知道你在说什么希望编译器使用pcmpgtd 指令与零进行比较。在 Haswell 上,psrad 在 p0 上运行,而 pcmpgtd 在 p15 上运行。你能解释一下为什么你认为这里有任何丢失的指令级并行性吗?我没有看得很仔细,所以也许有什么问题,在这种情况下,你应该在你的答案中解释它。
  • @PeterCordes 不,你是对的。我错误地认为它们在相同的端口上运行。在比较阶段,我专注于在同一个端口上并行执行 2xpcmpgtd,让其他端口空闲。在这种情况下,两个编译版本在性能上是相同的,如果它没有从另一条指令中删除 p0。
  • @PeterCordes 顺便说一句,我尝试使用 cmpltps 而不是两个整数比较来处理特殊情况 -0.0f。它节省了 3 条指令,并引入了 2 条额外的延迟。所以它节省了 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()) 的结果进行ANDing 来修复它

// 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));
}

【讨论】:

  • 我对整数结果提出了类似的答案,但没有 SSSE3。
  • 很好的答案!我+1。我不介意-0.0f 变成-0.0f。但不能是-1f。你将如何最有效地完成第二部分?如果允许 SSE3 怎么办?
  • 我现在才意识到,对于-0.0f,您的解决方案会生成-0.0f,而不是-1.0f。如果结果将用于浮点运算,它一点也不比 Cory 的解决方案差。更好的是,它保留了所有浮点值的符号位,同时为符号生成正确的数学结果。实际上,它是一个更符合 IEEE-754 的解决方案,我喜欢它。
  • @plasmacel:不过,两者都没有传播 NaN。由于全为位模式是 NaN,我们可以使用 x 自身的 cmpneqps 来获得 NaN 掩码,然后将其与结果相或。
  • @plasmacel:查看我的更新。大多数 CPU 上的关键路径延迟应该几乎不会增加,因为 NaN 检查可以并行发生。将其应用于x_signbit 而不是最终结果意味着更多 ILP。 Cory 的版本也不传播 NaN(我认为为 NaN 返回 0.0),所以这与 NaN 处理的性能大致相同。我当然留下了根据 NaN 的符号返回 -/+1.0f 的更高性能版本。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-09-05
  • 1970-01-01
  • 2022-07-04
  • 1970-01-01
  • 2018-06-10
  • 2011-08-09
  • 1970-01-01
相关资源
最近更新 更多