Die Inline-Version einer Funktion gibt einen anderen Wert zurück als die Nicht-Inline-Version

85

Wie können zwei Versionen derselben Funktion, die sich nur darin unterscheiden, dass eine inline ist und die andere nicht, unterschiedliche Werte zurückgeben? Hier ist ein Code, den ich heute geschrieben habe und ich bin mir nicht sicher, wie er funktioniert.

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

Ich würde erwarten, dass alle Ausgaben gleich sind 1, aber es gibt dies tatsächlich aus (g ++ 8.3.1, keine Flags):

1
0
1

anstatt

1
1
1

Edit: clang ++ 7.0.0 gibt Folgendes aus:

0
0
0

und g ++ -Ofast this:

1
1
1
zbrojny120
quelle
3
Können Sie bitte angeben, welchen Compiler, welche Compileroptionen Sie verwenden und welchen Computer? Funktioniert gut für mich unter GCC 7.1 unter Windows.
Diodacus
31
Ist ==Gleitkommawerte nicht immer etwas unvorhersehbar?
500 - Interner
3
verwandte stackoverflow.com/questions/588004/…
idclev 463035818
2
Haben Sie die -OfastOption festgelegt, die solche Optimierungen ermöglicht?
cmdLP
4
Der Compiler gibt cbrt(27.0)den Wert von zurück, 0x0000000000000840während die Standardbibliothek zurückgibt 0x0100000000000840. Die Doppel unterscheiden sich in der 16. Zahl nach dem Komma. Mein System: archlinux4.20 x64 gcc8.2.1 glibc2.28 mit Karo dieser . Ich frage mich, ob gcc oder glibc richtig ist.
KamilCuk

Antworten:

73

Erläuterung

Einige Compiler (insbesondere GCC) verwenden eine höhere Genauigkeit, wenn Ausdrücke zur Kompilierungszeit ausgewertet werden. Wenn ein Ausdruck nur von konstanten Eingaben und Literalen abhängt, kann er zur Kompilierungszeit ausgewertet werden, auch wenn der Ausdruck keiner constexpr-Variablen zugewiesen ist. Ob dies geschieht oder nicht, hängt ab von:

  • Die Komplexität des Ausdrucks
  • Der Schwellenwert, den der Compiler als Grenzwert verwendet, wenn versucht wird, eine Auswertung der Kompilierungszeit durchzuführen
  • Andere Heuristiken, die in besonderen Fällen verwendet werden (z. B. wenn Clang Elides-Schleifen)

Wenn ein Ausdruck wie im ersten Fall explizit angegeben wird, weist er eine geringere Komplexität auf, und der Compiler wird ihn wahrscheinlich zur Kompilierungszeit auswerten.

Wenn eine Funktion als inline markiert ist, wird sie vom Compiler eher zur Kompilierungszeit ausgewertet, da Inline-Funktionen den Schwellenwert erhöhen, bei dem die Auswertung erfolgen kann.

Höhere Optimierungsstufen erhöhen auch diesen Schwellenwert, wie im Beispiel -Ofast, bei dem alle Ausdrücke aufgrund der Bewertung der Kompilierungszeit mit höherer Genauigkeit auf gcc als wahr ausgewertet werden.

Wir können dieses Verhalten hier im Compiler-Explorer beobachten. Bei der Kompilierung mit -O1 wird zur Kompilierungszeit nur die inline markierte Funktion ausgewertet, bei -O3 werden beide Funktionen zur Kompilierungszeit ausgewertet.

NB: In den Compiler-Explorer-Beispielen verwende ich printfstattdessen iostream, da dies die Komplexität der Hauptfunktion verringert und den Effekt sichtbarer macht.

Dies inlinehat keinen Einfluss auf die Laufzeitauswertung

Wir können sicherstellen, dass keiner der Ausdrücke zur Kompilierungszeit ausgewertet wird, indem wir den Wert von der Standardeingabe erhalten. Wenn wir dies tun, geben alle drei Ausdrücke false zurück, wie hier gezeigt: 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
}

Im Gegensatz zu diesem Beispiel , in dem wir dieselben Compilereinstellungen verwenden, aber den Wert zur Kompilierungszeit angeben, führt dies zu einer genaueren Auswertung der Kompilierungszeit.

J. Antonio Perez
quelle
22

Wie beobachtet, hat die Verwendung des ==Operators zum Vergleichen von Gleitkommawerten zu unterschiedlichen Ausgaben mit unterschiedlichen Compilern und mit unterschiedlichen Optimierungsstufen geführt.

Ein guter Weg, um Gleitkommawerte zu vergleichen, ist der im Artikel beschriebene relative Toleranztest : Gleitkommatoleranzen überarbeitet .

Wir berechnen zuerst den Wert Epsilon(die relative Toleranz ), der in diesem Fall wäre:

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

Und verwenden Sie es dann sowohl in der Inline- als auch in der Nicht-Inline-Funktion auf folgende Weise:

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

Die Funktionen sind jetzt:

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

Jetzt wird die Ausgabe wie erwartet ( [1 1 1]) mit verschiedenen Compilern und auf verschiedenen Optimierungsstufen erfolgen.

Live-Demo

PW
quelle
Was ist der Zweck des max()Anrufs? Ist per Definition floor(x)kleiner oder gleich x, max(x, floor(x))wird also immer gleich sein x.
Ken Thomases
@ KenThomases: In diesem speziellen Fall, in dem ein Argument maxnur das floordes anderen ist, ist es nicht erforderlich. Aber ich habe einen allgemeinen Fall betrachtet, in dem Argumente maxWerte oder Ausdrücke sein können, die unabhängig voneinander sind.
PW
Sollte das nicht operator==(double, double)genau so sein, prüfen Sie, ob der Unterschied kleiner als ein skaliertes Epsilon ist? Etwa 90% der Gleitkomma-Fragen zu SO würden dann nicht existieren.
Peter - Monica am
Ich denke, es ist besser, wenn der Benutzer den EpsilonWert abhängig von seiner speziellen Anforderung angeben kann.
PW