Daha verimli olan nedir? Pow'u karelemek için mi yoksa sadece kendisiyle çarpmak için mi kullanıyorsunuz?


119

Bu iki yöntemden hangisi C'de daha etkilidir? Ve nasıl olur:

pow(x,3)

vs.

x*x*x // etc?

9
Mi xayrılmaz veya kayan nokta?
Matthew Flaschen

6
Yukarıdaki iki işlemi yapan bir program yazmayı deneyebilirsiniz ve bir profil oluşturma kitaplığı ile uygulamanın ne kadar süreceğini ölçebilirsiniz. Bu, uygulama süresi açısından size iyi bir cevap verecektir.
J. Polfer

3
Verimli derken, zamandan mı yoksa mekandan mı (yani bellek kullanımı) mı bahsediyorsunuz?
J. Polfer

4
@sheepsimulator: Hızlı bir test yazmanın size SO'dan potansiyel olarak belirsiz veya yanlış bir cevap alacağınızdan daha hızlı kesin bir cevap vereceğini (tekrar) belirtmek için gereken zamandan tasarruf ettiğim için +1.
SADECE

5
@kirill_igum, eğer bunlar bir hata olmayan kayan nokta değerleriyse, kayan nokta aritmetiği ilişkilendirilebilir değildir.
effeffe

Yanıtlar:


82

Ben arasındaki performans farkı test x*x*...vs pow(x,i)küçük için ibu kodu kullanarak:

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

Sonuçlar:

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

Derleyicinin onu optimize etmediğinden emin olmak için her güç hesaplamasının sonucunu biriktirdiğime dikkat edin.

std::pow(double, double)Sürümü kullanırsam ve loops = 1000000lşunu elde ederim:

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

Bu, Ubuntu 9.10 64bit çalıştıran bir Intel Core Duo üzerindedir. -O2 optimizasyonlu gcc 4.4.1 kullanılarak derlenmiştir.

Yani C'de evet x*x*xdaha hızlı olacaktır pow(x, 3)çünkü pow(double, int)aşırı yük yoktur . C ++ 'da kabaca aynı olacaktır. (Testlerimdeki metodolojinin doğru olduğunu varsayarsak.)


Bu, An Markm tarafından yapılan yoruma cevaben:

Bir Bile using namespace stddirektif verildiği için ikinci parametre ise, powbir olduğunu int, ardından std::pow(double, int)gelen aşırı yük <cmath>yerine çağrılacak ::pow(double, double)gelen <math.h>.

Bu test kodu şu davranışı doğrular:

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

1
Bu, "ad alanı std kullanarak" eklemenin C seçeneğini seçtiği ve bu çalışma zamanına zarar vereceği anlamına mı geliyor?
Andreas

Zamanlama döngülerinizin her ikisinde de, güç hesaplaması muhtemelen yalnızca bir kez gerçekleşir. gcc -O2, döngüde değişmeyen ifadeyi döngüden çıkarırken sorun yaşamamalıdır. Yani, derleyicinin sabit bir ekleme döngüsünü çarpmaya dönüştürmede veya sadece sabit ekleme döngüsünü optimize etmede ne kadar başarılı olduğunu test ediyorsunuz. Yazılı sürüm için bile, döngülerinizin üs = 1 ve üs = 5 ile aynı hızda olmasının bir nedeni var.
Peter Cordes

2
Bunu godbolt üzerinde denedim (zamanlama yorumlandı, çünkü godbolt'ta Boost kurulu değil). Şaşırtıcı bir şekilde, std::powkullanmadığınız sürece , aslında 8 * döngü sürelerini (üs> 2 için) çağırır -fno-math-errno. Ardından, düşündüğüm gibi pow çağrısını döngüden çıkarabilir. Sanırım errno bir global olduğundan, iş parçacığı güvenliği pow çağrısının errno'yu birden çok kez ayarlamasını gerektirir ... exp = 1 ve exp = 2 hızlıdır çünkü pow çağrı döngüden sadece -O3.. ( ile - ffast-math , döngünün dışında da 8'in toplamını yapar.)
Peter Cordes

Kullandığım godbolt seansında en az matematik yaptığımı fark etmeden önce olumsuz oy verdim. Bu olmadan bile, testpow <1> ve testpow <2> bozuldu, çünkü bunlar powdöngüden çıkarılan çağrı ile aynı hizadalar , bu yüzden orada büyük bir kusur var. Ayrıca, tüm testler aynı sürede çalıştığından, çoğunlukla FP eklemenin gecikmesini test ediyormuşsunuz gibi görünüyor. Daha test5yavaş olmayı beklerdin test1, ama öyle değil. Birden fazla akümülatör kullanmak bağımlılık zincirini böler ve gecikmeyi gizler.
Peter Cordes

@PeterCordes, 5 yıl önce neredeydin? :-) powSürekli değişen bir değere uygulayarak kıyaslamamı düzeltmeyi deneyeceğim (tekrarlanan güç ifadesinin kaldırılmasını önlemek için).
Emile Cormier

30

Bu yanlış türden bir soru. Doğru soru şudur: "Kodumu okuyanlar için hangisini anlamak daha kolay?"

Hız önemliyse (daha sonra), sormayın, ölçün. (Ve ondan önce, bunu optimize etmenin gerçekten gözle görülür bir fark yaratıp yaratmayacağını ölçün.) O zamana kadar, okunması en kolay olan kodu yazın.

Düzenleme
Sadece yapmak için bu açık (zaten olmalıydı rağmen): Cephe speedups genellikle gibi şeyler geliyor daha iyi algoritmaları kullanarak , verilerin yerellik iyileştirilmesi , dinamik hafıza kullanımını azaltarak , sonuçları önceden hesaplama vb Bunlar nadiren gelen mikro optimizasyonlu tek işlev çağrıları ve yaptıkları yerlerde bunu çok az yerde yaparlar, ki bu yalnızca dikkatli (ve zaman alan) profilleme ile bulunabilir , çoğu zaman hiç sezgisel olmayan yöntemlerle hızlandırılamazlar. şeyler (eklemek gibinoop ifadeler) ve bir platform için optimizasyon ne ise bazen diğeri için bir karamsarlıktır (bu yüzden sormak yerine ölçmeniz gerekir, çünkü ortamınızı tam olarak bilmiyoruz / sahip değiliz).

Hatta bu tür şeyler önemli birkaç uygulamalarda, onlar alıştığınız çoğu yerde olsun yoktur ve: ver tekrar vurgulayalım öyle çok onlar koduna bakılarak önemli olan yerleri bulacaksınız olası. Gerçekten önce sıcak noktaları belirlemeniz gerekir , çünkü aksi takdirde kodu optimize etmek sadece zaman kaybıdır .

Tek bir işlem (belirli bir değerin karesini hesaplamak gibi) uygulamanın yürütme süresinin% 10'unu (ki bu IME oldukça nadirdir) alsa ve optimize etse bile , bu işlem için gereken sürenin% 50'sinden tasarruf etse bile (ki bu IME hatta çok, çok daha nadir), yine de uygulamanın yalnızca% 5 daha az zaman almasını sağladınız .
Kullanıcılarınızın bunu fark etmesi için bir kronometreye ihtiyacı olacaktır. (Hiçbir şey altında% 20 hızlanma çoğu kullanıcı için gözden kaçmış olur çoğu durumda sanırım. Ve o sen bulmalıyız dört tür lekeler içindir.)


43
Doğru türde bir soru olabilir. Belki kendi pratik projesini düşünmüyor, sadece dilin / derleyicinin nasıl çalıştığıyla ilgileniyor ...
Andreas Rejbrand,

137
Stackoverflow'da standart bir sorumluluk reddi beyanı ekleyen bir düğme bulunmalıdır: "Erken optimizasyonun kötü olduğunu zaten biliyorum, ancak bu optimizasyon sorusunu akademik amaçlar için soruyorum veya bu satır / kod bloğunu bir darboğaz olarak tanımladım".
Emile Cormier

39
Okunabilirliğin burada bir sorun olduğunu düşünmüyorum. X * x'e karşı pow (x, 2) yazmak oldukça açık görünüyor.
KillianDS

41
Kalın ve italik yazıların aşırı kullanımı, gözleri yormaz.
15'te stagas

24
Bu yanıta tamamen katılmıyorum. Performans hakkında sorulması gereken geçerli bir soru. Elde edebileceğiniz en iyi performans, bazen geçerli bir gereksinimdir ve çoğu zaman birinin başka bir dil yerine c ++ kullanmasının nedenidir. Ve ölçüm yapmak her zaman iyi bir fikir değildir. Kabarcık sıralamayı ve hızlı sıralamayı ölçebilir ve 10 öğemle baloncukları daha hızlı bulabilirim çünkü öğelerin sayısının çok önemli olduğunu bilecek bir geçmişe sahip değildim ve daha sonra 1.000.000 öğemle bunun çok kötü bir seçim olduğunu anladım.
jcoder

17

x*xveya x*x*xdaha hızlı olacaktır pow, çünkü powgenel durumla ilgilenmek zorundadır, oysa x*xspesifiktir. Ayrıca, işlev çağrısı ve benzerlerini de atlayabilirsiniz.

Bununla birlikte, kendinizi bu şekilde mikro optimizasyon bulursanız, bir profil oluşturucu edinmeniz ve ciddi bir profilleme yapmanız gerekir. En büyük olasılık, ikisi arasında hiçbir fark görmemenizdir.


7
Test etmeye karar verene kadar aynı şeyi düşünüyordum. Az önce zamanlanmış bir döngüde x*x*xçifte karşı test ettim std::pow(double base, int exponent)ve istatistiksel olarak anlamlı bir performans farkı göremiyorum.
Emile Cormier

2
Derleyici tarafından optimize edilmediğinden emin olun.
Ponkadoodle

1
@Emile: Derleyici tarafından üretilen kodu kontrol edin. Doktorlar bazen bazı hileli (ve açık olmayan) şeyler yaparlar. Ayrıca çeşitli optimizasyon seviyelerinde performansı kontrol edin: -O0, -O1, -O2 ve -O3 örneğin.
SADECE

2
Genelleştirilmiş işlevlerin daha yavaş olduğunu varsayamazsınız. Bazen bunun tersi doğrudur çünkü daha basit kod derleyicinin optimize etmesi daha kolaydır.
karmaşık

5

Performans sorununu da merak ediyordum ve @ EmileCormier'ın cevabına göre derleyici tarafından optimize edilmesini umuyordum. Bununla birlikte, gösterdiği test kodunun derleyicinin std :: pow () çağrısını optimize etmesine izin vereceğinden endişeliydim, çünkü çağrıda her seferinde aynı değerler kullanıldı, bu da derleyicinin sonuçları depolamasına ve döngüde yeniden kullanın - bu, tüm durumlar için neredeyse aynı çalışma sürelerini açıklar. Bu yüzden ben de bir göz attım.

İşte kullandığım kod (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;
}

Bu, şu şekilde derlenmiştir:

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

Temel olarak, std :: pow () 'nin argümanı döngü sayacıdır. Korktuğum gibi, performanstaki fark belirgindir. -O2 bayrağı olmadan, sistemimdeki sonuçlar (Arch Linux 64-bit, g ++ 4.9.1, Intel i7-4930) şunlardı:

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)

Optimizasyonla elde edilen sonuçlar da aynı derecede çarpıcıydı:

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)

Öyleyse, derleyici en azından std :: pow (x, 2) durumunu optimize etmeye çalışıyor gibi görünüyor, ancak std :: pow (x, 3) durumunu değil (std :: pow'den ~ 40 kat daha uzun sürer (x, 2) durum). Her durumda, manuel genişletme daha iyi sonuç verdi - ancak özellikle 3. güç durumunda (60 kat daha hızlı). Sıkı bir döngüde 2'den büyük tamsayı güçleri ile std :: pow () çalıştırıyorsanız, bu kesinlikle akılda tutulmaya değerdir ...


4

En verimli yol, çarpımların üstel büyümesini dikkate almaktır. P ^ q için bu kodu kontrol edin:

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

2

Üs sabit ve küçükse, çarpma sayısını en aza indirerek genişletin. (Örneğin, x^4optimal değil x*x*x*x, ama y*ynerede y=x*xVe. x^5Olduğu y*y*xyerde y=x*xVe böyle devam eder..) Sabit tamsayı üstlerin için, sadece zaten optimize formu yazma; küçük üslerle bu, kodun profilli olup olmadığına bakılmaksızın gerçekleştirilmesi gereken standart bir optimizasyondur. Optimize edilmiş form, temelde her zaman yapmaya değecek kadar büyük bir yüzdede daha hızlı olacaktır.

(Visual C ++ kullanırsanız, std::pow(float,int)ima ettiğim optimizasyonu gerçekleştirir, burada işlem sırası üssün bit kalıbı ile ilgilidir. Derleyicinin sizin için döngüyü açacağına dair hiçbir garanti vermiyorum, bu yüzden yine de yapmaya değer elle.)

[değiştir] BTW'nin powprofil oluşturucu sonuçlarında (hiç) şaşırtıcı olmayan bir eğilimi var. Buna kesinlikle ihtiyacınız yoksa (yani, üs büyükse veya sabit değilse) ve performans konusunda endişeleriniz varsa, en iyisi en iyi kodu yazıp profilcinin size bunu söylemesini bekleyin (şaşırtıcı bir şekilde ) daha fazla düşünmeden önce zaman kaybetmek. (Alternatifi arayıp powprofilcinin size zaman kaybettiğini söylemesini sağlamaktır (şaşırtıcı bir şekilde) - akıllıca yaparak bu adımı atlıyorsunuz.)


0

Benzer bir problemle meşguldüm ve sonuçlar beni oldukça şaşırttı. Bir n-cisim durumunda Newton kütlesel çekimi için x⁻³ / ² hesaplıyordum (ivme, d mesafe vektöründe yer alan başka bir M kütlesinden kaynaklanan ivme): a = M G d*(d²)⁻³/²(burada d², d'nin kendi başına nokta (skaler) çarpımıdır), ve hesaplamanın M*G*pow(d2, -1.5)daha basit olacağını düşündümM*G/d2/sqrt(d2)

İşin püf noktası, bunun küçük sistemler için doğru olmasıdır, ancak sistemler büyüdükçe M*G/d2/sqrt(d2)daha verimli hale gelir ve sistemin boyutunun bu sonucu neden etkilediğini anlamıyorum, çünkü işlemi farklı veriler üzerinde tekrarlamak etkilemez. Sanki sistem büyüdükçe olası optimizasyonlar varmış gibi, ancak bunlar mümkün değilpow

görüntü açıklamasını buraya girin

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.