为什么要使用 abs() 或 fabs() 而不是条件取反?

57

在C/C++中,为什么要使用abs()fabs()来查找变量的绝对值而不使用以下代码?

int absoluteValue = value < 0 ? -value : value;

这是否与较低层级的指令数量有关?


25
为什么会同时使用C/C++? - user2736738
26
提高可读性和编译器优化。 - iBug
11
abs()是一种预定义的、广为人知的函数,所以几乎所有程序员都会使用它。这导致了代码的统一性。 - H.H
33
为什么要重新发明已经被测试并添加到标准库的东西,尤其是当你可能会出错或忽略特殊情况时? - codebender
13
为什么你不使用abs或fabs? - user253751
显示剩余11条评论
9个回答

124
你提出的"条件绝对值"与浮点数的std::abs(或fabs)不等价,详见:

e.g.


#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.00.0表示相同的实数“0”,这种差异可能重要也可能不重要,具体取决于结果的使用方式。然而,IEEE754规定的abs函数必须使结果的符号位为0,这将禁止出现-0.0的结果。我个人认为,任何用于计算“绝对值”的东西都应该遵循这种行为。

对于整数,这两种变量在运行时间和行为上都是等效的。(实时示例

但是,由于已知std::abs(或相应的C语言等效函数)是正确且易于阅读的,因此您应该始终优先选择它们。


18
在浮点数中,“负零”是真正需要考虑的事情。 - iBug
6
这个答案很有用。但是对于像abs()这样的函数,即使是标准实现也不完美。 INT_MIN < 0 && abs(INT_MIN) < 0 是正确的。 - llllllllll
16
在C++中,即使是abs(INT_MIN),也被视为未定义行为,这在语言本身上是一种基本的限制。如果您希望将返回类型更改为与源类型匹配(例如,在IEEE754中指定),那么无论如何都不能解决输入为INT_MIN时的运行时问题。因此,需要修复返回类型以匹配源类型。 - Baum mit Augen
2
@BaummitAugen 你说得对,即使在 C99 中也是未定义行为 http://port70.net/~nsz/c/c99/n1256.html#7.20.6.1p2。 - llllllllll
2
@Nebr 这确实是其中一个有区别的情况。https://wandbox.org/permlink/37Xk8h7qSJbHqFxl - Baum mit Augen
显示剩余4条评论

88

首先想到的是可读性。

比较一下这两行代码:

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);

13
尊重您,但这个低级挑剔的问题获得了很多赞。问题显然不是关于语法,而是实现。你的“问题”很容易解决... 你的答案适用于“为什么我们应该将一行代码封装成函数?”的问题。 - luk32
82
问题是“为什么选择一件事情而不是另一件?”其中一个答案是可读性,因为它们确实有很大的差异。我不会称这为“吹毛求疵”。 - SH7890
4
@SH7890 品味不是问题,因为你可以编写一个功能来防止这种情况,甚至是宏。这只需要一行代码,并且看起来完全相同。以下是提高可读性的解决方案:int cabs(int a) {return a > 0 ? a : -a;}。甚至还有一个提示,说明这是关于实现的。没有人会在每个用例中复制整个实现。加油。 - luk32
47
@luk32 好的,但标准库的作者们已经预见到了你可能想要这样做,所以他们为你编写了那个函数,这样你就不需要自己写了。它被称为 abs。为什么还要费力地编写你自己的 cabs 函数呢?可能有一个原因是你不知道 abs 的存在,但既然你现在知道了,就没有必要再去重复造轮子了。 - user253751
4
@luk32,你理解我的意思了。阅读性肯定不是问题,因为我可以用那行代码来定义宏/函数。我在寻找底层等效性或不对称性。 - Subhranil
显示剩余4条评论

30

编译器在底层最可能做的事情是相同的 - 至少是现代的称职编译器。

然而,至少对于浮点数,如果你想处理所有特殊情况,如无穷大、非数字 (NaN)、负零等,你将需要编写几十行代码。

同时,将绝对值操作表述为 abs 要比表述为 "如果小于零,则取反" 更易读。

如果编译器比较"愚蠢",那么对于 a = (a < 0)?-a:a 这种形式的代码,它可能生成更糟糕的代码,因为它会强制执行一个 if 判断(即使这个判断被隐藏了),而且除了特殊值的复杂性之外,这可能比处理器上内置的浮点数 abs 指令更糟糕。

Clang (6.0-pre-release) 和 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    $31, %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    $31, %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使用更多指令,但没有分支语句。我不确定哪种方式在哪种处理器模型上更快,但很明显,更多的指令通常不是更好的选择。


23
这篇回答说一个现代能力强的编译器很可能会为两者做相同的事情,然后展示了汇编代码,证明所选的编译器没有做相同的事情。这是一条冲突、令人困惑的信息。所选的编译器是无能还是不够现代?为什么要以它们作为例子呢?或者是“现代能力强的编译器很可能会为两者做相同的事情”的说法不正确?为什么会有生成的代码之间的差异? - Eric Postpischil
10
相当明显,更多的指令并不总是更好。”我持有不同意见,特别是当与分支进行比较时。它会影响预取和乱序执行。它很复杂,可能不安全被泛化。 - luk32
1
这是在-O3优化级别下吗? - Calchas
@EricPostpischil:我先写了答案,然后尝试了一下,发现它几乎相同,但并不完全相同——特别是在“and look what the compiler actually does”之前的最后一句话——在gcc情况下,条件代码实际上会进行分支,而在abs代码中则避免了这种情况。 - Mats Petersson
2
@Calchas:我使用的是-O2,这是我通常用于“优化”的代码。但是,-O3 对代码没有任何明显的改变。一个更新版本的 gcc 可能会有所不同——但我现在没有其中任何一个——我的计算机即将升级,所以也会得到一个新的编译器 [今天早上它没有启动,今晚让它正常运行了,替换零件正在途中]。 - Mats Petersson
显示剩余4条评论

13

为什么要使用abs()或fabs()而不是条件否定?

已经阐述了各种原因,但考虑到条件代码的优势,应避免使用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);

3
对于 abs(INT_MIN),使用 +1 是未定义行为。我之前并不知道这一点。为什么库的实现者会使其变成未定义行为呢? - manav m-n
3
考虑到可能的结果是abs(INT_MIN) --> INT_MIN,或者是abs(INT_MIN) --> INT_MAX,或者程序崩溃等等。没有一种结果是普遍优选的,因此最好让实现在处理其他值时更快,并让 abs(INT_MIN) --> UB 来涵盖所有实现。 同意不同意 - chux - Reinstate Monica
6
C语言规范规定这是未定义行为。"如果结果不能被表示,那么行为是未定义的。" §7.22.6.1 2 - chux - Reinstate Monica
1
@chux,abs(INT_MIN) 只有在二进制补码算术下才是未定义行为。在符号-大小算术下(虽然现在没有人使用它),它是完全定义良好的。 - Mark
1
@TrevorPowell:您的假设是错误的,(0u - value) 是使用提升为 unsigned int 进行计算,并且结果是按模计算的,就像所有无符号算术一样。 - Ben Voigt
显示剩余5条评论

7
在某个架构上,可能有比条件分支更高效的低级实现。例如,CPU 可能有一个 abs 指令或一种提取符号位的方法,而不需要分支开销。假设算术右移可以将寄存器 r 填充为 -1(如果数字为负数)或 0(如果数字为正数),abs x 可以变成 (x+r)^r(根据Mats Petersson的回答,g++在x86上实际上就是这样做的)。其他答案已经讨论了IEEE浮点数的情况。试图告诉编译器执行条件分支而不是信任库可能是过早的优化。

4
考虑到您可以将一个复杂的表达式输入 abs()。如果您使用 expr > 0 ? expr : -expr 的方法编码,则需要重复整个表达式三次,并且它将被评估两次。
此外,冒号前后的两个结果可能是不同类型的(如 signed int / unsigned int),这将禁用在返回语句中的使用。 当然,您可以添加一个临时变量,但这只解决了部分问题,并且在任何方面都不更好。

这可以通过初始化一个临时变量轻松解决。如果求值两次,具有副作用的参数将给出不正确的结果!abs(printf("hello, world!\n")) - Davislor
这个问题确实指定了变量的绝对值,因此副作用与这个特定的问题无关。 - pipe
它将被评估两次,而不是三次。 a?b:c 只评估 bc 中的一个。 - L. F.

4

如果你把它变成一个宏,你可能会有多个评估,而你可能不希望这样(副作用)。请考虑:

#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)));

函数调用不会产生意外的副作用。


2
真的,但基本上已经被https://dev59.com/yFYM5IYBdhLWcg3wnBFY#48611734所说了。我也很好奇谁首先提到了宏。 :P - underscore_d

3
假设编译器不能确定abs()和条件否定都试图实现相同的目标,那么条件否定将编译为比较指令、条件跳转指令和移动指令,而abs()要么编译成实际的绝对值指令(如果指令集支持这样的东西),要么编译成保持所有内容不变(除了符号位)的按位与。以上每个指令通常都是1个周期,因此使用abs()很可能至少与条件否定一样快,甚至更快(因为编译器可能仍然会认识到你在使用条件否定时试图计算绝对值,并生成一个绝对值指令)。即使编译后的代码没有更改,abs()仍比条件否定更易读。

7
只要编译器不足聪明以至于无法识别获取绝对值的意图,就可以这样做。 - iBug
2
不是这样的。这取决于编译器。GCC为两种情况生成等效的代码。 - user1143634
@Ivan 使abs()成为固有的比识别条件否定等同于绝对值更容易。 - Cpp plus 1
@Ivan 如果它不是内在的,那么符号位就会被清除。 - Cpp plus 1
@Cppplus1 这并不意味着编译器会这样做。大多数编译器对内部函数的优化效果非常差,没有人会关心它们。 - user1143634
显示剩余4条评论

3

abs()函数的意图是“(无条件地)将这个数的符号设为正数”。即使必须基于数字的当前状态来实现这一点,能够将其视为简单的“执行此操作”而不是更复杂的“如果……那么……”可能更有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接