Ce qui est plus efficace? En utilisant pow pour carré ou simplement le multiplier avec lui-même?

Qu'est-ce que de ces deux méthodes est en C plus efficace? Et que diriez-vous:

pow(x,3)

Vs.

x*x*x // etc?
101
demandé sur jamylak 2010-05-31 01:17:52

6 réponses

J'ai testé la différence de performance entre x*x*... vs pow(x,i) pour les petits i en utilisant ce code:

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

Les résultats sont:

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

Notez que j'accumule le résultat de chaque calcul de pow pour m'assurer que le compilateur ne l'optimise pas.

Si j'utilise la version std::pow(double, double), et loops = 1000000l, je reçois:

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

C'est sur un Intel Core Duo exécutant Ubuntu 9.10 64bit. Compilé en utilisant gcc 4.4.1 avec optimisation-o2.

, Donc en C, oui x*x*x sera plus rapide que pow(x, 3), car il n'y a pas de pow(double, int) surcharge. En C++, ce sera à peu près la même chose. (En supposant que la méthodologie dans mes tests est correcte.)


Ceci est en réponse au commentaire fait par un Markm:

Même si une directive using namespace std a été émise, si le deuxième paramètre de pow est un int, alors la surcharge std::pow(double, int) de <cmath> sera appelée à la place de ::pow(double, double) de <math.h>.

Ce code de test confirme ce comportement:

#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;
}
74
répondu Emile Cormier 2013-03-19 20:25:13

C'est le mauvais genre de question. La bonne question serait: "lequel est le plus facile à comprendre pour les lecteurs humains de mon code?"

Si la vitesse compte (plus tard), ne demandez pas, mais mesurez. (Et avant cela, mesurez si l'optimisation de cela fera réellement une différence notable.) Jusque-là, écrivez le code afin qu'il soit plus facile à lire.

Modifier
Juste pour que cela soit clair (bien que cela aurait déjà dû l'être): les accélérations révolutionnaires viennent généralement des choses comme les l'utilisation de meilleurs algorithmes, l'amélioration de la localité de données, la réduction de l'utilisation de la mémoire dynamique, pré-calcul des résultats, etc. Ils ont rarement jamais venu à partir de micro-optimisation de la fonction unique d'appels, et lorsqu'ils le font, ils le font dans très peu de places, ce qui ne pourrait être trouvé par attention (et de temps) profilage, le plus souvent jamais ils ne peuvent être accélérés en faisant des choses très non intuitives (comme l'insertion d'instructions noop), et ce qui est une optimisation pour une plate-forme est parfois une pessimisation pour une autre (c'est pourquoi vous devez mesurer, au lieu de demander, parce que nous ne connaissons pas complètement/avons votre environnement).

Permettez-moi de souligner à nouveau ceci: même dans les quelques applications où de telles choses comptent, elles n'ont pas d'importance dans la plupart des endroits où elles sont utilisées, et il est Très peu probable que vous trouviez les endroits où ils comptent en regardant le code. Vous avez vraiment besoin de identifier les points chauds de la première, parce que sinon, l'optimisation de code est juste une perte de temps.

, Même si une seule opération (comme le calcul du carré de la valeur) prend 10% de l'exécution de l'application en temps (qui IME qui est assez rare), et même si l'optimisation de ce dernier enregistre 50% du temps nécessaire pour cette opération (qui IME est même beaucoup, beaucoup plus rare), vous avez encore fait la demande de prendre seulement 5% de temps en moins.
Vos utilisateurs auront besoin d'un chronomètre pour même le remarquer. (Je suppose que dans la plupart des cas, quelque chose sous 20% speedup passe inaperçu pour la plupart des utilisateurs. Et que est quatre de ces endroits que vous devez trouver.)

27
répondu sbi 2010-05-30 23:38:31

x*x ou x*x*x sera plus rapide que pow depuis pow doit traiter le cas général, tandis que x*x est spécifique. En outre, vous pouvez elide l'appel de fonction et telchlike.

Cependant, si vous vous trouvez micro-optimisation comme ça, vous devez obtenir un profileur et faire un profilage sérieux. La probabilité écrasante est que vous ne remarquerez jamais aucune différence entre les deux.

14
répondu Puppy 2018-04-24 14:16:11

Je m'interrogeais également sur le problème des performances, et j'espérais que cela serait optimisé par le compilateur, basé sur la réponse de @EmileCormier. Cependant, j'étais inquiet que le code de test qu'il a montré permettrait toujours au compilateur d'optimiser l'appel std::pow (), puisque les mêmes valeurs étaient utilisées dans l'appel à chaque fois, ce qui permettrait au compilateur de stocker les résultats et de les réutiliser dans la boucle-cela expliquerait les temps d'exécution presque identiques pour tous les cas. J'ai donc eu un coup d'oeil trop.

Voici le code que j'ai utilisé (test_pow.rpc):

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

Cela a été compilé en utilisant:

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

Fondamentalement, la différence est l'argument de std:: pow () est le compteur de boucle. Comme je le craignais, la différence de performance est prononcée. Sans l'indicateur-O2, les résultats sur mon système (Arch Linux 64 bits, g++ 4.9.1, Intel i7-4930)étaient:

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)

Avec l'optimisation, les résultats étaient tout aussi frappants:

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)

Il semble donc que le compilateur essaie au moins pour optimiser le std::pow(x,2), mais pas le std::pow(x,3) cas (il faut ~40 fois plus longtemps que les std::pow(x,2)). Dans tous les cas, l'expansion manuelle a mieux fonctionné - mais en particulier pour le boîtier power 3 (60 fois plus rapide). Cela vaut vraiment la peine de garder à l'esprit si vous exécutez std::pow() avec des puissances entières supérieures à 2 dans une boucle serrée...

4
répondu jdtournier 2014-08-29 10:13:25

Le moyen le plus efficace est de considérer la croissance exponentielle des multiplications. Vérifiez ce code pour 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;
}
4
répondu mhaghighat 2015-11-02 00:24:11

Si l'exposant est constant et petit, développez-le, en minimisant le nombre de multiplications. (Par exemple, x^4 n'est pas optimale x*x*x*x, mais y*yy=x*x. Et x^5 est y*y*xy=x*x. Et ainsi de suite.) Pour les exposants entiers constants, il suffit d'écrire la forme optimisée déjà; avec les petits exposants, il s'agit d'une optimisation standard qui doit être effectuée, que le code ait été profilé ou non. Le formulaire optimisé sera plus rapide dans un si grand pourcentage de cas qu'il est fondamentalement toujours la peine de faire.

(si vous utilisez Visual C++, std::pow(float,int) effectue l'optimisation à laquelle je fais allusion, la séquence d'opérations étant liée au modèle de bits de l'exposant. Je ne garantis pas que le compilateur déroulera la boucle pour vous, donc ça vaut toujours la peine de le faire à la main.)

[edit] BTW pow a une tendance (non)surprenante à apparaître sur les résultats du profileur. Si vous n'avez pas absolument besoin (c'est à dire, l'exposant est grand ou pas une constante), et vous êtes du tout préoccupé par les performances, il est préférable d'écrire le code optimal et d'attendre que le profileur vous dise que c'est (étonnamment) perdre du temps avant de penser plus loin. (L'alternative est d'appeler pow et d'avoir le profileur vous dire que c'est (sans surprise) perdre du temps-vous découpez cette étape en le faisant intelligemment.)

2
répondu caf 2010-05-31 05:11:14