C++ VS/gcc/clang optimieren schlecht, Intel besser - Ideen?
-
ich versuche gerade eine Berechnung zu optimieren (ich komme da mehrere Mio mal durch) und dabei ist mir aufgefallen das der VS,gcc,clang
eine nach meinem Gefühl optimierbare triviale Berechnung komplett unoptimiert lassen - scheinbar ist nur der Intel Kompiler (wenn ich den ASM-Code richtig interpretiere)
in der Lage das Szenario zu optimieren - wenn das Define ONLY_CONST_VALUES aktiviert wird sieht man das alle Kompiler die Berechnung komplett auf das Ergebnis reduzieren könnenHier der Source-Code:
// gcc.godbolt.org // x86-64 CL 19 2017 RTW // x86-86 clang 4.0.0, -O2 -Wall -std=c++1z // x86-86 gcc 7.1, -O2 -Wall -std=c++1z // x86-64 icc 17 #include <cstddef> #include <cassert> #include <array> namespace { template <typename ValueT> class vector3 { public: typedef ValueT value_type; typedef ValueT& reference; typedef const ValueT& const_reference; vector3() { m_data.fill(ValueT()); } vector3(const_reference x_, const_reference y_, const_reference z_):m_data{x_,y_,z_} { } const_reference x() const { return m_data[0]; } reference x() { return m_data[0]; } const_reference y() const { return m_data[1]; } reference y() { return m_data[1]; } const_reference z() const { return m_data[2]; } reference z() { return m_data[2]; } vector3& operator+=(const vector3& other) { m_data[0] += other.m_data[0]; m_data[1] += other.m_data[1]; m_data[2] += other.m_data[2]; return *this; } vector3& operator*=(const_reference value) { m_data[0] *= value; m_data[1] *= value; m_data[2] *= value; return *this; } private: std::array<value_type, 3> m_data; }; typedef double real_type; typedef vector3<real_type> point3; template <typename ValueT> inline vector3<ValueT> operator*(typename vector3<ValueT>::const_reference a, vector3<ValueT> b) { return b *= a; } template <typename ValueT> inline vector3<ValueT> operator+(vector3<ValueT> a, const vector3<ValueT>& b) { return a += b; } static inline point3 calc(const point3& p_point, const point3& p_direction_x, const point3& p_direction_y, const point3& p_direction_z, const point3& p_origin) { return (p_point.x() * p_direction_x + p_point.y() * p_direction_y + p_point.z() * p_direction_z) + p_origin; } // 0=run calc function using structs // 1=hand inlined calc function using structs // 2=hand inlined calc function using x,y,z,direction,origin direct // 3=hand optimized result of the calc function #define TEST_LEVEL 0 static point3 test(const real_type& p_x, const real_type& p_y, const real_type& p_z) { #if TEST_LEVEL == 0 || TEST_LEVEL == 1 const point3 point(p_x, p_y, p_z); const point3 direction_x(0.0, -1.0, 0.0); const point3 direction_y(1.0, 0.0, 0.0); const point3 direction_z(0.0, 0.0, 1.0); const point3 origin(0.0, 0.0, 0.0); #endif #if TEST_LEVEL == 0 const point3 result = calc(point, direction_x, direction_y, direction_z, origin); #elif TEST_LEVEL == 1 const point3 result = (point.x() * direction_x + point.y() * direction_y + point.z() * direction_z) + origin; #elif TEST_LEVEL == 2 const point3 result = (p_x * point3(0.0, -1.0, 0.0) + p_y * point3(1.0, 0.0, 0.0) + p_z * point3(0.0, 0.0, 1.0)) + point3(0.0, 0.0, 0.0); #elif TEST_LEVEL == 3 const point3 result(p_y, -p_x, p_z); #else #error unknown TEST_LEVEL #endif return result; } } //#define ONLY_CONST_VALUES int main(int argc, char* argv[]) { const real_type x = 1.3; const real_type y = 2.2; const real_type z = 3.1; #if defined(ONLY_CONST_VALUES) const point3 result = test(x, y, z); #else //+ argv == anti-optimizer trick const point3 result = test(x+argv[0][0], y+argv[0][1], z+argv[0][2]); #endif const int dummy = (result.x() + result.y() + result.z()) * 1000; #if defined(ONLY_CONST_VALUES) assert(dummy == 4000); #endif return dummy; // anti-optimizer-trick }
oder auf Pastebin: https://pastebin.com/nMW5Am0p
die Direction(x,y,z) und Origin Vektoren sind (kompiletime)konstant - und erlaube einfache "*1"(multiplikation fällt weg) oder "*0"(wird immer zu 0) Optimierungen
nur Point(x,y,z) ist variantTEST_LEVEL=0 ist mein Original-Code, TEST_LEVEL=3 ist handoptimiert (symb. Vektor-Operationen auf dem Papier durchgefuehrt), TEST_LEVEL=1,2 sind nur Versuche
hier der Source und die 4 Kompiler zum Spielen mit ASM-Ausgabe auf gcc.godbolt
https://godbolt.org/g/zIOEpHeinfach in Zeile 106 den TEST_LEVEL auf 0 oder 3 stellen und die Unterschiede sehen
bei TEST_LEVEL=0
VS2017: kaum optimiert - super viel Addition/Multiplikationen - viel mehr als ich erwarte?
gcc/clang: ein wenig besser - aber immer noch alle Addition/Multiplikationen (kein Ausnutzen von "*1" oder "*0" Wissen)
Intel: so gut optimert wie TEST_LEVEL=3 - oder?bei TEST_LEVEL=3
soweit ich das sehe sind alle Kompiler auf gleichem Niveauhab mit const/static/anonymus namespace usw. denke alles gemacht um es "optimierbarer" zu machen
hat jemand eine Idee wie ich dem Kompiler helfen kann mit (verändertem) TEST_LEVEL=0 Code die gleiche Optimierung wie mit
TEST_LEVEL=3 zu erreichen? Oder "sollte" man sowas grundsätzlich von Hand optimieren?
-
Mit -ffast-math sehen clang und gcc schonmal ganz gut aus.
-
Mit -ffast-math sehen clang und gcc schonmal ganz gut aus.
ohne die Erfolge von -ffast-math in Abrede zu stellen?
aber warum können clang/gcc (nur) damit besser optimieren - alleine das "händische" inlinen der calc-Funktion reicht doch schon?ich sehe auch nicht warum das ignorieren von "strict IEEE compliance" hier so viel hilft?
http://stackoverflow.com/questions/7420665/what-does-gccs-ffast-math-actually-do
First of all, of course, it does break strict IEEE compliance, allowing e.g. the reordering of instructions to something which is mathematically the same (ideally) but not exactly the same in floating point.
Second, it disables setting errno after single-instruction math functions, which means avoiding a write to a thread-local variable (this can make a 100% difference for those functions on some architectures).
Third, it makes the assumption that all math is finite, which means that no checks for NaN (or zero) are made in place where they would have detrimental effects. It is simply assumed that this isn't going to happen.
Fourth, it enables reciprocal approximations for division and reciprocal square root.
Further, it disables signed zero (code assumes signed zero does not exist, even if the target supports it) and rounding math, which enables among other things constant folding at compile-time.
Last, it generates code that assumes that no hardware interrupts can happen due to signalling/trapping math (that is, if these cannot be disabled on the target architecture and consequently do happen, they will not be handled).
ich empfinde -ffast-math als ziemlichen Knüppel - oder sollte man das in seinen Projekten einfach so einsetzen - wohl eher nicht - oder?
Wie würdet ihr in einen solchen Situation vorgehen - ich hab noch einige(10-20) solcher Optimierungs-Punkte im Code - und laut Profiling sind das die 70-80% Zeitfresser
-
Nach eingehender systemanalytischer Auswertung (Advanced Business
Security Consideration) des Sachverhaltes wird der Generic Risk
Exposure Level in einer Secondary Transitional Incidence Kategorie
eingestuft. Das Aggregated Detriment verbleibt also im Admissible
Range. Ein Grund, auf per se löchrige Fricklerware umzusteigen, wäre
eine drastische Gefährdung der Business Integrity, Trust, Transparency,
Documentation, Security, Risk Management und Q&A, und würde, wie
Case Studies gezeigt haben, bzgl. der Long-Term Profitability zu einer
dramatischen Verschlechterung führen. Weiters zeigt die Distributed
Program Performance Analysis, dass Big Data besser als die Native Cloud
performed. Betrachtet man nun den Zusammenhang zwischen Marktanteil
und Meldung über Fehler, muss schlussendlich folgende Reasonable
Business Conclusion getroffen werden: Der gcc taugt nichts, es wird
weiterhin uneingeschränkt empfohlen, die Microsoft Visual Studio
Premium-IDE zu verwenden.
-
Gast3 schrieb:
Mit -ffast-math sehen clang und gcc schonmal ganz gut aus.
ohne die Erfolge von -ffast-math in Abrede zu stellen?
aber warum können clang/gcc (nur) damit besser optimieren - alleine das "händische" inlinen der calc-Funktion reicht doch schon?Zumindest bei gcc, kann man ja die Optionen noch genauer spezifizieren, tatsächlich benötigt werden nur
-fno-signed-zeros -ffinite-math-only
Damit deutet sich auch an, was dem Compiler Schwierigkeiten bereitet: er muss nämlich zeigen, dass die Rechenoperationen alle definierte Ergebnisse haben und keine Unendlichkeiten erzeugen. Das wäre in diesem speziellen Fall noch prinzipiell machbar, ist aber bei komplizierteren Operationen und freier Eingabe dagegen im Allgemeinen nicht gewährleistet.
-
-fno-signed-zeros -ffinite-math-only
Danke
weitere Fragen:
1.
gibt dafür irgendwas vergleichbares beim Microsoft CL - /fp:fast geht wohl nicht weit genug - hier bin ich wohl gezwungen von Hand zu optimieren - oder?könnte es noch Tricks geben wie man dem Kompiler die Optimierung doch noch erlauben kann (ohne -f(fast-math)-optionen) - nicht direkt mit doubles arbeiten sondern irgendeinem Value-Wrapper der die 0 ist wirklich nur 0 usw. Erkennung ermöglicht
3. kann man die -f(fast-math)-optionen) auch nur auf einzelne Funktionen oder units anwenden - #pragma ...?
4. @camper
Wie würdet du in diesem Beispiel vorgehen
-f(fast-math)-optionen nutzen oder von Hand optimieren?
eigentlich ist der calc Aufruf Teil einer Kette der noch weiter optimiert werden
kann - aber dann erkennt man eben nicht so schön was man da berechnet