找到绝对最小值的最短方法。两个数,并乘以它在 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);
这基本上对a
和b
进行了排序(并排除了A>0 && B<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 < 0 < -B
,最后一种情况是-B <= 0 <= 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 结果上使用 or
和 set1(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 上运行。 vpxor
和 vpor
之类的布尔值可以在 p0、p1 或 p5 中的任何一个上运行。 (https://uops.info/) 所以这种方式可能比使用两次psignw
的其他想法更好。它通过 1 条指令将两个操作数的依赖链“耦合”在一起,但这可能会受到吞吐量限制,即使数据来自同一通道中的另一个操作。
pabsw
和 pminuw
都需要 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<0
和in2 = -0x8000
。
@chtz:整数 VPXOR 可以在 Intel CPU 的任何端口上运行。你在想vxorps
。我考虑过这一点,但担心当两个输入相同时它会创建一个0
,因此下一个vpsignw
会将结果归零。例如in1=in2=任何东西。我们可以通过将非零低位 ORing 到 XOR 结果中来解决这个问题。
确实,这是一个问题(但-0x8000
的边缘情况也是如此)。我会写下我的替代解决方案(我很确定它有效)。
我在第一个想法中错误地计算了端口使用情况,但实际上现在找到了一个更简单的解决方案。只是3p01+1p015
。以上是关于找到绝对最小值的最短方法。两个数,并乘以它在 AVX 中的输入符号的主要内容,如果未能解决你的问题,请参考以下文章