Ich mache eine numerische Optimierung für eine wissenschaftliche Anwendung. Eine Sache, die mir aufgefallen ist, ist, dass GCC den Aufruf pow(a,2)
durch Kompilieren optimiert a*a
, aber der Aufruf pow(a,6)
nicht optimiert ist und tatsächlich die Bibliotheksfunktion aufruft pow
, was die Leistung erheblich verlangsamt. (Im Gegensatz dazu eliminiert der ausführbare Intel C ++ - Compilericc
den Bibliotheksaufruf für pow(a,6)
.)
Was ich bin gespannt ist , dass , wenn ich ersetzt pow(a,6)
mit a*a*a*a*a*a
Verwendung von GCC 4.5.1 und Optionen „ -O3 -lm -funroll-loops -msse4
“ es 5 verwendet mulsd
Anweisungen:
movapd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
während, wenn ich schreibe (a*a*a)*(a*a*a)
, wird es produzieren
movapd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm13, %xmm13
Dies reduziert die Anzahl der Multiplikationsbefehle auf 3. icc
hat ein ähnliches Verhalten.
Warum erkennen Compiler diesen Optimierungstrick nicht?
(a*a)*(a*a)*(a*a)
in die Mischung werfen . Gleiche Anzahl von Multiplikationen, aber wahrscheinlich genauer.Antworten:
Weil Gleitkomma-Mathematik nicht assoziativ ist . Die Art und Weise, wie Sie die Operanden in Gleitkomma-Multiplikation gruppieren, wirkt sich auf die numerische Genauigkeit der Antwort aus.
Infolgedessen sind die meisten Compiler sehr konservativ bei der Neuordnung von Gleitkommaberechnungen, es sei denn, sie können sicher sein, dass die Antwort gleich bleibt, oder Sie sagen ihnen, dass Ihnen die numerische Genauigkeit egal ist. Zum Beispiel: die
-fassociative-math
Option von gcc, mit der gcc Gleitkommaoperationen neu zuordnen kann, oder sogar die-ffast-math
Option, die noch aggressivere Kompromisse zwischen Genauigkeit und Geschwindigkeit ermöglicht.quelle
pow
sind weder hier noch dort; Diese Antwort bezieht sich nicht einmalpow
.-fp-model precise
ICC festlegen .clang
undgcc
standardmäßig strikte Konformität für die Neuzuordnung.-fassociative-math
es ungenau wäre; es ist nur dasa*a*a*a*a*a
und(a*a*a)*(a*a*a)
sind anders. Es geht nicht um Genauigkeit; Es geht um Standardkonformität und streng wiederholbare Ergebnisse, z. B. dieselben Ergebnisse auf jedem Compiler. Gleitkommazahlen sind bereits nicht genau. Es ist selten unangemessen, mit zu kompilieren-fassociative-math
.Lambdageek weist zutreffend darauf hin, dass die "Optimierung" von
a*a*a*a*a*a
to(a*a*a)*(a*a*a)
den Wert ändern kann, da die Assoziativität für Gleitkommazahlen nicht gilt. Aus diesem Grund wird es von C99 nicht zugelassen (sofern vom Benutzer nicht ausdrücklich über das Compiler-Flag oder das Pragma zugelassen). Im Allgemeinen wird davon ausgegangen, dass die Programmiererin aus einem bestimmten Grund geschrieben hat, was sie getan hat, und der Compiler sollte dies respektieren. Wenn du willst(a*a*a)*(a*a*a)
, schreibe das.Das kann jedoch ein Schmerz sein zu schreiben; Warum kann der Compiler nicht einfach das Richtige tun, wenn Sie es verwenden
pow(a,6)
? Weil es das Falsche wäre . Auf einer Plattform mit einer guten Mathematikbibliothekpow(a,6)
ist deutlich genauer als entwedera*a*a*a*a*a
oder(a*a*a)*(a*a*a)
. Um einige Daten bereitzustellen, habe ich auf meinem Mac Pro ein kleines Experiment durchgeführt, bei dem der schlimmste Fehler bei der Auswertung von ^ 6 für alle schwebenden Zahlen mit einfacher Genauigkeit zwischen [1,2] gemessen wurde:Die Verwendung
pow
anstelle eines Multiplikationsbaums reduziert den um einen Faktor 4 begrenzten Fehler . Compiler sollten keine (und im Allgemeinen keine) "Optimierungen" vornehmen, die den Fehler erhöhen, es sei denn, der Benutzer hat eine Lizenz dafür (z-ffast-math
. B. über ).Beachten Sie, dass GCC
__builtin_powi(x,n)
eine Alternative zu bietetpow( )
, die einen Inline-Multiplikationsbaum generieren sollte. Verwenden Sie diese Option, wenn Sie die Genauigkeit gegen die Leistung austauschen möchten, aber keine schnelle Mathematik aktivieren möchten.quelle
_set_SSE2_enable(<flag>)
mitflag=1
, wird es SSE2 wenn möglich nutzen. Dies verringert die Genauigkeit ein wenig, verbessert jedoch die Geschwindigkeit (in einigen Fällen). MSDN: _set_SSE2_enable () und pow ()pow
Verwendung mit nur 32-Bit-Registern zu liefern , wenn der Bibliotheksschreiber so motiviert ist. Es gibt SSE-basiertepow
Implementierungen , die sind mehr genauer als die meisten x87-basierten Implementierungen, und es gibt auch Implementierungen , dass der Handel aus einer gewissen Genauigkeit für die Geschwindigkeit.a*a*a*a*a*a
, aber das ist anscheinend nicht der Fall! :)Ein weiterer ähnlicher Fall: Die meisten Compiler optimieren nicht
a + b + c + d
auf(a + b) + (c + d)
(dies ist eine Optimierung, da der zweite Ausdruck besser per Pipeline übertragen werden kann) und bewerten ihn als gegeben (dh als(((a + b) + c) + d)
). Auch dies liegt an Eckfällen:Dies gibt aus
1.000000e-05 0.000000e+00
quelle
Fortran (entwickelt für wissenschaftliches Rechnen) verfügt über einen eingebauten Energieoperator. Soweit ich weiß, optimieren Fortran-Compiler das Erhöhen auf ganzzahlige Kräfte auf ähnliche Weise wie von Ihnen beschrieben. C / C ++ hat leider keinen Power Operator, nur die Bibliotheksfunktion
pow()
. Dies hindert intelligente Compiler nicht daran, siepow
speziell zu behandeln und für spezielle Fälle schneller zu berechnen, aber es scheint, dass sie dies weniger häufig tun ...Vor einigen Jahren habe ich versucht, es einfacher zu machen, ganzzahlige Potenzen optimal zu berechnen, und habe Folgendes gefunden. Es ist C ++, nicht C, und hängt immer noch davon ab, dass der Compiler etwas klug ist, wie man Dinge optimiert / inline macht. Wie auch immer, ich hoffe, Sie finden es in der Praxis nützlich:
Klarstellung für Neugierige: Dies ist kein optimaler Weg, um Kräfte zu berechnen, aber da das Finden der optimalen Lösung ein NP-vollständiges Problem ist und dies ohnehin nur für kleine Kräfte sinnvoll ist (im Gegensatz zur Verwendung
pow
), gibt es keinen Grund zur Aufregung mit dem Detail.Dann benutze es einfach als
power<6>(a)
.Dies erleichtert das Eingeben von Potenzen (es ist nicht erforderlich, 6
a
Sekunden mit Parens zu buchstabieren ) und ermöglicht Ihnen diese Art der Optimierung, ohne dass-ffast-math
Sie etwas Präzisionsabhängiges wie eine kompensierte Summierung haben (ein Beispiel, bei dem die Reihenfolge der Operationen wesentlich ist). .Sie können wahrscheinlich auch vergessen, dass dies C ++ ist, und es einfach im C-Programm verwenden (wenn es mit einem C ++ - Compiler kompiliert wird).
Hoffe das kann nützlich sein.
BEARBEITEN:
Folgendes bekomme ich von meinem Compiler:
Für
a*a*a*a*a*a
,Für
(a*a*a)*(a*a*a)
,Für
power<6>(a)
,quelle
GCC optimiert tatsächlich
a*a*a*a*a*a
,(a*a*a)*(a*a*a)
wenn a eine Ganzzahl ist. Ich habe es mit diesem Befehl versucht:Es gibt viele gcc-Flaggen, aber nichts Besonderes. Sie bedeuten: Lesen Sie von stdin; O2-Optimierungsstufe verwenden; Auflistung der Assembler-Sprache anstelle einer Binärdatei; Die Auflistung sollte die Syntax der Assemblersprache von Intel verwenden. Die Eingabe erfolgt in C-Sprache (normalerweise wird die Sprache aus der Dateierweiterung der Eingabe abgeleitet, beim Lesen aus stdin gibt es jedoch keine Dateierweiterung). und schreibe an stdout.
Hier ist der wichtige Teil der Ausgabe. Ich habe es mit einigen Kommentaren kommentiert, die angeben, was in der Assemblersprache vor sich geht:
Ich verwende das System GCC unter Linux Mint 16 Petra, einem Ubuntu-Derivat. Hier ist die gcc-Version:
Wie andere Poster angemerkt haben, ist diese Option im Gleitkomma nicht möglich, da Gleitkomma-Arithmetik nicht assoziativ ist.
quelle
unsigned int
.Weil eine 32-Bit-Gleitkommazahl wie 1.024 nicht 1.024 ist. In einem Computer ist 1.024 ein Intervall: von (1.024-e) bis (1.024 + e), wobei "e" einen Fehler darstellt. Einige Leute erkennen dies nicht und glauben auch, dass * in a * a für die Multiplikation von Zahlen mit beliebiger Genauigkeit steht, ohne dass mit diesen Zahlen Fehler verbunden sind. Der Grund, warum manche Menschen dies nicht erkennen, sind möglicherweise die mathematischen Berechnungen, die sie in Grundschulen durchgeführt haben: Sie arbeiten nur mit idealen Zahlen ohne Fehler und glauben, dass es in Ordnung ist, "e" einfach zu ignorieren, während Sie die Multiplikation durchführen. Sie sehen das "e" nicht implizit in "float a = 1.2", "a * a * a" und ähnlichen C-Codes.
Sollte die Mehrheit der Programmierer die Idee erkennen (und ausführen können), dass der C-Ausdruck a * a * a * a * a * a nicht mit idealen Zahlen funktioniert, kann der GCC-Compiler "a * a" KOSTENLOS optimieren * a * a * a * a "in say" t = (a * a); t * t * t ", was eine geringere Anzahl von Multiplikationen erfordert. Leider weiß der GCC-Compiler nicht, ob der Programmierer, der den Code schreibt, der Meinung ist, dass "a" eine Zahl mit oder ohne Fehler ist. Und so wird GCC nur das tun, wie der Quellcode aussieht - denn das sieht GCC mit seinem "bloßen Auge".
... Sobald Sie wissen, was für ein Programmierer Sie sind, können Sie den Schalter "-ffast-math" verwenden, um GCC mitzuteilen, dass "Hey, GCC, ich weiß, was ich tue!". Auf diese Weise kann GCC a * a * a * a * a * a in einen anderen Text konvertieren - es sieht anders aus als a * a * a * a * a * a -, berechnet jedoch eine Zahl innerhalb des Fehlerintervalls von a * a * a * a * a * a. Dies ist in Ordnung, da Sie bereits wissen, dass Sie mit Intervallen arbeiten, nicht mit idealen Zahlen.
quelle
int x = 3
alsx
3 +/- 0,5 interpretieren können .Distance
das nicht genau seinem numerischen Wert entspricht. Dies bedeutet, dass der numerische Wert nur eine Annäherung an eine physikalische Größe ist, die modelliert wird.Noch keine Poster haben die Kontraktion schwebender Ausdrücke erwähnt (ISO C-Standard, 6.5p8 und 7.12.2). Wenn das
FP_CONTRACT
Pragma auf gesetzt ist,ON
kann der Compiler einen Ausdruck wiea*a*a*a*a*a
eine einzelne Operation betrachten, als würde er genau mit einer einzelnen Rundung ausgewertet. Zum Beispiel kann ein Compiler es durch eine interne Power-Funktion ersetzen, die sowohl schneller als auch genauer ist. Dies ist besonders interessant, da das Verhalten teilweise vom Programmierer direkt im Quellcode gesteuert wird, während vom Endbenutzer bereitgestellte Compileroptionen manchmal falsch verwendet werden.Der Standardstatus des
FP_CONTRACT
Pragmas ist implementierungsdefiniert, sodass ein Compiler standardmäßig solche Optimierungen vornehmen kann. Daher sollte portabler Code, der die IEEE 754-Regeln genau befolgen muss, explizit festgelegt werdenOFF
.Wenn ein Compiler dieses Pragma nicht unterstützt, muss es konservativ sein, indem eine solche Optimierung vermieden wird, falls der Entwickler dies festgelegt hat
OFF
.GCC unterstützt dieses Pragma nicht, aber mit den Standardoptionen wird davon ausgegangen, dass dies der Fall ist
ON
. Wenn Sie also für Ziele mit einer Hardware-FMA die Umwandlunga*b+c
in fma (a, b, c) verhindern möchten, müssen Sie eine Option bereitstellen, z. B.-ffp-contract=off
(um das Pragma explizit festzulegenOFF
) oder-std=c99
(um GCC anzuweisen, sich an einige anzupassen C Standardversion, hier C99, folgen Sie daher dem obigen Absatz). In der Vergangenheit hat die letztere Option die Transformation nicht verhindert, was bedeutet, dass GCC in diesem Punkt nicht konform war: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845quelle
Wie Lambdageek hervorhob, ist die Float-Multiplikation nicht assoziativ und Sie können weniger Genauigkeit erzielen. Wenn Sie jedoch eine bessere Genauigkeit erzielen, können Sie gegen eine Optimierung argumentieren, da Sie eine deterministische Anwendung wünschen. Zum Beispiel in einem Spielsimulations-Client / Server, bei dem jeder Client dieselbe Welt simulieren muss, in der Gleitkommaberechnungen deterministisch sein sollen.
quelle
Bibliotheksfunktionen wie "pow" werden normalerweise sorgfältig ausgearbeitet, um den minimal möglichen Fehler zu erzielen (im allgemeinen Fall). Dies wird normalerweise erreicht, indem Funktionen mit Splines angenähert werden (laut Pascals Kommentar scheint die häufigste Implementierung die Verwendung des Remez-Algorithmus zu sein ).
Grundsätzlich die folgende Operation:
hat einen inhärenten Fehler von ungefähr der gleichen Größe wie der Fehler bei einer einzelnen Multiplikation oder Division .
Während der folgenden Operation:
hat einen inhärenten Fehler, der mehr als das Fünffache des Fehlers einer einzelnen Multiplikation oder Division beträgt (weil Sie 5 Multiplikationen kombinieren).
Der Compiler sollte sehr vorsichtig mit der Art der Optimierung sein, die er durchführt:
pow(a,6)
aufa*a*a*a*a*a
sie kann die Leistung verbessern, aber drastisch reduziert die Genauigkeit für Gleitkommazahlen.a*a*a*a*a*a
aufpow(a,6)
es tatsächlich die Genauigkeit verringern kann , weil „a“ war etwas spezieller Wert, die Multiplikation ohne Fehler (eine Potenz von 2 oder einer kleinen ganzen Zahl ist ) ermöglichtpow(a,6)
auf(a*a*a)*(a*a*a)
oder(a*a)*(a*a)*(a*a)
es immer noch zu einem Genauigkeitsverlust im Vergleich zurpow
Funktion kommen kann.Im Allgemeinen wissen Sie, dass für beliebige Gleitkommawerte "pow" eine bessere Genauigkeit aufweist als jede Funktion, die Sie eventuell schreiben könnten. In einigen speziellen Fällen können jedoch mehrere Multiplikationen eine bessere Genauigkeit und Leistung aufweisen. Es ist Sache des Entwicklers, die geeignetere zu wählen. schließlich den Code kommentieren, so dass niemand sonst diesen Code "optimieren" würde.
Das einzige, was Sinn macht (persönliche Meinung und anscheinend eine Wahl in GCC ohne eine bestimmte Optimierung oder ein bestimmtes Compiler-Flag), um zu optimieren, sollte sein, "pow (a, 2)" durch "a * a" zu ersetzen. Das wäre das einzig Vernünftige, was ein Compiler-Anbieter tun sollte.
quelle
Ich hätte nicht erwartet, dass dieser Fall überhaupt optimiert wird. Es kann nicht sehr oft vorkommen, dass ein Ausdruck Unterausdrücke enthält, die neu gruppiert werden können, um ganze Operationen zu entfernen. Ich würde erwarten, dass Compiler-Autoren ihre Zeit in Bereiche investieren, die eher zu spürbaren Verbesserungen führen, als einen selten anzutreffenden Randfall abzudecken.
Ich war überrascht, aus den anderen Antworten zu erfahren, dass dieser Ausdruck tatsächlich mit den richtigen Compiler-Schaltern optimiert werden kann. Entweder ist die Optimierung trivial, oder es handelt sich um einen Randfall einer viel häufigeren Optimierung, oder die Compiler-Autoren waren äußerst gründlich.
Es ist nichts Falsches daran, dem Compiler Hinweise zu geben, wie Sie es hier getan haben. Es ist ein normaler und erwarteter Teil des Mikrooptimierungsprozesses, Anweisungen und Ausdrücke neu anzuordnen, um festzustellen, welche Unterschiede sie mit sich bringen.
Während der Compiler berechtigt sein kann, die beiden Ausdrücke zu berücksichtigen, um inkonsistente Ergebnisse zu liefern (ohne die richtigen Schalter), müssen Sie nicht an diese Einschränkung gebunden sein. Der Unterschied wird unglaublich klein sein - so sehr, dass Sie, wenn der Unterschied für Sie wichtig ist, überhaupt keine Standard-Gleitkomma-Arithmetik verwenden sollten.
quelle
Es gibt bereits einige gute Antworten auf diese Frage, aber der Vollständigkeit halber wollte ich darauf hinweisen, dass der anwendbare Abschnitt der C-Norm 5.1.2.2.3 / 15 ist (der gleiche wie Abschnitt 1.9 / 9 in der C ++ 11 Standard). In diesem Abschnitt wird angegeben, dass Operatoren nur dann neu gruppiert werden können, wenn sie wirklich assoziativ oder kommutativ sind.
quelle
gcc kann diese Optimierung sogar für Gleitkommazahlen durchführen. Zum Beispiel,
wird
mit
-O -funsafe-math-optimizations
. Diese Neuordnung verstößt jedoch gegen IEEE-754, sodass das Flag erforderlich ist.Vorzeichenbehaftete Ganzzahlen können, wie Peter Cordes in einem Kommentar hervorhob, diese Optimierung ohne durchführen,
-funsafe-math-optimizations
da sie genau dann gelten, wenn kein Überlauf vorliegt und wenn ein Überlauf vorliegt, erhalten Sie ein undefiniertes Verhalten. Also verstehst dumit nur
-O
. Für vorzeichenlose Ganzzahlen ist es sogar noch einfacher, da sie Mod-Potenzen von 2 haben und daher auch bei Überlauf frei neu angeordnet werden können.quelle
-ffast-math
)