c++ 为什么要用abs()或fabs()来代替条件否定?

cdmah0mi  于 2022-12-27  发布在  其他
关注(0)|答案(9)|浏览(182)

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

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

这是否与较低层次的指令较少有关?

xlpyo6sf

xlpyo6sf1#

你提出的"条件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.00.0表示相同的实数"0",这个差异可能重要也可能不重要,这取决于结果的使用方式。然而,IEEE754规定的abs函数要求结果的符号位为0,这将禁止结果-0.0。我个人认为任何用于计算一些"绝对值"的东西都应该与这种行为相匹配。
对于整数,两个变量在运行时和行为上都是等效的。(Live example
但是,由于std::abs(或fitting C的等价形式)是正确的,而且更容易阅读,因此您应该总是更喜欢这些形式。

gijlo24d

gijlo24d2#

首先想到的是可读性。
比较这两行代码:

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

ewm0tg9j3#

编译器最有可能在底层为这两个编译器做同样的事情--至少是一个现代的有能力的编译器。
但是,至少对于浮点数,如果要处理无穷大、非数字(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为func 1生成以下代码:

_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++函数1:

_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++函数2:

_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使用了更多的指令,但没有分支;我不确定在哪种处理器型号上哪个更快,但很明显,更多的指令很少更好。

vktxenjb

vktxenjb4#

为什么要用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);
tuwxkamq

tuwxkamq5#

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

6tqwzwtp

6tqwzwtp6#

假设您可以将一个复杂的表达式输入到abs()中,如果使用expr > 0 ? expr : -expr编写代码,则必须将整个表达式重复三次,并且它将被计算两次。
此外,两个结果(冒号前后)可能是不同的类型(如signed int/unsigned int),这就禁止了在return语句中使用。当然,您可以添加一个临时变量,但这只能解决部分问题,无论如何也不会更好。

qhhrdooz

qhhrdooz7#

......如果您将其制作为宏,则可能会有多个您可能不希望的求值(副作用)。请考虑:

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

函数调用不会有这种意想不到的副作用。

8ehkhllq

8ehkhllq8#

假设编译器无法确定两个abs()和条件否定试图实现相同的目标,条件否定编译为比较指令、条件跳转指令和移动指令,而abs()或者编译成支持实际绝对值指令的指令集中的实际绝对值指令,或者编译成保持所有内容相同的逐位指令,除了符号位。上面的每个指令通常是1个周期,因此使用abs()可能至少与条件否定一样快,或比条件否定更快(由于编译器可能仍然识别出在使用条件否定时您正试图计算绝对值,并且无论如何都生成绝对值指令)。即使编译后的代码没有变化,abs()仍然比条件否定更可读。

tnkciper

tnkciper9#

abs()背后的意图是“(无条件地)将这个数的符号设置为正数”,即使这必须根据数的当前状态作为条件来实现,但将其视为简单的“do this”可能更有用,而不是更复杂的“if... this... that”。

相关问题