c++ floating-point double literals

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:

  1. 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 sufijo f , se eliminan ambas conversiones. En muchas plataformas, cada una de esas conversiones es tan costosa como la multiplicación misma.

  2. 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".

  3. 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 ejecuta something ? No, no lo es, porque x mantiene 1.2 redondeado a un float , pero se está comparando con el valor de precisión doble 1.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.