c++ - ¿Deberíamos generalmente utilizar literales flotantes para flotadores en lugar de los dobles literales más simples?
floating-point double (7)
De la Norma C ++ (Borrador de Trabajo) , sección 5 sobre operadores binarios
Muchos operadores binarios que esperan operandos de tipo aritmético o de enumeración provocan conversiones y producen tipos de resultados de manera similar. El propósito es generar un tipo común, que también es el tipo del resultado. Este patrón se denomina conversiones aritméticas habituales, que se definen de la siguiente manera: - Si alguno de los operandos es de tipo de enumeración de alcance (7.2), no se realizan conversiones; Si el otro operando no tiene el mismo tipo, la expresión está mal formada. - Si cualquiera de los operandos es de tipo doble largo, el otro se convertirá en doble largo. - De lo contrario, si cualquiera de los operandos es doble, el otro se convertirá en doble. - De lo contrario, si cualquiera de los operandos es flotante, el otro se convertirá en flotador.
Y también la sección 4.8.
Un prvalue de tipo de punto flotante se puede convertir a un prvalue de otro tipo de punto flotante. Si el valor de origen se puede representar exactamente en el tipo de destino, el resultado de la conversión es esa representación exacta. Si el valor de origen se encuentra entre dos valores de destino adyacentes, el resultado de la conversión es una elección definida por la implementación de cualquiera de esos valores. De lo contrario, el comportamiento no está definido.
El resultado de esto es que puede evitar conversiones innecesarias especificando sus constantes en la precisión dictada por el tipo de destino, siempre que no pierda precisión en el cálculo al hacerlo (es decir, sus operandos son exactamente representables en la precisión del cálculo). tipo de destino).
En C ++ (o quizás solo nuestros compiladores VC8 y VC10) 3.14
es un doble literal y 3.14f
es un literal flotante.
Ahora tengo un colega que declaró:
Deberíamos usar literales flotantes para cálculos de flotación y literales dobles para cálculos dobles, ya que esto podría tener un impacto en la precisión de un cálculo cuando se usan constantes en una calculación.
Específicamente, creo que quiso decir:
double d1, d2;
float f1, f2;
... init and stuff ...
f1 = 3.1415 * f2;
f1 = 3.1415f * f2; // any difference?
d1 = 3.1415 * d2;
d1 = 3.1415f * d2; // any difference?
O, añadido por mí, incluso:
d1 = 42 * d2;
d1 = 42.0f * d2; // any difference?
d1 = 42.0 * d2; // any difference?
De manera más general, el único punto que puedo ver para usar 2.71828183f
es asegurarse de que la constante que estoy tratando de especificar encajará realmente en un flotador (error / advertencia del compilador de lo contrario).
¿Alguien puede arrojar algo de luz sobre esto? ¿Especificas el f
postfix? ¿Por qué?
Para citar de una respuesta lo que implícitamente doy por sentado:
Si está trabajando con una variable flotante y un literal doble, toda la operación se realizará como doble y luego se convertirá de nuevo a flotante.
¿Podría haber algún daño en esto? (Aparte de un impacto de rendimiento muy, muy teórico?)
Edición adicional: sería bueno si las respuestas que contienen detalles técnicos (¡apreciado!) También podrían incluir cómo estas diferencias afectan el código de propósito general . (Sí, si está haciendo cálculos numéricos, probablemente le gustaría asegurarse de que sus operaciones de punto flotante de big-n sean tan eficientes (y correctas) como sea posible, pero ¿es importante para el código de propósito general que se llama varias veces? ¿Es más limpio si el código solo usa 0.0
y omite el - ¿Es difícil de mantener? - ¿Sufijo flotante?)
Hay una diferencia: si utiliza una constante doble y la multiplica por una variable flotante, la variable se convierte primero en doble, el cálculo se realiza en doble y el resultado se convierte en flotante. Si bien la precisión no es realmente un problema aquí, esto podría llevar a confusión.
Hice una prueba.
Compilé este código:
float f1(float x) { return x*3.14; }
float f2(float x) { return x*3.14F; }
Usando gcc 4.5.1 para i686 con optimización -O2.
Este fue el código ensamblador generado para f1:
pushl %ebp
movl %esp, %ebp
subl $4, %esp # Allocate 4 bytes on the stack
fldl .LC0 # Load a double-precision floating point constant
fmuls 8(%ebp) # Multiply by parameter
fstps -4(%ebp) # Store single-precision result on the stack
flds -4(%ebp) # Load single-precision result from the stack
leave
ret
Y este es el código ensamblador generado para f2:
pushl %ebp
flds .LC2 # Load a single-precision floating point constant
movl %esp, %ebp
fmuls 8(%ebp) # Multiply by parameter
popl %ebp
ret
Así que lo interesante es que para f1, el compilador almacenó el valor y lo volvió a cargar solo para asegurarse de que el resultado se truncara a precisión simple.
Si usamos la opción -ffast-math, entonces esta diferencia se reduce significativamente:
pushl %ebp
fldl .LC0 # Load double-precision constant
movl %esp, %ebp
fmuls 8(%ebp) # multiply by parameter
popl %ebp
ret
pushl %ebp
flds .LC2 # Load single-precision constant
movl %esp, %ebp
fmuls 8(%ebp) # multiply by parameter
popl %ebp
ret
Pero todavía existe la diferencia entre cargar una constante de precisión simple o doble.
Actualización para 64 bits
Estos son los resultados con gcc 5.2.1 para x86-64 con optimización -O2:
f1:
cvtss2sd %xmm0, %xmm0 # Convert arg to double precision
mulsd .LC0(%rip), %xmm0 # Double-precision multiply
cvtsd2ss %xmm0, %xmm0 # Convert to single-precision
ret
f2:
mulss .LC2(%rip), %xmm0 # Single-precision multiply
ret
Con la matemática rápida, los resultados son los mismos.
Normalmente, no creo que haga ninguna diferencia, pero vale la pena señalar que 3.1415f
y 3.1415f
(por lo general) no son iguales. Por otro lado, normalmente no haces ningún cálculo en float
todos modos, al menos en las plataformas habituales. (el double
es igual de rápido, si no más rápido). Casi la única vez que debería ver float
es cuando hay matrices grandes, e incluso entonces, todos los cálculos se harán normalmente en double
.
Personalmente tiendo a usar la notación f postfix como una cuestión de principios y para que sea tan obvio como pueda que este es un tipo flotante en lugar de un doble.
Mis dos centavos
Sí, deberías usar el sufijo f
. Las razones incluyen:
Actuación. Cuando escribes
float foo(float x) { return x*3.14; }
float foo(float x) { return x*3.14; }
, obligas al compilador a emitir código que convierte x en doble, luego hace la multiplicación y luego convierte el resultado de nuevo a simple. Si agrega el sufijof
, se eliminan ambas conversiones. En muchas plataformas, cada una de esas conversiones es tan costosa como la multiplicación misma.Rendimiento (continuación). Hay plataformas (la mayoría de los teléfonos celulares, por ejemplo), en las que la aritmética de doble precisión es dramáticamente más lenta que la precisión simple. Incluso ignorando la sobrecarga de conversión (cubierta en 1), cada vez que obliga a que un cálculo se evalúe al doble, ralentiza el programa. Esto no es solo una cuestión "teórica".
Reduce tu exposición a los insectos. Considere el ejemplo
float x = 1.2; if (x == 1.2) // something;
float x = 1.2; if (x == 1.2) // something;
¿Se ejecutasomething
? No, no lo es, porque x mantiene1.2
redondeado a unfloat
, pero se está comparando con el valor de precisión doble1.2
. Los dos no son iguales.
Sospecho algo como esto: si está trabajando con una variable flotante y un literal doble, toda la operación se realizará como doble y luego se convertirá de nuevo en flotante.
Si utiliza un literal flotante, teóricamente, el cálculo se realizará con una precisión de flotación, aunque algunos equipos lo convertirán al doble de todos modos para realizar el cálculo.