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

shortest way to find absolute min. of two number & multiply it with signs of its inputs in AVX

关于如何在没有乘法的情况下为以下 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位寄存器

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

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

有一个极端情况是错误的:in1 = INT16_MIN = 0x8000, in2<0。取反 in1 的结果仍然是负数;感谢 2 的补码大多数负数没有倒数。

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

@chtz 提出了一种变通策略:将输入异或在一起以获得符号位的正确值。但这将触发 vpsignw 对 in1==in2 的归零行为,因为 in1^in2==0。您可以 orset1(1) 在 XOR 结果上确保它是 non-zero.

// 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 上 运行,所以选择相同数量的指令,但使用 can 利用端口 5 可以更好地平衡 Skylake 上 back-end 的执行端口压力。 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)

另一个解决方法是 _mm256_or_si256(in1, _mm256_set1_epi16(1))vpsignw 之前确保值具有相同的符号但不是 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[ 的最终输出为零=47=]


IDK 如果有任何聪明的技巧会比 vpabsw on each input separately to feed vpminuw

您的问题有很短的(但 non-obvious)解决方案:

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

(所有 min/max 操作均已签名)

为了解释为什么这样做,首先让我们设置

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

这基本上对 ab 进行了排序(并排除了 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 操作将发生在任何循环之外,因此对于每个数据包,有三个 min/max 操作和一个 psub-operation。在 Intel-CPUs 上,第一个在端口 p01 上执行,而 psub 在任何 p015 上执行,因此循环将在 p01 上 bottle-neck,需要 1.5每个数据包的周期数。

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