用数学方法找到最接近 0 的值
Mathematically find the value that is closest to 0
有没有办法从数学上确定一个值是否比另一个值更接近 0?
例如 closerToZero(-2, 3)
会 return -2
.
我尝试删除符号,然后比较最小值,但我会分配初始数字的无符号版本。
a 和 b 是符合 IEEE-754 标准的浮点双精度数(js 数)
(64 位 => 1 位符号 11 位指数 52 位小数)
min (a,b) => b-((a-b)&((a-b)>>52));
result = min(abs(a), abs(b));
// result has the wrong sign ...
显而易见的算法是比较绝对值,并将其用于select原始值。
如果这绝对需要无分支(例如为了加密安全),请小心 ? :
三进制。它通常编译为无分支 asm,但这并不能保证。 (我假设这就是你标记 branch-prediction 的原因?如果只是出于性能考虑,编译器通常会做出正确的决定。)
在具有固定的 2 补码整数的语言中,请记住 abs(INT_MIN)
会溢出与输入宽度相同的带符号结果。 In C and C++, abs()
是不方便地设计为 return 和 int
并且在 2 的补码系统上用最负的 2 的补码整数调用它是未定义的行为。在具有明确包装的有符号整数数学(如 gcc -fwrapv
,或者 Java)的系统上,有符号 abs(INT_MIN)
会溢出回 INT_MIN,如果你这样做会给出错误的结果有符号比较,因为 INT_MIN 最大程度地远离 0.
确保对 abs
结果进行无符号比较,以便正确处理 INT_MIN
. (或者如@kaya3 建议的那样,映射正整数负,而不是负到正。)
避免未定义行为的安全 C 实现:
unsigned absu(int x) {
return x<0? 0U - x : x;
}
int minabs(int a, int b) {
return absu(a) < absu(b) ? a : b;
}
请注意 <
与 <=
实际上在 minabs
中很重要:如果它们的大小相等,则决定 select 哪个。
0U - x
将 x
转换为 unsigned
在 从 0 减去可能溢出之前。将负有符号整数类型转换为无符号整数在 C 和 C++ 中明确定义为模缩减(与浮点数不同,UB IIRC)。在 2 的补码机器上,这意味着使用相同的位模式不变。
这对 x86-64 (Godbolt) 编译得很好,尤其是对 clang。 (即使使用 -march=skylake
,GCC 也避免了 cmov
,结果序列更糟。除了在执行两个 absu 操作后的最终 select,然后它使用 cmovbe
,即 2 微指令而不是 Intel CPU 上 cmovb
的 1,因为它需要读取 ZF 和 CF 标志。如果它在 EAX 中已经得到相反的值,它可能已经使用 cmovb
。)
# clang -O3
absu:
mov eax, edi
neg eax # sets flags like sub-from-0
cmovl eax, edi # select on signed less-than condition
ret
minabs:
mov ecx, edi
neg ecx
cmovl ecx, edi # inlined absu(a)
mov eax, esi
mov edx, esi
neg edx
cmovl edx, esi # inlined absu(b)
cmp ecx, edx # compare absu results
cmovb eax, edi # select on unsigned Below condition.
ret
GCC 和 clang 完全无分支,启用优化。可以肯定的是,其他 ISA 也将是相同的。
它可能会很好地自动矢量化,但 x86 在 AVX512 之前没有 SIMD 无符号整数比较。 (您可以通过翻转高位来使用有符号整数来模拟pcmpgtd
)。
对于 float / double,abs
更便宜且不会溢出:只需清除符号位,然后将其用于 select 原始值。
有没有办法从数学上确定一个值是否比另一个值更接近 0?
例如 closerToZero(-2, 3)
会 return -2
.
我尝试删除符号,然后比较最小值,但我会分配初始数字的无符号版本。
a 和 b 是符合 IEEE-754 标准的浮点双精度数(js 数)
(64 位 => 1 位符号 11 位指数 52 位小数)
min (a,b) => b-((a-b)&((a-b)>>52));
result = min(abs(a), abs(b));
// result has the wrong sign ...
显而易见的算法是比较绝对值,并将其用于select原始值。
如果这绝对需要无分支(例如为了加密安全),请小心 ? :
三进制。它通常编译为无分支 asm,但这并不能保证。 (我假设这就是你标记 branch-prediction 的原因?如果只是出于性能考虑,编译器通常会做出正确的决定。)
在具有固定的 2 补码整数的语言中,请记住 abs(INT_MIN)
会溢出与输入宽度相同的带符号结果。 In C and C++, abs()
是不方便地设计为 return 和 int
并且在 2 的补码系统上用最负的 2 的补码整数调用它是未定义的行为。在具有明确包装的有符号整数数学(如 gcc -fwrapv
,或者 Java)的系统上,有符号 abs(INT_MIN)
会溢出回 INT_MIN,如果你这样做会给出错误的结果有符号比较,因为 INT_MIN 最大程度地远离 0.
确保对 abs
结果进行无符号比较,以便正确处理 INT_MIN
. (或者如@kaya3 建议的那样,映射正整数负,而不是负到正。)
避免未定义行为的安全 C 实现:
unsigned absu(int x) {
return x<0? 0U - x : x;
}
int minabs(int a, int b) {
return absu(a) < absu(b) ? a : b;
}
请注意 <
与 <=
实际上在 minabs
中很重要:如果它们的大小相等,则决定 select 哪个。
0U - x
将 x
转换为 unsigned
在 从 0 减去可能溢出之前。将负有符号整数类型转换为无符号整数在 C 和 C++ 中明确定义为模缩减(与浮点数不同,UB IIRC)。在 2 的补码机器上,这意味着使用相同的位模式不变。
这对 x86-64 (Godbolt) 编译得很好,尤其是对 clang。 (即使使用 -march=skylake
,GCC 也避免了 cmov
,结果序列更糟。除了在执行两个 absu 操作后的最终 select,然后它使用 cmovbe
,即 2 微指令而不是 Intel CPU 上 cmovb
的 1,因为它需要读取 ZF 和 CF 标志。如果它在 EAX 中已经得到相反的值,它可能已经使用 cmovb
。)
# clang -O3
absu:
mov eax, edi
neg eax # sets flags like sub-from-0
cmovl eax, edi # select on signed less-than condition
ret
minabs:
mov ecx, edi
neg ecx
cmovl ecx, edi # inlined absu(a)
mov eax, esi
mov edx, esi
neg edx
cmovl edx, esi # inlined absu(b)
cmp ecx, edx # compare absu results
cmovb eax, edi # select on unsigned Below condition.
ret
GCC 和 clang 完全无分支,启用优化。可以肯定的是,其他 ISA 也将是相同的。
它可能会很好地自动矢量化,但 x86 在 AVX512 之前没有 SIMD 无符号整数比较。 (您可以通过翻转高位来使用有符号整数来模拟pcmpgtd
)。
对于 float / double,abs
更便宜且不会溢出:只需清除符号位,然后将其用于 select 原始值。