web-dev-qa-db-fra.com

Quoi de plus efficace? Utiliser pow to square ou simplement le multiplier par lui-même?

Laquelle de ces deux méthodes est la plus efficace en C? Et que diriez-vous:

pow(x,3)

vs.

x*x*x // etc?
111
user163408

J'ai testé la différence de performance entre x*x*... vs pow(x,i) pour les petits i à l'aide de 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

Ceci est sur un processeur Intel Core Duo exécutant Ubuntu 9.10 64bit. Compilé avec 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 surcharge pow(double, int). En C++, ce sera à peu près le même. (En supposant que la méthodologie de mes tests est correcte.)


C'est en réponse au commentaire de An Markm:

Même si une directive using namespace std a été émise, si le deuxième paramètre de pow est un int, la surcharge std::pow(double, int) de <cmath> sera appelé à la place de ::pow(double, double) depuis <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;
}
77
Emile Cormier

C'est le mauvais type 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 fera réellement une différence notable.) Jusque-là, écrivez le code pour qu'il soit le plus facile à lire.

Edit
Juste pour que ce soit clair (même si cela aurait déjà dû l’être): Les accélérations décisives proviennent généralement de choses comme utilisant de meilleurs algorithmes , - amélioration de la localité des données , réduisant l'utilisation de la mémoire dynamique , - résultats de pré-calcul , etc. Ils proviennent rarement de la micro-optimisation d'appels à une seule fonction , et où ils le font, ils le font en très peu d'endroits , qui ne seraient trouvés que par attention (et fastidieux) profiling , plus souvent que jamais, ils peuvent être accélérés en faisant des choses très intuitives (comme insérer des instructions noop), et ce qui est 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 le faisons pas. connaître parfaitement/avoir votre environnement).

Permettez-moi de souligner à nouveau ceci: même dans les quelques applications où de telles choses importent, elles ne comptent pas dans la plupart des endroits où elles sont utilisées, et c'est very peu probable que vous trouverez les endroits où ils comptent en regardant le code. Vous devez vraiment identifier les points chauds en premier , car sinon l'optimisation du code est juste une perte de temps .

Même si une seule opération (telle que calculer le carré d'une valeur) occupe 10% du temps d'exécution de l'application (dont IME est assez rare), et même si l'optimisation enregistre 50% du temps == nécessaire pour cette opération (dont l'IME est encore beaucoup, beaucoup plus rare), vous avez quand même créé l'application prenez seulement 5% moins de temps .
Vos utilisateurs auront besoin d’un chronomètre pour le remarquer. (Je suppose que dans la plupart des cas, tout accélération inférieure à 20% passe inaperçue pour la plupart des utilisateurs. Et que correspond à quatre de ces endroits qu'il vous faut trouver.)

31
sbi

x*x ou x*x*x sera plus rapide que pow, puisque pow doit traiter le cas général, alors que x*x est spécifique. En outre, vous pouvez éviter l'appel de fonction et autres.

Cependant, si vous vous trouvez dans une situation de micro-optimisation, vous devez vous procurer un profileur et en faire un profilage sérieux. La probabilité écrasante est que vous ne remarqueriez jamais de différence entre les deux.

16
Puppy

Je m'interrogeais également sur le problème de performances et espérais que le compilateur optimiserait le résultat, en se basant sur la réponse de @EmileCormier. Cependant, je craignais que le code de test présenté ne permette toujours au compilateur d’optimiser l’appel std :: pow (), car les mêmes valeurs étaient utilisées dans l’appel à chaque fois, ce qui permettrait au compilateur de stocker les résultats et le réutiliser dans la boucle - cela expliquerait les temps d'exécution presque identiques pour tous les cas. Alors j'ai jeté un coup d'oeil aussi.

Voici le code que j'ai utilisé (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;
}

Ceci 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 boucles. 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 les suivants:

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 optimisation, les résultats sont également 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 d'optimiser le cas std :: pow (x, 2), mais pas le cas std :: pow (x, 3) (cela prend environ 40 fois plus longtemps que le std :: pow (x, 2) cas). Dans tous les cas, l’extension manuelle s’est mieux comportée, mais particulièrement 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 étroite ...

5
jdtournier

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
mhaghighat

Si l'exposant est constant et petit, développez-le en minimisant le nombre de multiplications. (Par exemple, x^4 N'est pas optimal 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 à nombre entier constant, écrivez simplement le formulaire optimisé déjà; avec de petits exposants, il s'agit d'une optimisation standard à effectuer, que le code ait été profilé ou non. La forme optimisée sera plus rapide dans un pourcentage de cas si important que cela en vaut toujours la peine.

(Si vous utilisez Visual C++, std::pow(float,int) exécute l'optimisation évoquée, la séquence d'opérations étant liée au motif binaire de l'exposant. Je ne garantis pas que le compilateur déroulera la boucle pour vous, cependant, cela vaut toujours la peine de le faire à la main.)

[edit] BTW pow a une tendance (peu) surprenante à apparaître dans les résultats du profileur. Si vous n’en avez pas absolument besoin (c’est-à-dire que l’exposant est grand ou non constant) et que vous vous souciez de la performance, il est préférable d’écrire le code optimal et d’attendre que le profileur vous le dise (étonnamment ) perdre du temps avant de penser plus loin. (L'alternative consiste à appeler pow et à demander au profileur de perdre du temps (sans surprise) - vous supprimez cette étape en le faisant intelligemment.)

2
please delete me

J'ai été occupé par un problème similaire et les résultats me rendent perplexe. Je calculais x⁻³/² pour la gravitation newtonienne dans une situation de n-corps (accélération subie par un autre corps de masse M situé à un vecteur distance d): a = M G d*(d²)⁻³/² (où d² est le point (scalaire) produit de d par lui-même), et je pensais que calculer M*G*pow(d2, -1.5) serait plus simple que M*G/d2/sqrt(d2)

Le truc, c’est que c’est vrai pour les petits systèmes, mais à mesure que les systèmes grossissent, M*G/d2/sqrt(d2) devient plus efficace et je ne comprends pas pourquoi la taille du système a un impact sur ce résultat, car la répétition de l’opération sur différents les données ne le sont pas. C'est comme s'il y avait des optimisations possibles à mesure que le système se développe, mais qui ne sont pas possibles avec pow

enter image description here

0
Camion