找到绝对最小值的最短方法。两个数,并乘以它在 AVX 中的输入符号

Posted

技术标签:

【中文标题】找到绝对最小值的最短方法。两个数,并乘以它在 AVX 中的输入符号【英文标题】:shortest way to find absolute min. of two number & multiply it with signs of its inputs in AVX 【发布时间】:2020-09-21 15:07:44 【问题描述】:

关于如何在没有乘法的情况下为以下 C 逻辑实现 AVX 的任何提示,

for(int i = 0;i<4096;i++)

   out[i] = sign(inp1[i])*sign(inp2[i])*min(abs(inp1[i]), abs(inp2[i])); 

// inp1, inp2 & out 是 16 位寄存器。

【问题讨论】:

【参考方案1】:

您的问题有很短(但不明显)的解决方案:

res = max(min(a,b), -max(a,b));

(所有最小/最大操作均已签名)

要解释为什么会这样,首先让我们设置

A = min(a,b); B = max(a,b);

这基本上对ab 进行了排序(并排除了A&gt;0 &amp;&amp; B&lt;0 的情况)。我们现在只需要区分 3 种情况:

A<0  && B<0:     res = -B 
A<0  && B>=0:    res = -min(-A, B) = max(A, -B)
A>=0 && B>=0:    res = A

幸运的是,第一种和最后一种情况也可以计算为max(A,-B),因为第一种情况是A &lt; 0 &lt; -B,最后一种情况是-B &lt;= 0 &lt;= A

或者,您可以询问(并信任)WolframAlpha。(不是很有帮助,因为它仅评估为真“假设 a 和 b 为正数” - 您可以绘制差异虽然在两个表达式之间)


用 AVX2 实现这个(忽略加载和存储):

__m256i A = _mm256_min_epi16(a,b);
__m256i B = _mm256_max_epi16(a,b);
__m256i res = _mm256_max_epi16(A, _mm256_sub_epi16(_mm256_setzero_si256(), B));

setzero 操作将发生在任何循环之外,因此对于每个数据包都有三个最小/最大操作和一个 psub 操作。在 Intel-CPU 上,首先在端口 p01 上执行,而 psub 在任何 p015 上执行,因此循环会在 p01 上出现瓶颈,每个数据包需要 1.5 个周期。

正如@Soonts 所指出的,-B 操作可能会溢出,对于 B=-0x8000(对于带符号的 int16,没有正的 0x8000)。这只发生在a=b=-0x8000。如果您更喜欢在这种情况下输出0x7fff,您可以将减法替换为饱和减法(_mm256_subs_epi16)。

【讨论】:

不错;这比我的想法要好得多,OP应该接受这个答案。 很好的答案,但它有一个错误,绝对和一元减号可能会溢出整数。幸运的是,这里很容易修复,而且修复在性能方面是免费的,请将 _mm256_sub_epi16 指令替换为 _mm256_subs_epi16 @Sonts 我认为这个问题只会出现在a==b==-0x8000 上(在这种情况下,它会输出0x8000,(这里的意思是+0x8000,但当然对于有符号整数来说,这会被看到)为否定)。我将对此添加注释。【参考方案2】:

sign(inp1[i])*sign(inp2[i]) 部分几乎可以完全用_mm256_sign_epi16(in1, in2) 实现,并将其用作另一个vpsignw 的第二个操作数,以将其符号应用于min(abs,abs) 结果。

psignw 否定或置零第一个操作数,具体取决于第二个操作数是负数还是零。 (Intrinsics guide)。 (我们不需要psignw 的归零部分:如果任一输入为零,则它们的绝对值的无符号最小值将为零。但我们必须避免这取决于我们如何生成输入,如果我们的实际输入都不为零时会发生这种情况。)

有一个极端情况是错误的:in1 = INT16_MIN = 0x8000, in2。否定in1 的结果仍然是否定的;由于 2 的补码,大多数负数没有逆数。

如果 2 个值之一不能是 0x8000,则将其用作 _mm256_sign_epi16 的第一个参数,无需额外操作。

@chtz 提出了一种解决方法:将输入异或,以获得符号位的正确值。但这将触发vpsignw 对 in1==in2 的归零行为,因为 in1^in2==0。您可以在 XOR 结果上使用 orset1(1) 以确保它不为零。

// pseudocode because the full intrinsic names are long and hard to read / type
    sign = (in1 ^ in2) | 1;
    out = psignw( min(abs1,abs2), sign);
  // operation count: XOR, OR, PSIGNW = 3 plus min(abs,abs)

在 Skylake 上,vpsignw 可以在执行端口 p0 或 p1 上运行。 vpxorvpor 之类的布尔值可以在 p0、p1 或 p5 中的任何一个上运行。 (https://uops.info/) 所以这种方式可能比使用两次psignw 的其他想法更好。它通过 1 条指令将两个操作数的依赖链“耦合”在一起,但这可能会受到吞吐量限制,即使数据来自同一通道中的另一个操作。

pabswpminuw 都需要 p0 / p1,不能在 p5 上运行,因此选择相同数量的指令但使用 可以利用端口 5 的指令会导致更好的Skylake 后端的执行端口压力平衡。 Zen2 有点相似,布尔值能够在任何 FP 执行端口 (0/1/2/3) 上运行,但 psignw / pabsw 仅 FP0 / FP3,pminuw 仅 FP0/1/3。


另一种选择是完全避免psignw,而不是解决它的归零行为:异或,然后用算术右移广播符号位,然后用2的补码标识-x = ~x - (-1)实现条件否定。但这需要多一次操作。

    sign = (in1 ^ in2) >> 15;   // pxor  psraw
    out =  (min(abs1,abs2) ^ sign) - sign;  // pxor, psubw
  // operation count: XOR, shift, XOR, SUB = 4 plus min(abs,abs)

另一个解决方法是在vpsignw 之前使用_mm256_or_si256(in1, _mm256_set1_epi16(1)),以确保该值具有相同的符号但不是INT16_MIN

// not as good as 
   sign = psignw(in1 | 1, in2);   // VPOR, VPSIGNW
   out = psignw( min(abs1,abs2), sign);
// operation count: OR, 2x PSIGNW = 3 plus min(abs,abs)

算术右移 1 是不安全的:当输入为 1 时,它可以使操作数为零,从而导致输入为 1, 2 的最终输出为零


IDK 如果有任何聪明的技巧会比 vpabsw 在每个输入上单独提供 vpminuw 更好

【讨论】:

sign(inp1[i])*sign(inp2[i]) 也可以使用_mm256_xor_si256(inp1[i],inp2[i]) 计算,因为只有高位是相关的(在大多数(全部?)Intel CPU 上也将使用p5)。我想我找到了一种采用3p01+3p015(而不是4p01+1p015)的替代方法——这只有在与您的解决方案混合时才有用(不过需要检查一下)。 其实_mm256_sign_epi16(in1, in2)是错误的(或者不是预期的结果),如果in1&lt;0in2 = -0x8000 @chtz:整数 VPXOR 可以在 Intel CPU 的任何端口上运行。你在想vxorps。我考虑过这一点,但担心当两个输入相同时它会创建一个0,因此下一个vpsignw 会将结果归零。例如in1=in2=任何东西。我们可以通过将非零低位 ORing 到 XOR 结果中来解决这个问题。 确实,这是一个问题(但-0x8000 的边缘情况也是如此)。我会写下我的替代解决方案(我很确定它有效)。 我在第一个想法中错误地计算了端口使用情况,但实际上现在找到了一个更简单的解决方案。只是3p01+1p015

以上是关于找到绝对最小值的最短方法。两个数,并乘以它在 AVX 中的输入符号的主要内容,如果未能解决你的问题,请参考以下文章

有效国际电话号码的最短长度是多少?

检查 null 并在没有时分配另一个值的最短方法

为变量分配默认值的最短方法?

寻找包含两个节点的最短循环

用布尔值(PHP)评估数组的最短方法?

ActionScript 3 AS3找到两个角度之间的最短旋转角度