找到绝对最小值的最短方法。两个数字并将其乘以其在 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。您可以 or
与 set1(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 上 运行。 vpxor
和 vpor
之类的布尔值可以在 p0、p1 或 p5 中的任何一个上 运行。 (https://uops.info/) 所以这种方式可能比使用 psignw
两次的其他想法更好。它通过 1 条指令更早地将两个操作数的依赖链“耦合”在一起,但即使数据来自同一通道中的另一个操作,这也可能会限制吞吐量。
pabsw
和 pminuw
都需要 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);
这基本上对 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
操作将发生在任何循环之外,因此对于每个数据包,有三个 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
)代替减法。
关于如何在没有乘法的情况下为以下 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。您可以 or
与 set1(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 上 运行。 vpxor
和 vpor
之类的布尔值可以在 p0、p1 或 p5 中的任何一个上 运行。 (https://uops.info/) 所以这种方式可能比使用 psignw
两次的其他想法更好。它通过 1 条指令更早地将两个操作数的依赖链“耦合”在一起,但即使数据来自同一通道中的另一个操作,这也可能会限制吞吐量。
pabsw
和 pminuw
都需要 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);
这基本上对 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
操作将发生在任何循环之外,因此对于每个数据包,有三个 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
)代替减法。