Was ist effizienter? Verwenden Sie pow, um es zu quadrieren oder einfach mit sich selbst zu multiplizieren?

119

Welche dieser beiden Methoden ist in C effizienter? Und wie wäre es mit:

pow(x,3)

vs.

x*x*x // etc?
Jamylak
quelle
9
Ist xIntegral oder Gleitkomma?
Matthew Flaschen
6
Sie können versuchen, ein Programm zu schreiben, das die beiden oben genannten Vorgänge ausführt, und festlegen, wie lange die Ausführung mit einer Profilbibliothek dauert. Das gibt Ihnen eine gute Antwort in Bezug auf die Ausführungszeit.
J. Polfer
3
Wenn Sie effizient sagen, beziehen Sie sich auf Zeit oder Raum (dh Speichernutzung)?
J. Polfer
4
@sheepsimulator: +1, um mir die Zeit zu sparen, die erforderlich ist, um (erneut) darauf hinzuweisen, dass Sie durch das Schreiben eines Schnelltests schneller eine endgültige Antwort erhalten, als Sie eine möglicherweise vage oder falsche Antwort von SO erhalten.
Nur meine richtige Meinung
5
@kirill_igum Wenn dies Gleitkommawerte sind, die kein Fehler sind, ist Gleitkomma-Arithmetik nicht assoziativ.
effeffe

Antworten:

82

Ich habe den Leistungsunterschied zwischen x*x*...vs pow(x,i)für small imit diesem Code getestet :

#include <cstdlib>
#include <cmath>
#include <boost/date_time/posix_time/posix_time.hpp>

inline boost::posix_time::ptime now()
{
    return boost::posix_time::microsec_clock::local_time();
}

#define TEST(num, expression) \
double test##num(double b, long loops) \
{ \
    double x = 0.0; \
\
    boost::posix_time::ptime startTime = now(); \
    for (long i=0; i<loops; ++i) \
    { \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
    } \
    boost::posix_time::time_duration elapsed = now() - startTime; \
\
    std::cout << elapsed << " "; \
\
    return x; \
}

TEST(1, b)
TEST(2, b*b)
TEST(3, b*b*b)
TEST(4, b*b*b*b)
TEST(5, b*b*b*b*b)

template <int exponent>
double testpow(double base, long loops)
{
    double x = 0.0;

    boost::posix_time::ptime startTime = now();
    for (long i=0; i<loops; ++i)
    {
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
    }
    boost::posix_time::time_duration elapsed = now() - startTime;

    std::cout << elapsed << " ";

    return x;
}

int main()
{
    using std::cout;
    long loops = 100000000l;
    double x = 0.0;
    cout << "1 ";
    x += testpow<1>(rand(), loops);
    x += test1(rand(), loops);

    cout << "\n2 ";
    x += testpow<2>(rand(), loops);
    x += test2(rand(), loops);

    cout << "\n3 ";
    x += testpow<3>(rand(), loops);
    x += test3(rand(), loops);

    cout << "\n4 ";
    x += testpow<4>(rand(), loops);
    x += test4(rand(), loops);

    cout << "\n5 ";
    x += testpow<5>(rand(), loops);
    x += test5(rand(), loops);
    cout << "\n" << x << "\n";
}

Ergebnisse sind:

1 00:00:01.126008 00:00:01.128338 
2 00:00:01.125832 00:00:01.127227 
3 00:00:01.125563 00:00:01.126590 
4 00:00:01.126289 00:00:01.126086 
5 00:00:01.126570 00:00:01.125930 
2.45829e+54

Beachten Sie, dass ich das Ergebnis jeder Pow-Berechnung akkumuliere, um sicherzustellen, dass der Compiler es nicht entfernt.

Wenn ich die std::pow(double, double)Version benutze und loops = 1000000l, bekomme ich:

1 00:00:00.011339 00:00:00.011262 
2 00:00:00.011259 00:00:00.011254 
3 00:00:00.975658 00:00:00.011254 
4 00:00:00.976427 00:00:00.011254 
5 00:00:00.973029 00:00:00.011254 
2.45829e+52

Dies ist auf einem Intel Core Duo mit Ubuntu 9.10 64bit. Kompiliert mit gcc 4.4.1 mit -o2 Optimierung.

In C ist ja x*x*xalso schneller als pow(x, 3), weil es keine pow(double, int)Überlastung gibt. In C ++ wird es ungefähr gleich sein. (Vorausgesetzt, die Methodik in meinen Tests ist korrekt.)


Dies ist eine Antwort auf den Kommentar von An Markm:

Selbst wenn eine using namespace stdDirektive ausgegeben wurde und der zweite Parameter powein ist int, wird die std::pow(double, int)Überladung von <cmath>anstelle von ::pow(double, double)von aufgerufen<math.h> .

Dieser Testcode bestätigt dieses Verhalten:

#include <iostream>

namespace foo
{

    double bar(double x, int i)
    {
        std::cout << "foo::bar\n";
        return x*i;
    }


}

double bar(double x, double y)
{
    std::cout << "::bar\n";
    return x*y;
}

using namespace foo;

int main()
{
    double a = bar(1.2, 3); // Prints "foo::bar"
    std::cout << a << "\n";
    return 0;
}
Emile Cormier
quelle
1
Bedeutet dies, dass das Einfügen eines "using namespace std" die Option C wählt und dies sich nachteilig auf die Laufzeit auswirkt?
Andreas
In beiden Zeitschleifen wird die Pow-Berechnung wahrscheinlich nur einmal durchgeführt. gcc -O2 sollte kein Problem damit haben, den schleifeninvarianten Ausdruck aus der Schleife zu heben. Sie testen also nur, wie gut der Compiler darin ist, eine Add-Constant-Schleife in eine Multiplikation umzuwandeln oder einfach eine Add-Constant-Schleife zu optimieren. Es gibt einen Grund, warum Ihre Schleifen mit Exponent = 1 und Exponent = 5 dieselbe Geschwindigkeit haben, selbst für die ausgeschriebene Version.
Peter Cordes
2
Ich habe es mit Godbolt versucht (mit auskommentiertem Timing, da Godbolt Boost nicht installiert hat). Es ruft überraschenderweise tatsächlich std::pow8 * -Schleifenzeiten auf (für Exponenten> 2), es sei denn, Sie verwenden -fno-math-errno. Dann kann es den Pow-Call aus der Schleife ziehen, wie ich es mir vorgestellt habe. Ich denke, da errno ein globaler Thread ist, erfordert die Thread-Sicherheit, dass pow aufgerufen wird, um errno möglicherweise mehrmals zu setzen ... exp = 1 und exp = 2 sind schnell, da der pow-Aufruf mit nur -O3.. ( mit - aus der Schleife gehoben wird) ffast-math , es macht die Summe von 8 auch außerhalb der Schleife.)
Peter Cordes
Ich habe abgestimmt, bevor mir klar wurde, dass ich in der von mir verwendeten Godbolt-Sitzung -schnelle-Mathematik aktiviert hatte. Auch ohne das sind Testpow <1> und Testpow <2> defekt, weil sie mit dem powaus der Schleife gehobenen Anruf übereinstimmen , sodass dort ein großer Fehler vorliegt. Es sieht auch so aus, als würden Sie hauptsächlich die Latenz der FP-Addition testen, da alle Tests in derselben Zeitspanne ausgeführt werden. Sie würden erwarten test5, langsamer zu sein als test1, aber es ist nicht. Die Verwendung mehrerer Akkumulatoren würde die Abhängigkeitskette aufteilen und die Latenz verbergen.
Peter Cordes
@ PeterCordes, wo warst du vor 5 Jahren? :-) Ich werde versuchen, meinen Benchmark durch Anwenden powauf einen sich ständig ändernden Wert festzulegen (um zu verhindern, dass der wiederholte Pow-Ausdruck herausgehoben wird).
Emile Cormier
30

Das ist die falsche Frage. Die richtige Frage wäre: "Welches ist für menschliche Leser meines Codes leichter zu verstehen?"

Wenn Geschwindigkeit wichtig ist (später), fragen Sie nicht, sondern messen Sie. (Messen Sie vorher, ob die Optimierung tatsächlich einen spürbaren Unterschied macht.) Schreiben Sie den Code bis dahin so, dass er am einfachsten zu lesen ist.

Bearbeiten
Nur um dies zu verdeutlichen (obwohl dies bereits hätte sein sollen): Durchbruchbeschleunigungen entstehen normalerweise durch die Verwendung besserer Algorithmen , die Verbesserung der Datenlokalität, die Reduzierung des dynamischen Speichers , die Vorberechnung von Ergebnissen usw. Sie kommen selten vor Mikrooptimierende Einzelfunktionsaufrufe , und wo sie dies tun, tun sie dies an sehr wenigen Stellen , die nur durch sorgfältige (und zeitaufwändige) Profilerstellung gefunden werden können. Meistens können sie durch sehr nicht intuitive Funktionen beschleunigt werden Dinge (wie das Einfügennoop Aussagen), und was eine Optimierung für eine Plattform ist, ist manchmal eine Pessimierung für eine andere (weshalb Sie messen müssen, anstatt zu fragen, weil wir Ihre Umgebung nicht vollständig kennen / haben).

Lassen Sie mich dies noch einmal unterstreichen: Selbst in den wenigen Anwendungen, in denen solche Dinge wichtig sind, spielen sie an den meisten Stellen, an denen sie verwendet werden, keine Rolle, und es ist sehr unwahrscheinlich, dass Sie die Stellen finden, an denen sie wichtig sind, indem Sie sich den Code ansehen. Sie müssen die Hot Spots wirklich zuerst identifizieren , da sonst die Optimierung des Codes nur Zeitverschwendung ist .

Selbst wenn eine einzelne Operation (wie das Berechnen des Quadrats eines Werts) 10% der Ausführungszeit der Anwendung beansprucht (was IME ziemlich selten ist), und selbst wenn die Optimierung 50% der für diese Operation erforderlichen Zeit spart (was IME ist) sogar viel, viel seltener), Sie haben dafür gesorgt, dass die Anwendung nur 5% weniger Zeit in Anspruch nimmt .
Ihre Benutzer benötigen eine Stoppuhr, um dies überhaupt zu bemerken. (Ich denke, in den meisten Fällen bleibt etwas unter 20% Beschleunigung für die meisten Benutzer unbemerkt. Und das sind vier solcher Stellen, die Sie finden müssen.)

sbi
quelle
43
Es könnte die richtige Art von Frage sein. Vielleicht denkt er nicht über sein eigenes praktisches Projekt nach, sondern interessiert sich nur dafür, wie die Sprache / der Compiler funktioniert ...
Andreas Rejbrand
137
Stackoverflow sollte eine Schaltfläche haben, die einen Standard-Haftungsausschluss einfügt: "Ich weiß bereits, dass vorzeitige Optimierung böse ist, aber ich stelle diese Optimierungsfrage für akademische Zwecke, oder ich habe diese Codezeile / diesen Codeblock bereits als Engpass identifiziert."
Emile Cormier
39
Ich denke nicht, dass Lesbarkeit hier ein Problem ist. Das Schreiben von x * x gegen pow (x, 2) scheint beides ziemlich klar zu sein.
KillianDS
41
Übermäßiger Gebrauch von Fett und Kursiv, nicht augenschonend.
Stagas
24
Ich stimme dieser Antwort nicht ganz zu. Es ist eine berechtigte Frage, nach der Leistung zu fragen. Die beste Leistung, die Sie erzielen können, ist manchmal eine gültige Anforderung und oft der Grund, warum jemand C ++ anstelle einer anderen Sprache verwendet hat. Und Messen ist nicht immer eine gute Idee. Ich könnte Blasensortierung und Quicksortierung messen und Blasensortierung mit meinen 10 Artikeln schneller finden, da ich nicht den Hintergrund hatte zu wissen, dass die Anzahl der Artikel von großer Bedeutung ist, und später bei meinen 1.000.000 Artikeln feststellen konnte, dass dies eine sehr schlechte Wahl war.
Jcoder
17

x*xoder x*x*xwird schneller sein als pow, da powmuss sich mit dem allgemeinen Fall befassen, während x*xspezifisch ist. Sie können auch den Funktionsaufruf und dergleichen weglassen.

Wenn Sie jedoch feststellen, dass Sie auf diese Weise eine Mikrooptimierung durchführen, müssen Sie einen Profiler erstellen und ernsthafte Profilerstellungen durchführen. Die überwältigende Wahrscheinlichkeit ist, dass Sie niemals einen Unterschied zwischen den beiden bemerken würden.

Hündchen
quelle
7
Ich dachte das Gleiche, bis ich mich entschied, es zu testen. Ich habe gerade x*x*xgegen Double std::pow(double base, int exponent)in einer Zeitschleife getestet und kann keinen statistisch bedeutsamen Leistungsunterschied feststellen.
Emile Cormier
2
Stellen Sie sicher, dass es nicht vom Compiler optimiert wird.
Ponkadoodle
1
@Emile: Überprüfen Sie den vom Compiler generierten Code. Optimierer machen manchmal einige knifflige (und nicht offensichtliche) Dinge. Überprüfen Sie auch die Leistung auf verschiedenen Optimierungsstufen: -O0, -O1, -O2 und -O3 zum Beispiel.
Nur meine richtige Meinung
2
Sie können nicht davon ausgehen, dass verallgemeinerte Funktionen langsamer sind. Manchmal ist das Gegenteil der Fall, weil der Compiler den einfacheren Code leichter optimieren kann.
Cambunctious
5

Ich habe mich auch über das Leistungsproblem gewundert und gehofft, dass dies vom Compiler basierend auf der Antwort von @EmileCormier optimiert wird. Ich befürchtete jedoch, dass der von ihm angezeigte Testcode es dem Compiler weiterhin ermöglichen würde, den Aufruf von std :: pow () zu optimieren, da im Aufruf jedes Mal dieselben Werte verwendet wurden, wodurch der Compiler die Ergebnisse und speichern konnte Verwenden Sie es erneut in der Schleife - dies würde die nahezu identischen Laufzeiten für alle Fälle erklären. Also habe ich es mir auch angesehen.

Hier ist der Code, den ich verwendet habe (test_pow.cpp):

#include <iostream>                                                                                                                                                                                                                       
#include <cmath>
#include <chrono>

class Timer {
  public:
    explicit Timer () : from (std::chrono::high_resolution_clock::now()) { }

    void start () {
      from = std::chrono::high_resolution_clock::now();
    }

    double elapsed() const {
      return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - from).count() * 1.0e-6;
    }

  private:
    std::chrono::high_resolution_clock::time_point from;
};

int main (int argc, char* argv[])
{
  double total;
  Timer timer;



  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += std::pow (i,2);
  std::cout << "std::pow(i,2): " << timer.elapsed() << "s (result = " << total << ")\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += i*i;
  std::cout << "i*i: " << timer.elapsed() << "s (result = " << total << ")\n";

  std::cout << "\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += std::pow (i,3);
  std::cout << "std::pow(i,3): " << timer.elapsed() << "s (result = " << total << ")\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += i*i*i;
  std::cout << "i*i*i: " << timer.elapsed() << "s (result = " << total << ")\n";


  return 0;
}

Dies wurde kompiliert mit:

g++ -std=c++11 [-O2] test_pow.cpp -o test_pow

Grundsätzlich ist der Unterschied das Argument, dass std :: pow () der Schleifenzähler ist. Wie ich befürchtet habe, ist der Leistungsunterschied ausgeprägt. Ohne das Flag -O2 waren die Ergebnisse auf meinem System (Arch Linux 64-Bit, g ++ 4.9.1, Intel i7-4930):

std::pow(i,2): 0.001105s (result = 3.33333e+07)
i*i: 0.000352s (result = 3.33333e+07)

std::pow(i,3): 0.006034s (result = 2.5e+07)
i*i*i: 0.000328s (result = 2.5e+07)

Bei der Optimierung waren die Ergebnisse gleichermaßen beeindruckend:

std::pow(i,2): 0.000155s (result = 3.33333e+07)
i*i: 0.000106s (result = 3.33333e+07)

std::pow(i,3): 0.006066s (result = 2.5e+07)
i*i*i: 9.7e-05s (result = 2.5e+07)

Es sieht also so aus, als würde der Compiler zumindest versuchen, den Fall std :: pow (x, 2) zu optimieren, nicht jedoch den Fall std :: pow (x, 3) (es dauert ~ 40-mal länger als der Fall std :: pow (x, 2) Fall). In allen Fällen schnitt die manuelle Erweiterung besser ab - insbesondere beim Power 3-Gehäuse (60-mal schneller). Dies ist auf jeden Fall zu beachten, wenn Sie std :: pow () mit ganzzahligen Potenzen größer als 2 in einer engen Schleife ausführen ...

jdtournier
quelle
4

Am effizientesten ist es, das exponentielle Wachstum der Multiplikationen zu berücksichtigen. Überprüfen Sie diesen Code auf p ^ q:

template <typename T>
T expt(T p, unsigned q){
    T r =1;
    while (q != 0) {
        if (q % 2 == 1) {    // if q is odd
            r *= p;
            q--;
        }
        p *= p;
        q /= 2;
    }
    return r;
}
mhaghighat
quelle
2

Wenn der Exponent konstant und klein ist, erweitern Sie ihn und minimieren Sie die Anzahl der Multiplikationen. (Zum Beispiel x^4ist nicht optimal x*x*x*x, aber y*ywo y=x*x. Und x^5ist y*y*xwoy=x*x . Und so weiter.) Für konstante ganzzahlige Exponenten schreiben Sie einfach das optimierte Formular bereits aus; Bei kleinen Exponenten ist dies eine Standardoptimierung, die durchgeführt werden sollte, unabhängig davon, ob der Code profiliert wurde oder nicht. Das optimierte Formular ist in so vielen Fällen schneller, dass es sich grundsätzlich immer lohnt, es zu tun.

(Wenn Sie Visual C ++ verwenden, std::pow(float,int)führt es die Optimierung durch , auf die ich anspreche, wobei die Reihenfolge der Operationen mit dem Bitmuster des Exponenten zusammenhängt. Ich kann jedoch nicht garantieren, dass der Compiler die Schleife für Sie abrollt, sodass es sich dennoch lohnt, dies zu tun es von Hand.)

Übrigens powhat eine (nicht) überraschende Tendenz, auf den Profiler-Ergebnissen aufzutauchen. Wenn Sie es nicht unbedingt benötigen (dh der Exponent ist groß oder keine Konstante) und sich überhaupt Gedanken über die Leistung machen, schreiben Sie am besten den optimalen Code und warten Sie, bis der Profiler Ihnen dies mitteilt (überraschenderweise) ) Zeit verschwenden, bevor man weiter nachdenkt. (Die Alternative besteht darin, anzurufen powund sich vom Profiler mitteilen zu lassen, dass es (nicht überraschend) Zeitverschwendung ist. Sie können diesen Schritt durch intelligentes Ausführen unterbinden.)

caf
quelle
0

Ich war mit einem ähnlichen Problem beschäftigt und bin ziemlich verwirrt über die Ergebnisse. Ich berechnete x⁻³ / ² für die Newtonsche Gravitation in einer Situation mit n Körpern (Beschleunigung durch einen anderen Massenkörper M, der sich in einem Abstandsvektor d befindet): a = M G d*(d²)⁻³/²(wobei d² das Punktprodukt (Skalarprodukt) von d für sich ist), und ich dachte, das Berechnen M*G*pow(d2, -1.5)wäre einfacher alsM*G/d2/sqrt(d2)

Der Trick ist, dass dies für kleine Systeme zutrifft, aber mit zunehmender Größe der Systeme M*G/d2/sqrt(d2)effizienter wird und ich nicht verstehe, warum sich die Größe des Systems auf dieses Ergebnis auswirkt, da das Wiederholen des Vorgangs mit verschiedenen Daten dies nicht tut. Es ist, als ob es mögliche Optimierungen gäbe, wenn das System wächst, die aber mit nicht möglich sindpow

Geben Sie hier die Bildbeschreibung ein

Camion
quelle