为什么使用 abs() 或 fabs() 而不是条件否定?
Why use abs() or fabs() instead of conditional negation?
在C/C++中,为什么要使用abs()
或fabs()
求变量的绝对值而不使用下面的代码?
int absoluteValue = value < 0 ? -value : value;
是不是跟低级指令少有关?
假设编译器无法确定 abs() 和条件否定都试图实现相同的目标,条件否定将编译为比较指令、条件跳转指令和移动指令,而 abs() 要么编译成实际的绝对值指令,在支持这种东西的指令集中,要么按位编译,除了符号位之外,一切都保持不变。上面的每条指令通常是 1 个周期,因此使用 abs() 可能至少与条件否定一样快,或者更快(因为编译器可能仍然会识别出您在使用条件否定时试图计算绝对值,并且无论如何生成一个绝对值指令)。即使编译后的代码没有变化,abs() 仍然比条件否定更具可读性。
首先想到的是可读性。
比较这两行代码:
int x = something, y = something, z = something;
// Compare
int absall = (x > 0 ? x : -x) + (y > 0 ? y : -y) + (z > 0 ? z : -z);
int absall = abs(x) + abs(y) + abs(z);
您建议的 "conditional abs" 不等同于浮点数的 std::abs
(或 fabs
),请参见
#include <iostream>
#include <cmath>
int main () {
double d = -0.0;
double a = d < 0 ? -d : d;
std::cout << d << ' ' << a << ' ' << std::abs(d);
}
输出:
-0 -0 0
鉴于 -0.0
和 0.0
表示相同的实数“0”,此差异可能重要也可能无关紧要,具体取决于结果的使用方式。但是,IEEE754 规定的 abs 函数要求结果的符号位为 0,这将禁止结果 -0.0
。我个人认为任何用于计算一些 "absolute value" 的东西都应该符合这种行为。
对于整数,两种变体在运行时和行为上都是等效的。 (Live example)
但由于 std::abs
(或合适的 C 等价物)被认为是正确且更易于阅读的,因此您应该始终更喜欢它们。
编译器很可能会在底层为两者做同样的事情——至少是一个现代的有能力的编译器。
但是,至少对于浮点数,如果要处理无穷大、非数字 (NaN)、负零等所有特殊情况,您最终将需要编写几十行代码。
而且更容易理解 abs
取绝对值比阅读如果它小于零则取反更容易。
如果编译器是 "stupid",它很可能最终会为 a = (a < 0)?-a:a
编写更糟糕的代码,因为它会强制执行 if
(即使它是隐藏的),这很可能比该处理器上的内置浮点 abs 指令差(除了特殊值的复杂性)
Clang(6.0 预发行版)和 gcc (4.9.2) 都会为第二种情况生成更糟糕的代码。
我写了这个小样本:
#include <cmath>
#include <cstdlib>
extern int intval;
extern float floatval;
void func1()
{
int a = std::abs(intval);
float f = std::abs(floatval);
intval = a;
floatval = f;
}
void func2()
{
int a = intval < 0?-intval:intval;
float f = floatval < 0?-floatval:floatval;
intval = a;
floatval = f;
}
clang 为 func1 编写此代码:
_Z5func1v: # @_Z5func1v
movl intval(%rip), %eax
movl %eax, %ecx
negl %ecx
cmovll %eax, %ecx
movss floatval(%rip), %xmm0 # xmm0 = mem[0],zero,zero,zero
andps .LCPI0_0(%rip), %xmm0
movl %ecx, intval(%rip)
movss %xmm0, floatval(%rip)
retq
_Z5func2v: # @_Z5func2v
movl intval(%rip), %eax
movl %eax, %ecx
negl %ecx
cmovll %eax, %ecx
movss floatval(%rip), %xmm0
movaps .LCPI1_0(%rip), %xmm1
xorps %xmm0, %xmm1
xorps %xmm2, %xmm2
movaps %xmm0, %xmm3
cmpltss %xmm2, %xmm3
movaps %xmm3, %xmm2
andnps %xmm0, %xmm2
andps %xmm1, %xmm3
orps %xmm2, %xmm3
movl %ecx, intval(%rip)
movss %xmm3, floatval(%rip)
retq
g++ func1:
_Z5func1v:
movss .LC0(%rip), %xmm1
movl intval(%rip), %eax
movss floatval(%rip), %xmm0
andps %xmm1, %xmm0
sarl , %eax
xorl %eax, intval(%rip)
subl %eax, intval(%rip)
movss %xmm0, floatval(%rip)
ret
g++ func2:
_Z5func2v:
movl intval(%rip), %eax
movl intval(%rip), %edx
pxor %xmm1, %xmm1
movss floatval(%rip), %xmm0
sarl , %eax
xorl %eax, %edx
subl %eax, %edx
ucomiss %xmm0, %xmm1
jbe .L3
movss .LC3(%rip), %xmm1
xorps %xmm1, %xmm0
.L3:
movl %edx, intval(%rip)
movss %xmm0, floatval(%rip)
ret
请注意,两种情况在第二种形式中都明显更复杂,而在 gcc 情况下,它使用了一个分支。 Clang 使用了更多指令,但没有分支。我不确定哪种处理器型号的速度更快,但很明显指令越多越好。
考虑一下,您可以将复杂的表达式输入 abs()
。如果你用expr > 0 ? expr : -expr
编码,你必须将整个表达式重复三次,它会被计算两次。
此外,这两个结果(冒号前后)可能会变成不同类型(如 signed int
/ unsigned int
),这会禁用 return 语句中的使用。
当然,你可以添加一个临时变量,但这只能解决部分问题,而且也好不到哪里去。
在给定的体系结构上,可能存在比条件分支更高效的低级实现。例如,CPU 可能有一个 abs
指令,或者一种在没有分支开销的情况下提取符号位的方法。假设算术右移可以填充寄存器 r 如果数字为负则为 -1,如果为正则为 0,abs x
可能变为 (x+r)^r
(并且看到
Mats Petersson 的回答,g++ 实际上在 x86 上执行此操作)。
其他答案已经讨论了 IEEE 浮点数的情况。
试图告诉编译器执行条件分支而不是信任库可能是不成熟的优化。
Why use abs() or fabs() instead of conditional negation?
已经陈述了各种原因,但考虑到条件代码的优势,因为 abs(INT_MIN)
应该避免。
当寻求整数的负绝对值
时,使用条件代码代替abs()
是有充分理由的
// Negative absolute value
int nabs(int value) {
return -abs(value); // abs(INT_MIN) is undefined behavior.
}
int nabs(int value) {
return value < 0 ? value : -value; // well defined for all `int`
}
当需要一个正的绝对函数并且 value == INT_MIN
是一个真正的可能性时,abs()
,尽管它的清晰度和速度都无法满足极端情况。多种选择
unsigned absoluteValue = value < 0 ? (0u - value) : (0u + value);
abs() 背后的意图是“(无条件地)将这个数字的符号设置为正数”。即使必须根据数字的当前状态将其实现为条件,将其视为简单的 "do this" 而不是更复杂的 "if… this… that" 可能更有用。
...你会把它变成一个宏,你可以有多个你可能不想要的评估(副作用)。考虑:
#define ABS(a) ((a)<0?-(a):(a))
并使用:
f= 5.0;
f=ABS(f=fmul(f,b));
这将扩展到
f=((f=fmul(f,b)<0?-(f=fmul(f,b)):(f=fmul(f,b)));
函数调用不会产生这种意外的副作用。
在C/C++中,为什么要使用abs()
或fabs()
求变量的绝对值而不使用下面的代码?
int absoluteValue = value < 0 ? -value : value;
是不是跟低级指令少有关?
假设编译器无法确定 abs() 和条件否定都试图实现相同的目标,条件否定将编译为比较指令、条件跳转指令和移动指令,而 abs() 要么编译成实际的绝对值指令,在支持这种东西的指令集中,要么按位编译,除了符号位之外,一切都保持不变。上面的每条指令通常是 1 个周期,因此使用 abs() 可能至少与条件否定一样快,或者更快(因为编译器可能仍然会识别出您在使用条件否定时试图计算绝对值,并且无论如何生成一个绝对值指令)。即使编译后的代码没有变化,abs() 仍然比条件否定更具可读性。
首先想到的是可读性。
比较这两行代码:
int x = something, y = something, z = something;
// Compare
int absall = (x > 0 ? x : -x) + (y > 0 ? y : -y) + (z > 0 ? z : -z);
int absall = abs(x) + abs(y) + abs(z);
您建议的 "conditional abs" 不等同于浮点数的 std::abs
(或 fabs
),请参见
#include <iostream>
#include <cmath>
int main () {
double d = -0.0;
double a = d < 0 ? -d : d;
std::cout << d << ' ' << a << ' ' << std::abs(d);
}
输出:
-0 -0 0
鉴于 -0.0
和 0.0
表示相同的实数“0”,此差异可能重要也可能无关紧要,具体取决于结果的使用方式。但是,IEEE754 规定的 abs 函数要求结果的符号位为 0,这将禁止结果 -0.0
。我个人认为任何用于计算一些 "absolute value" 的东西都应该符合这种行为。
对于整数,两种变体在运行时和行为上都是等效的。 (Live example)
但由于 std::abs
(或合适的 C 等价物)被认为是正确且更易于阅读的,因此您应该始终更喜欢它们。
编译器很可能会在底层为两者做同样的事情——至少是一个现代的有能力的编译器。
但是,至少对于浮点数,如果要处理无穷大、非数字 (NaN)、负零等所有特殊情况,您最终将需要编写几十行代码。
而且更容易理解 abs
取绝对值比阅读如果它小于零则取反更容易。
如果编译器是 "stupid",它很可能最终会为 a = (a < 0)?-a:a
编写更糟糕的代码,因为它会强制执行 if
(即使它是隐藏的),这很可能比该处理器上的内置浮点 abs 指令差(除了特殊值的复杂性)
Clang(6.0 预发行版)和 gcc (4.9.2) 都会为第二种情况生成更糟糕的代码。
我写了这个小样本:
#include <cmath>
#include <cstdlib>
extern int intval;
extern float floatval;
void func1()
{
int a = std::abs(intval);
float f = std::abs(floatval);
intval = a;
floatval = f;
}
void func2()
{
int a = intval < 0?-intval:intval;
float f = floatval < 0?-floatval:floatval;
intval = a;
floatval = f;
}
clang 为 func1 编写此代码:
_Z5func1v: # @_Z5func1v
movl intval(%rip), %eax
movl %eax, %ecx
negl %ecx
cmovll %eax, %ecx
movss floatval(%rip), %xmm0 # xmm0 = mem[0],zero,zero,zero
andps .LCPI0_0(%rip), %xmm0
movl %ecx, intval(%rip)
movss %xmm0, floatval(%rip)
retq
_Z5func2v: # @_Z5func2v
movl intval(%rip), %eax
movl %eax, %ecx
negl %ecx
cmovll %eax, %ecx
movss floatval(%rip), %xmm0
movaps .LCPI1_0(%rip), %xmm1
xorps %xmm0, %xmm1
xorps %xmm2, %xmm2
movaps %xmm0, %xmm3
cmpltss %xmm2, %xmm3
movaps %xmm3, %xmm2
andnps %xmm0, %xmm2
andps %xmm1, %xmm3
orps %xmm2, %xmm3
movl %ecx, intval(%rip)
movss %xmm3, floatval(%rip)
retq
g++ func1:
_Z5func1v:
movss .LC0(%rip), %xmm1
movl intval(%rip), %eax
movss floatval(%rip), %xmm0
andps %xmm1, %xmm0
sarl , %eax
xorl %eax, intval(%rip)
subl %eax, intval(%rip)
movss %xmm0, floatval(%rip)
ret
g++ func2:
_Z5func2v:
movl intval(%rip), %eax
movl intval(%rip), %edx
pxor %xmm1, %xmm1
movss floatval(%rip), %xmm0
sarl , %eax
xorl %eax, %edx
subl %eax, %edx
ucomiss %xmm0, %xmm1
jbe .L3
movss .LC3(%rip), %xmm1
xorps %xmm1, %xmm0
.L3:
movl %edx, intval(%rip)
movss %xmm0, floatval(%rip)
ret
请注意,两种情况在第二种形式中都明显更复杂,而在 gcc 情况下,它使用了一个分支。 Clang 使用了更多指令,但没有分支。我不确定哪种处理器型号的速度更快,但很明显指令越多越好。
考虑一下,您可以将复杂的表达式输入 abs()
。如果你用expr > 0 ? expr : -expr
编码,你必须将整个表达式重复三次,它会被计算两次。
此外,这两个结果(冒号前后)可能会变成不同类型(如 signed int
/ unsigned int
),这会禁用 return 语句中的使用。
当然,你可以添加一个临时变量,但这只能解决部分问题,而且也好不到哪里去。
在给定的体系结构上,可能存在比条件分支更高效的低级实现。例如,CPU 可能有一个 abs
指令,或者一种在没有分支开销的情况下提取符号位的方法。假设算术右移可以填充寄存器 r 如果数字为负则为 -1,如果为正则为 0,abs x
可能变为 (x+r)^r
(并且看到
Mats Petersson 的回答,g++ 实际上在 x86 上执行此操作)。
其他答案已经讨论了 IEEE 浮点数的情况。
试图告诉编译器执行条件分支而不是信任库可能是不成熟的优化。
Why use abs() or fabs() instead of conditional negation?
已经陈述了各种原因,但考虑到条件代码的优势,因为 abs(INT_MIN)
应该避免。
当寻求整数的负绝对值
时,使用条件代码代替abs()
是有充分理由的
// Negative absolute value
int nabs(int value) {
return -abs(value); // abs(INT_MIN) is undefined behavior.
}
int nabs(int value) {
return value < 0 ? value : -value; // well defined for all `int`
}
当需要一个正的绝对函数并且 value == INT_MIN
是一个真正的可能性时,abs()
,尽管它的清晰度和速度都无法满足极端情况。多种选择
unsigned absoluteValue = value < 0 ? (0u - value) : (0u + value);
abs() 背后的意图是“(无条件地)将这个数字的符号设置为正数”。即使必须根据数字的当前状态将其实现为条件,将其视为简单的 "do this" 而不是更复杂的 "if… this… that" 可能更有用。
...你会把它变成一个宏,你可以有多个你可能不想要的评估(副作用)。考虑:
#define ABS(a) ((a)<0?-(a):(a))
并使用:
f= 5.0;
f=ABS(f=fmul(f,b));
这将扩展到
f=((f=fmul(f,b)<0?-(f=fmul(f,b)):(f=fmul(f,b)));
函数调用不会产生这种意外的副作用。