Зачем использовать 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)));
Функциональные вызовы не будут иметь этих непреднамеренных побочных эффектов.