Cos'è più efficiente? Usare pow per quadrare o semplicemente moltiplicarlo con se stesso?


119

Quale di questi due metodi è più efficiente in C? E che ne dici di:

pow(x,3)

vs.

x*x*x // etc?

9
È xintegrale o in virgola mobile?
Matthew Flaschen

6
Potresti provare a scrivere un programma che esegua le due operazioni precedenti e calcolare il tempo necessario per l'esecuzione con una libreria di profili. Questo ti darà una buona risposta in termini di tempo di esecuzione.
J. Polfer

3
Quando dici efficiente, ti riferisci al tempo o allo spazio (cioè, l'utilizzo della memoria)?
J. Polfer

4
@sheepsimulator: +1 per avermi risparmiato il tempo necessario per (di nuovo) sottolineare che scrivere un test rapido ti darà una risposta definitiva più velocemente di quanto otterrai una risposta potenzialmente vaga o errata da SO.
SOLO LA MIA OPINIONE corretta

5
@kirill_igum se questi sono valori in virgola mobile che non sono un bug, l'aritmetica in virgola mobile non è associativa.
effeffe

Risposte:


82

Ho testato la differenza di prestazioni tra x*x*...vs pow(x,i)per piccoli iutilizzando questo codice:

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

I risultati sono:

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

Nota che accumulo il risultato di ogni calcolo pow per assicurarmi che il compilatore non lo ottimizzi.

Se utilizzo la std::pow(double, double)versione e loops = 1000000lottengo:

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

Questo è su un Intel Core Duo con Ubuntu 9.10 a 64 bit. Compilato usando gcc 4.4.1 con ottimizzazione -o2.

Quindi in C, sì, x*x*xsarà più veloce di pow(x, 3), perché non c'è pow(double, int)sovraccarico. In C ++, sarà più o meno lo stesso. (Supponendo che la metodologia nei miei test sia corretta.)


Questo è in risposta al commento fatto da An Markm:

Anche se è using namespace stdstata emessa una direttiva, se il secondo parametro to powè an int, verrà chiamato l' std::pow(double, int)overload from <cmath>invece di ::pow(double, double)from <math.h>.

Questo codice di test conferma quel comportamento:

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

1
questo significa che l'inserimento di un "using namespace std" sceglie l'opzione C e questo sarà dannoso per il runtime?
Andreas

In entrambi i tuoi cicli di temporizzazione, il calcolo del pow probabilmente avviene solo una volta. gcc -O2 non dovrebbe avere problemi a sollevare l'espressione invariante di ciclo fuori dal ciclo. Quindi stai solo testando quanto bene il compilatore fa nel trasformare un ciclo di add-costante in un moltiplicatore, o semplicemente ottimizzando un ciclo di add-costante. C'è un motivo per cui i tuoi loop hanno la stessa velocità con esponente = 1 contro esponente = 5, anche per la versione scritta.
Peter Cordes

2
L'ho provato su godbolt (con i tempi commentati, poiché godbolt non ha Boost installato). Sorprendentemente chiama effettivamente std::pow8 * cicli di volte (per esponente> 2), a meno che non si usi -fno-math-errno. Quindi può tirare fuori dal loop la chiamata pow, come pensavo. Immagino che poiché errno è un globale, la sicurezza del thread richiede che chiami pow per eventualmente impostare errno più volte ... exp = 1 ed exp = 2 sono veloci perché la chiamata pow viene sollevata dal ciclo solo con -O3.. ( con - ffast-math , fa anche la somma di 8 fuori dal ciclo.)
Peter Cordes

Ho downvoted prima di rendermi conto che avevo -ffast-math nella sessione Godbolt che stavo usando. Anche senza quello, testpow <1> e testpow <2> sono interrotti, perché sono in linea con la powchiamata sollevata dal circuito, quindi c'è un grosso difetto lì. Inoltre, sembra che tu stia principalmente testando la latenza dell'aggiunta FP, poiché tutti i test vengono eseguiti nella stessa quantità di tempo. Ti aspetteresti test5di essere più lento di test1, ma non lo è. L'utilizzo di più accumulatori dividerebbe la catena delle dipendenze e nasconderebbe la latenza.
Peter Cordes

@PeterCordes, dov'eri 5 anni fa? :-) Proverò a correggere il mio benchmark applicandolo powa un valore in continua evoluzione (per evitare che l'espressione pow ripetuta venga sollevata).
Emile Cormier

30

È il tipo di domanda sbagliato. La domanda giusta sarebbe: "Quale è più facile da capire per i lettori umani del mio codice?"

Se la velocità è importante (in seguito), non chiedere, ma misura. (E prima, misura se l'ottimizzazione di questo effettivamente farà una differenza notevole.) Fino ad allora, scrivi il codice in modo che sia più facile da leggere.

Modifica
Proprio per rendere questa chiara (anche se già avrebbe dovuto essere): incrementi nella velocità Breakthrough di solito provengono da cose come utilizzando algoritmi migliori , migliorando località dei dati , ridurre l'uso di memoria dinamica , i risultati pre-computing , ecc Raramente mai provengono da micro-ottimizzazione delle chiamate a funzione singola e, laddove lo fanno, lo fanno in pochissimi posti , il che sarebbe trovato solo da un'attenta (e dispendiosa in termini di tempo) profiling , il più delle volte possono essere velocizzati facendo molto non intuitivo cose (come inserirenoop dichiarazioni), e ciò che è un'ottimizzazione per una piattaforma a volte è una pessimazione per un'altra (motivo per cui è necessario misurare, invece di chiedere, perché non conosciamo / abbiamo completamente il tuo ambiente).

Consentitemi di sottolinearlo di nuovo: anche nelle poche applicazioni in cui queste cose sono importanti, non hanno importanza nella maggior parte dei luoghi in cui vengono utilizzate ed è molto improbabile che troverete i punti in cui sono importanti guardando il codice. È davvero necessario identificare prima i punti caldi , perché altrimenti l'ottimizzazione del codice è solo una perdita di tempo .

Anche se una singola operazione (come calcolare il quadrato di un certo valore) occupa il 10% del tempo di esecuzione dell'applicazione (che IME è piuttosto raro), e anche se l'ottimizzazione fa risparmiare il 50% del tempo necessario per quell'operazione (che IME è anche molto, molto più raro), hai comunque fatto in modo che l'applicazione impiegasse solo il 5% in meno di tempo .
I tuoi utenti avranno bisogno di un cronometro per accorgersene. (Immagino che nella maggior parte dei casi qualsiasi cosa sotto il 20% di velocità passi inosservata per la maggior parte degli utenti. E questi sono quattro punti di questo tipo che devi trovare.)


43
Potrebbe essere il giusto tipo di domanda. Forse non sta pensando al suo progetto pratico, ma è semplicemente interessato a come funziona il langauge / compilatore ...
Andreas Rejbrand

137
Stackoverflow dovrebbe avere un pulsante che inserisce un disclaimer standard: "So già che l'ottimizzazione prematura è dannosa, ma sto facendo questa domanda di ottimizzazione per scopi accademici o ho già identificato quella riga / blocco di codice come un collo di bottiglia".
Emile Cormier

39
Non credo che la leggibilità sia un problema qui. Scrivere x * x contro pow (x, 2) sembrano entrambi abbastanza chiari.
KillianDS

41
Uso eccessivo di grassetto e corsivo, non facile per gli occhi.
stagas

24
Non sono completamente d'accordo con questa risposta. È una domanda valida da porre sulla performance. Le migliori prestazioni che puoi ottenere a volte sono un requisito valido e spesso il motivo per cui qualcuno ha usato c ++ piuttosto che un altro linguaggio. E misurare non è sempre una buona idea. Potrei misurare il Bubble Sort e il Quicksort e trovare il Bubble Sort più velocemente con i miei 10 articoli perché non avevo lo sfondo per sapere che il numero di elementi è estremamente importante e scoprire che in seguito con i miei 1.000.000 di articoli è stata una pessima scelta.
jcoder

17

x*xo x*x*xsarà più veloce di pow, poiché powdeve occuparsi del caso generale, mentre x*xè specifico. Inoltre, puoi elide la chiamata di funzione e simili.

Tuttavia, se ti ritrovi a microottimizzare in questo modo, devi procurarti un profiler e fare una profilazione seria. L'incredibile probabilità è che non noterai mai alcuna differenza tra i due.


7
Stavo pensando la stessa cosa finché non ho deciso di provarlo. Ho appena testato il x*x*xvs double std::pow(double base, int exponent)in un loop temporizzato e non riesco a vedere una differenza di prestazioni statisticamente significativa.
Emile Cormier

2
Assicurati che non venga ottimizzato dal compilatore.
Ponkadoodle

1
@Emile: controlla il codice generato dal compilatore. Gli ottimizzatori a volte fanno alcune cose complicate (e non ovvie). Controlla anche le prestazioni a vari livelli di ottimizzazione: -O0, -O1, -O2 e -O3 ad esempio.
SOLO LA MIA PARERE corretta

2
Non si può presumere che le funzioni generalizzate siano più lente. A volte è vero il contrario perché il codice più semplice è più facile da ottimizzare per il compilatore.
cambunctious

5

Mi stavo anche chiedendo del problema delle prestazioni e speravo che sarebbe stato ottimizzato dal compilatore, in base alla risposta di @EmileCormier. Tuttavia, ero preoccupato che il codice di prova che ha mostrato avrebbe comunque consentito al compilatore di ottimizzare la chiamata std :: pow (), poiché gli stessi valori sono stati utilizzati nella chiamata ogni volta, il che avrebbe consentito al compilatore di memorizzare i risultati e riutilizzarlo nel ciclo - questo spiegherebbe i tempi di esecuzione quasi identici per tutti i casi. Quindi ho dato un'occhiata anche io.

Ecco il codice che ho usato (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;
}

Questo è stato compilato utilizzando:

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

Fondamentalmente, la differenza è che l'argomento di std :: pow () è il contatore del ciclo. Come temevo, la differenza di prestazioni è pronunciata. Senza il flag -O2, i risultati sul mio sistema (Arch Linux 64-bit, g ++ 4.9.1, Intel i7-4930) sono stati:

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)

Con l'ottimizzazione, i risultati sono stati altrettanto sorprendenti:

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)

Quindi sembra che il compilatore cerchi almeno di ottimizzare il caso std :: pow (x, 2), ma non il caso std :: pow (x, 3) (impiega circa 40 volte di più rispetto allo std :: pow (x, 2) caso). In tutti i casi, l'espansione manuale ha funzionato meglio, ma in particolare per il case Power 3 (60 volte più veloce). Vale sicuramente la pena tenerlo a mente se si esegue std :: pow () con potenze intere maggiori di 2 in un ciclo stretto ...


4

Il modo più efficiente è considerare la crescita esponenziale delle moltiplicazioni. Controlla questo codice per 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;
}

2

Se l'esponente è costante e piccolo, espanderlo riducendo al minimo il numero di moltiplicazioni. (Ad esempio, x^4non è ottimale x*x*x*x, ma y*ydove y=x*x. Ed x^5è y*y*xdove y=x*x. E così via.) Per esponenti interi costanti, scrivi già la forma ottimizzata; con piccoli esponenti, questa è un'ottimizzazione standard che dovrebbe essere eseguita indipendentemente dal fatto che il codice sia stato profilato o meno. Il modulo ottimizzato sarà più veloce in una percentuale così ampia di casi che in pratica vale sempre la pena farlo.

(Se usi Visual C ++, std::pow(float,int)esegue l'ottimizzazione a cui alludo, per cui la sequenza di operazioni è correlata al modello di bit dell'esponente. Non garantisco che il compilatore srotolerà il ciclo per te, quindi vale comunque la pena farlo a mano.)

[modifica] BTW powha una (non) sorprendente tendenza a comparire sui risultati del profiler. Se non ne hai assolutamente bisogno (ovvero, l'esponente è grande o non è una costante) e sei preoccupato per le prestazioni, allora è meglio scrivere il codice ottimale e attendere che il profiler ti dica che è (sorprendentemente ) perdere tempo prima di pensare oltre. (L'alternativa è chiamare powe chiedere al profiler di dirti che (non sorprendentemente) stai perdendo tempo: stai eliminando questo passaggio facendolo in modo intelligente.)


0

Sono stato impegnato con un problema simile e sono abbastanza perplesso dai risultati. Stavo calcolando x⁻³ / ² per la gravitazione newtoniana in una situazione di n-corpi (accelerazione subita da un altro corpo di massa M situato ad un vettore di distanza d): a = M G d*(d²)⁻³/²(dove d² è il prodotto scalare di d di per sé), e ho pensato che il calcolo M*G*pow(d2, -1.5)sarebbe stato più semplice diM*G/d2/sqrt(d2)

Il trucco è che è vero per i sistemi piccoli, ma man mano che le dimensioni dei sistemi crescono, M*G/d2/sqrt(d2)diventa più efficiente e non capisco perché la dimensione del sistema influisca su questo risultato, perché ripetere l'operazione su dati diversi non lo fa. È come se ci fossero possibili ottimizzazioni man mano che il sistema cresce, ma che non sono possibili conpow

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.