Qual desses dois métodos é mais eficiente em C? E que tal:
pow(x,3)
vs.
x*x*x // etc?
Qual desses dois métodos é mais eficiente em C? E que tal:
pow(x,3)
vs.
x*x*x // etc?
Respostas:
Testei a diferença de desempenho entre x*x*...
vs pow(x,i)
para pequenos i
usando este código:
#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";
}
Os resultados são:
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
Observe que eu acumulo o resultado de cada cálculo de potência para garantir que o compilador não o otimize.
Se eu usar a std::pow(double, double)
versão e loops = 1000000l
, obtenho:
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
Este é um Intel Core Duo rodando Ubuntu 9.10 64bit. Compilado usando gcc 4.4.1 com otimização -o2.
Então em C sim x*x*x
vai ser mais rápido pow(x, 3)
, porque não há pow(double, int)
sobrecarga. Em C ++, será mais ou menos o mesmo. (Supondo que a metodologia em meu teste esteja correta.)
Esta é uma resposta ao comentário feito por An Markm:
Mesmo se uma using namespace std
diretiva foi emitida, se o segundo parâmetro para pow
for um int
, então a std::pow(double, int)
sobrecarga de <cmath>
será chamada em vez de ::pow(double, double)
de <math.h>
.
Este código de teste confirma esse 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 * tempos de loops (para expoente> 2), a menos que você use -fno-math-errno
. Então, ele pode puxar o pow call para fora do loop, como eu pensei que faria. Eu acho que como errno é global, thread safety requer que ele chame pow para possivelmente definir errno várias vezes ... exp = 1 e exp = 2 são rápidos porque a chamada de pow é içada para fora do loop com apenas -O3
.. ( com - ffast-math , ele faz a soma de 8 fora do loop também.)
pow
chamada suspensa do loop, então há uma grande falha aí. Além disso, parece que você está testando principalmente a latência da adição de FP, uma vez que todos os testes são executados no mesmo período de tempo. Você esperaria test5
ser mais lento do que test1
, mas não é. O uso de vários acumuladores dividiria a cadeia de dependências e ocultaria a latência.
pow
a um valor em constante mudança (para evitar que a expressão pow repetida seja içada para fora).
Esse é o tipo errado de pergunta. A pergunta certa seria: "Qual é mais fácil de entender para os leitores humanos do meu código?"
Se a velocidade for importante (mais tarde), não pergunte, mas meça. (E antes disso, avalie se otimizar isso realmente fará alguma diferença perceptível.) Até então, escreva o código para que seja mais fácil de ler.
Editar
Só para deixar isso claro (embora já devesse ter sido): acelerações revolucionárias geralmente vêm de coisas como usar algoritmos melhores , melhorar a localidade dos dados , reduzir o uso de memória dinâmica , resultados pré-computacionais , etc. Eles raramente vêm de micro-otimizando chamadas de função única e, onde o fazem, o fazem em poucos lugares , o que só seria encontrado por meio de um perfil cuidadoso (e demorado), mais frequentemente do que nunca, podem ser aceleradas por procedimentos muito não intuitivos coisas (como inserirnoop
), e o que é uma otimização para uma plataforma às vezes é uma pessimização para outra (é por isso que você precisa medir, em vez de perguntar, porque não conhecemos / temos totalmente o seu ambiente).
Deixe-me sublinhar isso novamente: mesmo nos poucos aplicativos em que essas coisas importam, elas não importam na maioria dos lugares em que são usadas, e é muito improvável que você encontre os lugares onde elas importam olhando o código. Você realmente precisa identificar os pontos críticos primeiro , porque, do contrário, otimizar o código é apenas uma perda de tempo .
Mesmo que uma única operação (como calcular o quadrado de algum valor) ocupe 10% do tempo de execução do aplicativo (o que IME é bastante raro), e mesmo que otimizá-lo economize 50% do tempo necessário para aquela operação (que IME é muito, muito mais raro), você ainda fez a aplicação demorar apenas 5% menos tempo .
Seus usuários precisarão de um cronômetro para perceber isso. (Eu acho que na maioria dos casos nada menos de 20% speedup passa despercebido para a maioria dos usuários. E que é quatro desses pontos que você precisa encontrar.)
x*x
ou x*x*x
será mais rápido do que pow
, uma vez que pow
deve lidar com o caso geral, enquanto x*x
é específico. Além disso, você pode omitir a chamada de função e coisas semelhantes.
No entanto, se você estiver micro-otimizando assim, precisará obter um criador de perfil e fazer alguns perfis sérios. A probabilidade esmagadora é que você nunca notaria qualquer diferença entre os dois.
x*x*x
vs duplo std::pow(double base, int exponent)
em um loop cronometrado e não consigo ver uma diferença de desempenho estatisticamente significativa.
Eu também estava me perguntando sobre o problema de desempenho e esperava que isso fosse otimizado pelo compilador, com base na resposta de @EmileCormier. No entanto, eu estava preocupado que o código de teste que ele mostrou ainda permitiria ao compilador otimizar a chamada std :: pow (), uma vez que os mesmos valores eram usados na chamada todas as vezes, o que permitiria ao compilador armazenar os resultados e reutilize-o no loop - isso explicaria os tempos de execução quase idênticos para todos os casos. Então eu também dei uma olhada nisso.
Aqui está o código que usei (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;
}
Isso foi compilado usando:
g++ -std=c++11 [-O2] test_pow.cpp -o test_pow
Basicamente, a diferença é que o argumento para std :: pow () é o contador de loop. Como eu temia, a diferença de desempenho é pronunciada. Sem o sinalizador -O2, os resultados em meu sistema (Arch Linux 64-bit, g ++ 4.9.1, Intel i7-4930) foram:
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)
Com a otimização, os resultados foram igualmente impressionantes:
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)
Portanto, parece que o compilador tenta pelo menos otimizar o caso std :: pow (x, 2), mas não o caso std :: pow (x, 3) (leva cerca de 40 vezes mais do que o caso std :: pow (x, 2) caso). Em todos os casos, a expansão manual teve um desempenho melhor - mas particularmente para o case power 3 (60 vezes mais rápido). Isso definitivamente vale a pena ter em mente se executar std :: pow () com potências inteiras maiores que 2 em um loop apertado ...
A maneira mais eficiente é considerar o crescimento exponencial das multiplicações. Verifique este código para 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 o expoente for constante e pequeno, expanda-o, minimizando o número de multiplicações. (Por exemplo, x^4
não é ideal x*x*x*x
, mas y*y
onde y=x*x
. E x^5
é y*y*x
onde y=x*x
. E assim por diante.) Para expoentes inteiros constantes, basta escrever a forma otimizada já; com pequenos expoentes, esta é uma otimização padrão que deve ser realizada quer o código tenha sido perfilado ou não. A forma otimizada será mais rápida em uma porcentagem tão grande de casos que basicamente sempre vale a pena fazer.
(Se você usar Visual C ++, std::pow(float,int)
executa a otimização a que aludi, em que a seqüência de operações está relacionada ao padrão de bits do expoente. Não garanto que o compilador irá desenrolar o loop para você, então ainda vale a pena fazer à mão.)
[editar] BTW pow
tem uma tendência (des) surpreendente de aparecer nos resultados do profiler. Se você não precisa absolutamente dele (ou seja, o expoente é grande ou não é uma constante), e você está preocupado com o desempenho, é melhor escrever o código ideal e esperar que o criador de perfil diga que é (surpreendentemente ) perder tempo antes de pensar mais. (A alternativa é chamar pow
e fazer com que o criador de perfil lhe diga que está (sem surpresa) perdendo tempo - você está cortando esta etapa ao fazê-lo de forma inteligente.)
Tenho estado ocupado com um problema semelhante e estou bastante intrigado com os resultados. Eu estava calculando x⁻³ / ² para a gravitação newtoniana em uma situação de n-corpos (aceleração sofrida por outro corpo de massa M situado em um vetor de distância d): a = M G d*(d²)⁻³/²
(onde d² é o produto de ponto (escalar) de d por si mesmo), e pensei que calcular M*G*pow(d2, -1.5)
seria mais simples do queM*G/d2/sqrt(d2)
O truque é que isso é verdade para sistemas pequenos, mas conforme os sistemas aumentam de tamanho, eles M*G/d2/sqrt(d2)
se tornam mais eficientes e não entendo por que o tamanho do sistema impacta esse resultado, porque repetir a operação em dados diferentes não afeta. É como se houvesse otimizações possíveis conforme o sistema cresce, mas que não são possíveis compow
x
integral ou ponto flutuante?