¿Cómo pueden dos versiones de la misma función, que solo difieren en que una está en línea y la otra no, devolver valores diferentes? Aquí hay un código que escribí hoy y no estoy seguro de cómo funciona.
#include <cmath>
#include <iostream>
bool is_cube(double r)
{
return floor(cbrt(r)) == cbrt(r);
}
bool inline is_cube_inline(double r)
{
return floor(cbrt(r)) == cbrt(r);
}
int main()
{
std::cout << (floor(cbrt(27.0)) == cbrt(27.0)) << std::endl;
std::cout << (is_cube(27.0)) << std::endl;
std::cout << (is_cube_inline(27.0)) << std::endl;
}
Esperaría que todas las salidas fueran iguales 1
, pero en realidad genera esto (g ++ 8.3.1, sin banderas):
1
0
1
en vez de
1
1
1
Editar: clang ++ 7.0.0 genera esto:
0
0
0
y g ++ -De esto:
1
1
1
==
siempre un poco impredecible con valores de coma flotante?-Ofast
opción que permite tales optimizaciones?cbrt(27.0)
el valor de0x0000000000000840
mientras que la biblioteca estándar devuelve0x0100000000000840
. Los dobles difieren en el número 16 después de la coma. Mi sistema: archlinux4.20 x64 gcc8.2.1 glibc2.28 Comprobado con esto . Me pregunto si gcc o glibc tienen razón.Respuestas:
Explicación
Algunos compiladores (especialmente GCC) utilizan una mayor precisión al evaluar expresiones en tiempo de compilación. Si una expresión depende solo de entradas constantes y literales, se puede evaluar en tiempo de compilación incluso si la expresión no está asignada a una variable constexpr. Si esto ocurre o no depende de:
Si se proporciona explícitamente una expresión, como en el primer caso, tiene menor complejidad y es probable que el compilador la evalúe en el momento de la compilación.
De manera similar, si una función está marcada en línea, es más probable que el compilador la evalúe en tiempo de compilación porque las funciones en línea elevan el umbral en el que puede ocurrir la evaluación.
Los niveles de optimización más altos también aumentan este umbral, como en el ejemplo -Ofast, donde todas las expresiones se evalúan como verdaderas en gcc debido a una evaluación en tiempo de compilación de mayor precisión.
Podemos observar este comportamiento aquí en el explorador del compilador. Cuando se compila con -O1, solo la función marcada en línea se evalúa en tiempo de compilación, pero en -O3 ambas funciones se evalúan en tiempo de compilación.
-O1
: https://godbolt.org/z/u4gh0g-O3
: https://godbolt.org/z/nVK4SoNB: En los ejemplos del compilador-explorador, utilizo
printf
iostream en su lugar porque reduce la complejidad de la función principal, haciendo que el efecto sea más visible.Demostrar que
inline
no afecta la evaluación del tiempo de ejecuciónPodemos asegurarnos de que ninguna de las expresiones se evalúe en tiempo de compilación obteniendo el valor de la entrada estándar, y cuando hacemos esto, las 3 expresiones devuelven falso como se demuestra aquí: https://ideone.com/QZbv6X
#include <cmath> #include <iostream> bool is_cube(double r) { return floor(cbrt(r)) == cbrt(r); } bool inline is_cube_inline(double r) { return floor(cbrt(r)) == cbrt(r); } int main() { double value; std::cin >> value; std::cout << (floor(cbrt(value)) == cbrt(value)) << std::endl; // false std::cout << (is_cube(value)) << std::endl; // false std::cout << (is_cube_inline(value)) << std::endl; // false }
Contraste con este ejemplo , donde usamos la misma configuración del compilador pero proporcionamos el valor en tiempo de compilación, lo que resulta en una evaluación de mayor precisión en tiempo de compilación.
fuente
Como se observó, el uso del
==
operador para comparar valores de punto flotante ha dado como resultado diferentes salidas con diferentes compiladores y en diferentes niveles de optimización.Una buena forma de comparar los valores de punto flotante es la prueba de tolerancia relativa descrita en el artículo: Revisión de las tolerancias de punto flotante .
Primero calculamos el valor
Epsilon
(la tolerancia relativa ) que en este caso sería:double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();
Y luego úselo en las funciones en línea y no en línea de esta manera:
return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);
Las funciones ahora son:
bool is_cube(double r) { double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon(); return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon); } bool inline is_cube_inline(double r) { double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon(); return (std::fabs(std::round(std::cbrt(r)) - std::cbrt(r)) < Epsilon); }
Ahora la salida será la esperada (
[1 1 1]
) con diferentes compiladores y en diferentes niveles de optimización.Demo en vivo
fuente
max()
llamada? Por definición,floor(x)
es menor o igual quex
, pormax(x, floor(x))
lo que siempre será igualx
.max
es solo elfloor
del otro, no es necesario. Pero consideré un caso general donde los argumentosmax
pueden ser valores o expresiones que son independientes entre sí.operator==(double, double)
hacer exactamente eso, verificar que la diferencia sea más pequeña que un épsilon escalado? Alrededor del 90% de las preguntas relacionadas con el punto flotante sobre SO no existirían entonces.Epsilon
valor en función de sus requisitos particulares.