La versión en línea de una función devuelve un valor diferente al de la versión no en línea

85

¿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
zbrojny120
fuente
3
¿Puede proporcionar qué compilador, qué opciones de compilador está utilizando y qué máquina? Funciona bien para mí en GCC 7.1 en Windows.
Diodacus
31
¿No es ==siempre un poco impredecible con valores de coma flotante?
500 - Error interno del servidor
3
relacionado stackoverflow.com/questions/588004/…
idclev 463035818
2
¿Estableció la -Ofastopción que permite tales optimizaciones?
cmdLP
4
El compilador devuelve por cbrt(27.0)el valor de 0x0000000000000840mientras que la biblioteca estándar devuelve 0x0100000000000840. 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.
KamilCuk

Respuestas:

73

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:

  • La complejidad de la expresión
  • El umbral que el compilador usa como límite cuando intenta realizar la evaluación del tiempo de compilación
  • Otras heurísticas que se utilizan en casos especiales (como cuando el ruido elide los bucles)

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.

NB: En los ejemplos del compilador-explorador, utilizo printfiostream en su lugar porque reduce la complejidad de la función principal, haciendo que el efecto sea más visible.

Demostrar que inlineno afecta la evaluación del tiempo de ejecución

Podemos 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.

J. Antonio Pérez
fuente
22

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

PW
fuente
¿Cuál es el propósito de la max()llamada? Por definición, floor(x)es menor o igual que x, por max(x, floor(x))lo que siempre será igual x.
Ken Thomases
@KenThomases: En este caso particular, donde un argumento para maxes solo el floordel otro, no es necesario. Pero consideré un caso general donde los argumentos maxpueden ser valores o expresiones que son independientes entre sí.
PV
¿No debería 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.
Peter - Reincorpora a Monica
Creo que es mejor si el usuario puede especificar el Epsilonvalor en función de sus requisitos particulares.
PW