Quale di questi due metodi è più efficiente in C? E che ne dici di:
pow(x,3)
vs.
x*x*x // etc?
Quale di questi due metodi è più efficiente in C? E che ne dici di:
pow(x,3)
vs.
x*x*x // etc?
Risposte:
Ho testato la differenza di prestazioni tra x*x*...
vs pow(x,i)
per piccoli i
utilizzando 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 = 1000000l
ottengo:
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*x
sarà 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 std
stata 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;
}
std::pow
8 * 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.)
pow
chiamata 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 test5
di essere più lento di test1
, ma non lo è. L'utilizzo di più accumulatori dividerebbe la catena delle dipendenze e nasconderebbe la latenza.
pow
a un valore in continua evoluzione (per evitare che l'espressione pow ripetuta venga sollevata).
È 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.)
x*x
o x*x*x
sarà più veloce di pow
, poiché pow
deve 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.
x*x*x
vs double std::pow(double base, int exponent)
in un loop temporizzato e non riesco a vedere una differenza di prestazioni statisticamente significativa.
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 ...
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;
}
Se l'esponente è costante e piccolo, espanderlo riducendo al minimo il numero di moltiplicazioni. (Ad esempio, x^4
non è ottimale x*x*x*x
, ma y*y
dove y=x*x
. Ed x^5
è y*y*x
dove 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 pow
ha 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 pow
e chiedere al profiler di dirti che (non sorprendentemente) stai perdendo tempo: stai eliminando questo passaggio facendolo in modo intelligente.)
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
x
integrale o in virgola mobile?