Зачем использовать abs() или fabs() вместо условного отрицания?

В C/C++, почему нужно использовать abs() или fabs() чтобы найти абсолютное значение переменной без использования следующего кода?

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

Это связано с меньшим количеством инструкций на более низком уровне?

Ответы

Ответ 1

Предложенный вами "условный 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', эта разница может иметь или не иметь значения, в зависимости от того, как используется результат. Однако функция abs, как указано в IEEE754, предписывает, чтобы знаковый бит результата равнялся 0, что запрещало бы результат -0.0. Я лично думаю, что все, что используется для вычисления некоторой "абсолютной величины", должно соответствовать этому поведению.

Для целых чисел оба варианта будут эквивалентны как во время выполнения, так и в поведении. (Живой пример)

Но поскольку известно, что std::abs (или подходящие эквиваленты C) верны и их легче читать, вы всегда должны их предпочитать.

Ответ 2

Первое, что приходит на ум, - читаемость.

Сравните эти две строки кодов:

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

Ответ 3

Компилятор, скорее всего, сделает то же самое для обоих на нижнем уровне - по крайней мере, для современного компетентного компилятора.

Однако, по крайней мере, для с плавающей запятой вам будет написано несколько десятков строк, если вы хотите обрабатывать все особые случаи бесконечности, а не -a -number (NaN), отрицательный ноль и т.д.

Так же, как легко читать, abs принимает абсолютное значение, чем чтение, если оно меньше нуля, отрицайте его.

Если компилятор "глупый", вполне возможно, что он делает хуже код для a = (a < 0)?-a:a, потому что он заставляет if (даже если он скрыт), и это может быть хуже, чем встроенная команда абс. с плавающей запятой на этом процессоре (помимо сложности специальных значений)

Оба 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 используется ветка. Кланг использует больше инструкций, но не имеет ветки. Я не уверен, что быстрее, чем у процессоров, но довольно ясно, что больше инструкций редко бывает лучше.

Ответ 4

Зачем использовать 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);

Ответ 5

Возможно, более эффективная реализация на низком уровне, чем условная ветвь, в данной архитектуре. Например, CPU может иметь команду abs или способ извлечь бит знака без накладных расходов ветки. Предположим, что арифметический сдвиг вправо может заполнить регистр r с помощью -1, если число отрицательно, или 0, если положительно, abs x может стать (x+r)^r (и, увидев ответ Маца Петерссона, g++ на самом деле это делает x86).

Другие ответы затронули ситуацию с плавающей точкой IEEE.

Попытка сказать компилятору выполнить условную ветку вместо того, чтобы доверять библиотеке, вероятно, является преждевременной оптимизацией.

Ответ 6

Подумайте, что вы можете комбинировать сложное выражение в abs(). Если вы кодируете его с помощью expr > 0? expr: -expr expr > 0? expr: -expr, вы должны повторить все выражение три раза, и оно будет оцениваться два раза.
Кроме того, два результата (до и после двоеточия) могут оказаться разных типов (например, signed int/unsigned int), что отключает использование в операторе return. Конечно, вы могли бы добавить временную переменную, но это решает только ее части, и никоим образом не лучше.

Ответ 7

Предполагая, что компилятор не сможет определить, что оба абс() и условное отрицание пытаются достичь той же цели, условное отрицание компилируется в инструкцию сравнения, инструкцию условного перехода и инструкцию перемещения, тогда как abs() либо компилируется в фактическую инструкцию абсолютного значения, в наборах инструкций, которые поддерживают такую вещь, или побитовое и которое сохраняет все то же самое, за исключением знакового бита. Каждая вышеприведенная инструкция обычно представляет собой 1 цикл, поэтому использование abs(), вероятно, будет как минимум быстрым или быстрее условного отрицания (поскольку компилятор может все же признать, что вы пытаетесь вычислить абсолютное значение при использовании условного отрицания и генерировать инструкцию абсолютного значения). Даже если в скомпилированном коде нет изменений, abs() еще более читабельен, чем условное отрицание.

Ответ 8

Цель abs() - "(безоговорочно) установить знак этого числа на положительный". Даже если это должно было быть реализовано как условие, основанное на текущем состоянии числа, вероятно, более полезно уметь думать об этом как о простом "делать это", а не о более сложном "если... это... то",,

Ответ 9

... и вы бы сделали это в макрос, вы можете иметь несколько оценок, которые вы не можете хотеть (побочные эффекты). Рассматривать:

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

Функциональные вызовы не будут иметь этих непреднамеренных побочных эффектов.