O que é mais eficiente? Usando pow para elevar ao quadrado ou apenas multiplicá-lo por ele mesmo?

119

Qual desses dois métodos é mais eficiente em C? E que tal:

pow(x,3)

vs.

x*x*x // etc?
Jamylak
fonte
9
É xintegral ou ponto flutuante?
Matthew Flaschen
6
Você pode tentar escrever um programa que faça as duas operações acima e cronometrar quanto tempo a execução leva com uma biblioteca de perfis. Isso lhe dará uma boa resposta em termos de tempo de execução.
J. Polfer
3
Quando você diz eficiente, está se referindo a tempo ou espaço (ou seja, uso de memória)?
J. Polfer
4
@sheepsimulator: +1 por me poupar o tempo necessário para (novamente) apontar que escrever um teste rápido lhe dará uma resposta definitiva mais rápido do que você obterá uma resposta potencialmente vaga ou incorreta de SO.
APENAS MINHA OPINIÃO correta
5
@kirill_igum se esses valores de ponto flutuante não forem um bug, a aritmética de ponto flutuante não é associativa.
effeffe

Respostas:

82

Testei a diferença de desempenho entre x*x*...vs pow(x,i)para pequenos iusando 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*xvai 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 stddiretiva foi emitida, se o segundo parâmetro para powfor 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;
}
Emile Cormier
fonte
1
isso significa que inserir um "using namespace std" escolhe a opção C e isso será prejudicial para o tempo de execução?
Andreas
Em ambos os loops de temporização, o cálculo do pow provavelmente ocorre apenas uma vez. gcc -O2 não deve ter problemas para içar a expressão invariante do loop para fora do loop. Portanto, você está apenas testando o desempenho do compilador em transformar um loop de constante de adição em uma multiplicação ou apenas otimizando um loop de constante de adição. Há uma razão para que seus loops tenham a mesma velocidade com expoente = 1 vs. expoente = 5, mesmo para a versão escrita.
Peter Cordes,
2
Eu tentei no godbolt (com o tempo comentado, já que godbolt não tem Boost instalado). Surpreendentemente, ele realmente chama std::pow8 * 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.)
Peter Cordes,
Eu votei contra antes de perceber que tinha -rápido-matemática na sessão do godbolt que estava usando. Mesmo sem isso, testpow <1> e testpow <2> estão quebrados, porque eles estão em linha com a powchamada 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 test5ser 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.
Peter Cordes,
@PeterCordes, onde você estava 5 anos atrás? :-) Tentarei corrigir meu benchmark aplicando powa um valor em constante mudança (para evitar que a expressão pow repetida seja içada para fora).
Emile Cormier,
30

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.)

sbi
fonte
43
Pode ser o tipo certo de pergunta. Talvez ele não esteja pensando em seu próprio projeto prático, mas esteja apenas interessado em como o idioma / compilador funciona ...
Andreas Rejbrand
137
Stackoverflow deve ter um botão que insere uma isenção de responsabilidade padrão: "Eu já sei que a otimização prematura é má, mas estou fazendo esta pergunta de otimização para fins acadêmicos ou já identifiquei essa linha / bloco de código como um gargalo".
Emile Cormier
39
Não acho que a legibilidade seja um problema aqui. Escrever x * x versus pow (x, 2) parece bastante claro.
KillianDS
41
Uso excessivo de negrito e itálico, não é agradável para os olhos.
stagas
24
Não concordo inteiramente com esta resposta. É uma pergunta válida a se fazer sobre desempenho. O melhor desempenho que você pode alcançar é um requisito válido às vezes, e frequentemente o motivo pelo qual alguém usou c ++ em vez de outra linguagem. E medir nem sempre é uma boa ideia. Eu poderia medir a classificação por bolha e classificação rápida e encontrar classificação por bolhas mais rápido com meus 10 itens porque eu não tinha conhecimento para saber que o número de itens é muito importante e descobrir mais tarde com meus 1.000.000 itens que foi uma escolha muito ruim.
jcoder
17

x*xou x*x*xserá mais rápido do que pow, uma vez que powdeve 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.

Cachorro
fonte
7
Eu estava pensando a mesma coisa até que decidi testá-lo. Acabei de testar x*x*xvs duplo std::pow(double base, int exponent)em um loop cronometrado e não consigo ver uma diferença de desempenho estatisticamente significativa.
Emile Cormier
2
Certifique-se de que não está sendo otimizado pelo compilador.
Ponkadoodle
1
@Emile: Verifique o código gerado pelo compilador. Os otimizadores fazem algumas coisas complicadas (e inóbeis) às vezes. Verifique também o desempenho em vários níveis de otimização: -O0, -O1, -O2 e -O3, por exemplo.
APENAS MINHA OPINIÃO correta
2
Você não pode presumir que as funções generalizadas são mais lentas. Às vezes, o oposto é verdadeiro porque um código mais simples é mais fácil para o compilador otimizar.
cambunctious
5

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 ...

jdtournier
fonte
4

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;
}
mhaghighat
fonte
2

Se o expoente for constante e pequeno, expanda-o, minimizando o número de multiplicações. (Por exemplo, x^4não é ideal x*x*x*x, mas y*yonde y=x*x. E x^5é y*y*xonde 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 powtem 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 powe 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.)

caf
fonte
0

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

insira a descrição da imagem aqui

Camion
fonte