A versão embutida de uma função retorna um valor diferente da versão não embutida

85

Como duas versões da mesma função, diferindo apenas em uma sendo embutida e a outra não, podem retornar valores diferentes? Aqui está um código que escrevi hoje e não tenho certeza de como 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;
}

Eu esperaria que todas as saídas fossem iguais a 1, mas na verdade ele produz isso (g ++ 8.3.1, sem sinalizadores):

1
0
1

ao invés de

1
1
1

Editar: o clang ++ 7.0.0 resulta em:

0
0
0

e g ++ -Fast this:

1
1
1
zbrojny120
fonte
3
Você pode fornecer qual compilador, opções de compilador você está usando e qual máquina? Funciona bem para mim no GCC 7.1 no Windows.
Diodacus de
31
Nem ==sempre é um pouco imprevisível com valores de ponto flutuante?
500 - Erro interno do servidor,
3
related stackoverflow.com/questions/588004/…
maior_prime_is_463035818
2
Você definiu a -Ofastopção que permite essas otimizações?
cmdLP
4
O compilador retorna para cbrt(27.0)o valor de 0x0000000000000840enquanto a biblioteca padrão retorna 0x0100000000000840. As duplas diferem no 16º número após a vírgula. Meu sistema: archlinux4.20 x64 gcc8.2.1 glibc2.28 Verificado com isso . Gostaria de saber se gcc ou glibc está certo.
KamilCuk

Respostas:

73

Explicação

Alguns compiladores (notavelmente GCC) usam maior precisão ao avaliar expressões em tempo de compilação. Se uma expressão depende apenas de entradas e literais constantes, ela pode ser avaliada em tempo de compilação, mesmo se a expressão não for atribuída a uma variável constexpr. Se isso ocorre ou não depende de:

  • A complexidade da expressão
  • O limite que o compilador usa como um corte ao tentar realizar a avaliação do tempo de compilação
  • Outras heurísticas usadas em casos especiais (como quando clang elides loops)

Se uma expressão for fornecida explicitamente, como no primeiro caso, ela terá uma complexidade menor e o compilador provavelmente irá avaliá-la no momento da compilação.

Da mesma forma, se uma função for marcada em linha, é mais provável que o compilador avalie-a em tempo de compilação porque as funções em linha aumentam o limite no qual a avaliação pode ocorrer.

Níveis de otimização mais altos também aumentam esse limite, como no exemplo -Ofast, onde todas as expressões são avaliadas como verdadeiras no gcc devido à avaliação de tempo de compilação de maior precisão.

Podemos observar esse comportamento aqui no compilador explorer. Quando compilado com -O1, apenas a função marcada inline é avaliada em tempo de compilação, mas em -O3 ambas as funções são avaliadas em tempo de compilação.

NB: Nos exemplos do compilador-explorador, eu uso printf iostream porque reduz a complexidade da função principal, tornando o efeito mais visível.

Demonstrando que inline não afeta a avaliação do tempo de execução

Podemos garantir que nenhuma das expressões seja avaliada em tempo de compilação obtendo o valor da entrada padrão e, quando fazemos isso, todas as 3 expressões retornam falso, conforme demonstrado aqui: 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
}

Compare com este exemplo , onde usamos as mesmas configurações do compilador, mas fornecemos o valor em tempo de compilação, resultando em uma avaliação de tempo de compilação de maior precisão.

J. Antonio Perez
fonte
22

Conforme observado, o uso do ==operador para comparar valores de ponto flutuante resultou em diferentes saídas com diferentes compiladores e em diferentes níveis de otimização.

Uma boa maneira de comparar valores de ponto flutuante é o teste de tolerância relativa descrito no artigo: Tolerâncias de ponto flutuante revisitadas .

Primeiro calculamos o valor Epsilon(a tolerância relativa ) que, neste caso, seria:

double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();

E então use-o nas funções inline e não inline desta maneira:

return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);

As funções agora são:

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);
}

Agora a saída será a esperada ( [1 1 1]) com diferentes compiladores e em diferentes níveis de otimização.

Demonstração ao vivo

PW
fonte
Qual é o propósito da max()ligação? Por definição, floor(x)é menor ou igual a x, então max(x, floor(x))sempre será igual x.
Ken Thomases de
@KenThomases: Neste caso particular, onde um argumento para maxé apenas o floordo outro, não é necessário. Mas considerei um caso geral em que os argumentos de maxpodem ser valores ou expressões independentes uns dos outros.
PW de
Não deveria operator==(double, double)fazer exatamente isso, verifique se a diferença é menor do que um épsilon dimensionado? Cerca de 90% das questões relacionadas ao ponto flutuante no SO não existiriam então.
Peter - Reintegrar Monica de
Eu acho que é melhor se o usuário puder especificar o Epsilonvalor dependendo de sua necessidade particular.
PW de