c++ - labs - ¿Por qué usar abs() o fabs() en lugar de negación condicional?
int abs (9)
En C / C ++, ¿por qué debería uno usar
abs()
o
fabs()
para encontrar el valor absoluto de una variable sin usar el siguiente código?
int absoluteValue = value < 0 ? -value : value;
¿Tiene algo que ver con menos instrucciones en el nivel inferior?
¿Por qué usar abs () o fabs () en lugar de negación condicional?
Ya se han establecido varias razones, sin embargo, considere las ventajas del código condicional, ya que se deben evitar los
abs(INT_MIN)
.
Hay una buena razón para usar el código condicional en lugar de
abs()
cuando se busca el valor absoluto
negativo
de un entero
// 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`
}
Cuando se necesita una función absoluta positiva y el
value == INT_MIN
es una posibilidad real,
abs()
, para toda su claridad y velocidad falla un caso de esquina.
Varias alternativas
unsigned absoluteValue = value < 0 ? (0u - value) : (0u + value);
... y si lo convierte en una macro, puede tener múltiples evaluaciones que quizás no desee (efectos secundarios). Considerar:
#define ABS(a) ((a)<0?-(a):(a))
y use:
f= 5.0;
f=ABS(f=fmul(f,b));
que se expandiría a
f=((f=fmul(f,b)<0?-(f=fmul(f,b)):(f=fmul(f,b)));
Las llamadas a funciones no tendrán estos efectos secundarios no deseados.
La intención detrás de abs () es "(incondicionalmente) establecer el signo de este número en positivo". Incluso si eso tuviera que implementarse como un condicional basado en el estado actual del número, probablemente sea más útil poder considerarlo como un simple "hacer esto", en lugar de un "si ... esto ... eso" más complejo .
Lo más probable es que el compilador haga lo mismo para ambos en la capa inferior, al menos un compilador competente moderno.
Sin embargo, al menos para el punto flotante, terminarás escribiendo unas pocas docenas de líneas si quieres manejar todos los casos especiales de infinito, no un número (NaN), cero negativo, etc.
Además, es más fácil leer que
abs
está tomando el valor absoluto que leer que si es menor que cero, lo niega.
Si el compilador es "estúpido", puede terminar haciendo peor código para
a = (a < 0)?-a:a
, porque fuerza un
if
(incluso si está oculto), y eso podría ser peor que el instrucciones de abs de coma flotante incorporadas en ese procesador (aparte de la complejidad de los valores especiales)
Tanto Clang (6.0-pre-release) como gcc (4.9.2) generan un código PEOR para el segundo caso.
Escribí esta pequeña muestra:
#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 hace este código para 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
Tenga en cuenta que ambos casos son notablemente más complejos en la segunda forma, y en el caso de gcc, utiliza una rama. Clang usa más instrucciones, pero no rama. No estoy seguro de cuál es más rápido en qué modelos de procesador, pero claramente más instrucciones rara vez son mejores.
Lo primero que viene a la mente es la legibilidad.
Compare estas dos líneas de códigos:
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);
Los "abdominales condicionales" que usted propone no son equivalentes a
std::abs
(o
fabs
) para números de coma flotante, vea por ejemplo
#include <iostream>
#include <cmath>
int main () {
double d = -0.0;
double a = d < 0 ? -d : d;
std::cout << d << '' '' << a << '' '' << std::abs(d);
}
salida:
-0 -0 0
Dado que
-0.0
y
0.0
representan el mismo número real ''0'', esta diferencia puede o no importar, dependiendo de cómo se use el resultado.
Sin embargo, la función abs especificada por IEEE754 exige que el signo del resultado sea 0, lo que prohibiría el resultado
-0.0
.
Personalmente, creo que cualquier cosa utilizada para calcular algún "valor absoluto" debería coincidir con este comportamiento.
Para enteros, ambas variantes serán equivalentes tanto en tiempo de ejecución como en comportamiento. ( Ejemplo en vivo )
Pero como se sabe que
std::abs
(o los equivalentes C adecuados) son correctos y más fáciles de leer, siempre debe preferirlos.
Puede haber una implementación de bajo nivel más eficiente que una rama condicional, en una arquitectura dada.
Por ejemplo, la CPU podría tener una instrucción
abs
, o una forma de extraer el bit de signo sin la sobrecarga de una rama.
Suponiendo que un desplazamiento aritmético a la derecha puede llenar un registro
r
con -1 si el número es negativo, o 0 si es positivo,
abs x
podría convertirse en
(x+r)^r
(y al ver la respuesta de Mats Petersson, g ++ realmente hace esto en x86).
Otras respuestas han explicado la situación del punto flotante IEEE.
Intentar decirle al compilador que realice una ramificación condicional en lugar de confiar en la biblioteca es probablemente una optimización prematura.
Suponiendo que el compilador no podrá determinar que tanto abs () como la negación condicional intentan alcanzar el mismo objetivo, la negación condicional se compila en una instrucción de comparación, una instrucción de salto condicional y una instrucción de movimiento, mientras que abs () tampoco compila a una instrucción de valor absoluto real, en conjuntos de instrucciones que admiten tal cosa, o un bit a bit y que sigue todo igual, excepto el bit de signo. Cada instrucción anterior es típicamente de 1 ciclo, por lo que es probable que el uso de abs () sea al menos tan rápido o más rápido que la negación condicional (ya que el compilador aún puede reconocer que está intentando calcular un valor absoluto cuando usa la negación condicional, y generar una instrucción de valor absoluto de todos modos). Incluso si no hay cambios en el código compilado, abs () es aún más legible que la negación condicional.
Tenga en cuenta que podría alimentar una expresión complicada en
abs()
.
Si lo codifica con
expr > 0 ? expr : -expr
expr > 0 ? expr : -expr
, debe repetir la expresión completa tres veces, y se evaluará dos veces.
Además, los dos resultados (antes y después de los dos puntos) pueden ser de diferentes tipos (como
signed int
/
unsigned int
), lo que desactiva el uso en una declaración de devolución.
Por supuesto, podría agregar una variable temporal, pero eso resuelve solo partes de ella, y tampoco es mejor de ninguna manera.