【问题标题】:Why use abs() or fabs() instead of conditional negation?为什么使用 abs() 或 fabs() 而不是条件否定?
【发布时间】:2018-07-14 12:11:12
【问题描述】:

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

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

是不是跟底层指令少有关?

【问题讨论】:

  • c/c++ 为什么?为什么你都这样用?
  • 更好的可读性和编译器优化。
  • abs() 是一个预定义且众所周知的函数,因此几乎所有程序员都使用它。这导致代码的统一性。
  • 为什么要重新发明已经过测试并添加到标准库中的东西,尤其是当您可能会出错或错过特殊情况时?
  • 为什么你使用 abs 或 fabs?

标签: c++ c variables negation absolute-value


【解决方案1】:

您建议的“条件绝对值”不等同于浮点数的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(或合适的 C 等效项)是正确且易于阅读的,因此您应该始终更喜欢那些。

【讨论】:

  • 浮点中的“负零”确实需要考虑。
  • 这个答案很丰富。然而,对于像abs() 这样的功能,即使是标准的实现也不完美。 INT_MIN &lt; 0 &amp;&amp; abs(INT_MIN) &lt; 0 是真的。
  • @liliscent abs(INT_MIN) 甚至是 UB,至少在 C++ 中是这样,但这是该语言的基本限制。您想修复返回类型以匹配源类型(例如,在 IEEE754 中指定),因此输入为 INT_MIN 的运行时问题无论如何都无法真正解决。
  • @BaummitAugen 你是对的,即使在 C99 中也是 UB port70.net/~nsz/c/c99/n1256.html#7.20.6.1p2
  • 我不知道这是否真的会发生,但是如果你将 1 除以 0 并得到无穷大,那么在将 1 除以 -.0 时你可能会得到 -infinity。
【解决方案2】:

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

【讨论】:

  • +1 for abs(INT_MIN) 是未定义的行为。我不知道。为什么库实现者将其设为未定义?
  • @manavm-n 考虑可能的结果是abs(INT_MIN) --&gt; INT_MIN,或abs(INT_MIN) --&gt; INT_MAX 或程序死亡等。没有一个是普遍首选的,所以最好允许实现快速使用其他值和让abs(INT_MIN) --&gt; UB 接受所有实现。 Agree to disagree
  • @manavm-n C 规范规定它是 UB。 “如果结果无法表示,则行为未定义。” §7.22.6.1 2
  • @chux, abs(INT_MIN) 只是二进制恭维算法下的未定义行为。在符号幅度算术下(不再有人使用它),它的定义非常好。
  • @TrevorPowell:你猜错了,(0u - value) 是通过提升到unsigned int 执行的,结果是模计算的,就像所有无符号算术一样。
【解决方案3】:

...如果你把它做成一个宏,你可以有多个你可能不想要的评估(副作用)。考虑:

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

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

【讨论】:

【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

    【讨论】:

    • ... 前提是编译器愚蠢到无法识别获取绝对值的意图。
    • 不,不是。取决于编译器。 GCC 为这两种情况生成等效代码。
    • @Ivan 这就是为什么我说“至少一样快”
    • @Cppplus1 如果编译器不将abs 视为内在的,它可能会更慢,所以不会。在实践中abs?: 版本没有区别。
    • @Ivan 使 abs() 内在比认识到条件否定与绝对值相同更容易。
    【解决方案6】:

    在给定的架构上,可能存在比条件分支更高效的低级实现。例如,CPU 可能有一条abs 指令,或者一种无需分支开销即可提取符号位的方法。假设算术右移可以用 -1 填充寄存器 r 如果数字是负数,或者 0 如果数字是正数,abs x 可能变成(x+r)^r(并且看到 Mats Petersson 的回答,g++ 实际上在 x86 上执行此操作)。

    其他答案已经解决了 IEEE 浮点的情况。

    试图告诉编译器执行条件分支而不是信任库可能是过早的优化。

    【讨论】:

      【解决方案7】:

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

      【讨论】:

      • 这很容易通过初始化一个临时变量来解决。如果评估两次,具有副作用的论点会给出不正确的结果! abs(printf("hello, world!\n"))?
      • 该问题确实指定了 a variable 的绝对值,因此副作用与该特定问题无关。
      • 它将被评估两次,而不是三次。 a ? b : c 仅评估 bc 之一。
      【解决方案8】:

      编译器很可能会在底层为两者做同样的事情——至少是现代的称职的编译器。

      但是,至少对于浮点数,如果要处理无穷大、非数字 (NaN)、负零等所有特殊情况,您最终会写几十行代码。

      abs 取绝对值比读取它小于零的值更容易阅读。

      如果编译器是“愚蠢的”,它最终可能会为a = (a &lt; 0)?-a:a 编写更糟糕的代码,因为它会强制使用if(即使它是隐藏的),而且这很可能比内置的浮动代码更糟糕在该处理器上点 abs 指令(除了特殊值的复杂性)

      Clang (6.0-pre-release) 和 gcc (4.9.2) 都会为第二种情况生成 WORSE 代码。

      我写了这个小样本:

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

      【讨论】:

      • 这个答案说现代的称职的编译器很可能会为两者做同样的事情,然后显示汇编代码证明所选的编译器没有做同样的事情。这是一个矛盾的、令人困惑的信息。选择的编译器是不称职的还是不现代的?那为什么要用它们作为例子呢?或者说现代有能力的编译器很可能对两者都做同样的事情是不正确的?为什么生成的代码会有差异?
      • 很明显,更多的指令很少会更好。”我不敢苟同,尤其是当您与分支进行比较时。它会触发预取和乱序执行。它很复杂,概括起来可能不安全。
      • 这是-O3吗?
      • @EricPostpischil,对于整数情况,clang 为两者生成了相同的代码。 gcc 的代码略有不同,abs() 版本在单个操作中计算并存储结果,而条件版本将结果计算到寄存器中,然后将其复制到内存中。
      • @Calchas:我使用了 -O2,这是我通常用于“优化”代码的方法。但是 -O3 并没有以任何明显的方式改变代码。较新版本的 gcc 可能会有所不同 - 但我现在没有这些 - 我的计算机即将升级,因此也将获得一个新的编译器 [今天早上没有开始,开始了晚上,在路上替换位]。
      【解决方案9】:

      首先想到的是可读性。

      比较这两行代码:

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

      【讨论】:

      • 哇,恕我直言,但对于低位挑剔的人来说,这是很多赞成票。问题显然不在于语法,而在于实现。您的“问题”解决起来微不足道……而您的回答很适合“我们为什么要将单行代码包装到函数中?”
      • @luk32 问题是“为什么选择一件事而不是另一件事?”一个答案是可读性,因为它们确实有很大不同。我不会称其为“挑剔”。
      • @SH7890 可读性不是问题,因为您可以编写一个函数来防止这种情况,或者甚至是一个宏。它需要一行,并且看起来完全一样。这是可读性的修复:int cabs(int a) {return a &gt; 0 ? a : -a;}。甚至有一个提示,它是关于实施的。没有人会在每个用例上复制粘贴整个实现。来吧。
      • @luk32 当然可以,但是标准库的作者很有帮助地预见到您可能想要这样做,因此他们为您编写了该函数,因此您无需自己编写。它被称为abs。为什么要编写自己的 cabs 函数来做同样的事情呢?一个原因可能是你不知道abs 的存在,但既然你现在知道了,你不会从吐槽中得到什么。
      • @luk32 你明白我的意思。可读性当然不是问题,因为我可以用那行代码定义宏/函数。我正在寻找较低级别的等价或不对称。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-02
      • 2011-03-08
      • 1970-01-01
      • 2023-03-30
      • 2014-03-26
      • 1970-01-01
      • 2017-06-14
      相关资源
      最近更新 更多