Mana yang lebih efisien? Menggunakan pow untuk mengkuadratkan atau hanya mengalikannya dengan dirinya sendiri?

119

Apa dari kedua metode ini yang lebih efisien dalam C? Dan bagaimana dengan:

pow(x,3)

vs.

x*x*x // etc?
jamylak
sumber
9
Apakah xintegral atau floating point?
Matthew Flaschen
6
Anda dapat mencoba menulis program yang melakukan dua operasi di atas, dan menghitung waktu berapa lama eksekusi yang dibutuhkan dengan library profil. Itu akan memberi Anda jawaban yang bagus dalam hal waktu eksekusi.
J. Polfer
3
Ketika Anda mengatakan efisien, apakah Anda mengacu pada waktu, atau ruang (mis., Penggunaan memori)?
J. Polfer
4
@sheepsimulator: +1 untuk menghemat waktu yang saya butuhkan untuk (lagi) menunjukkan bahwa menulis tes cepat akan memberi Anda jawaban pasti lebih cepat daripada Anda akan mendapatkan jawaban yang berpotensi tidak jelas atau salah dari SO.
HANYA PENDAPAT SAYA yang benar
5
@ kirill_igum jika itu adalah nilai floating point yang bukan bug, aritmatika floating point tidak asosiatif.
effeffe

Jawaban:

82

Saya menguji perbedaan kinerja antara x*x*...vs pow(x,i)kecil imenggunakan kode ini:

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

Hasilnya adalah:

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

Perhatikan bahwa saya mengumpulkan hasil dari setiap perhitungan kekuatan untuk memastikan compiler tidak mengoptimalkannya.

Jika saya menggunakan std::pow(double, double)versi tersebut, dan loops = 1000000l, saya mendapatkan:

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

Ini menggunakan Intel Core Duo yang menjalankan Ubuntu 9.10 64bit. Dikompilasi menggunakan gcc 4.4.1 dengan optimasi -o2.

Jadi di C, ya x*x*xakan lebih cepat dari pow(x, 3), karena tidak ada pow(double, int)kelebihan beban. Di C ++, kira-kira sama. (Dengan asumsi metodologi dalam pengujian saya benar.)


Ini adalah tanggapan atas komentar yang dibuat oleh An Markm:

Bahkan jika using namespace stdperintah dikeluarkan, jika parameter kedua ke powadalah an int, maka std::pow(double, int)kelebihan beban dari <cmath>akan dipanggil, bukan ::pow(double, double)dari <math.h>.

Kode tes ini mengkonfirmasi perilaku itu:

#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
sumber
1
apakah ini berarti bahwa memasukkan "using namespace std" memilih opsi C dan ini akan merugikan runtime?
Andreas
Di kedua putaran waktu Anda, penghitungan kekuatan mungkin hanya terjadi sekali. gcc -O2 seharusnya tidak mengalami masalah dalam mengangkat ekspresi loop-invariant keluar dari loop. Jadi Anda hanya menguji seberapa baik kompilator dalam mengubah loop add-konstan menjadi multiply, atau hanya mengoptimalkan loop add-konstan. Ada alasan mengapa putaran Anda memiliki kecepatan yang sama dengan eksponen = 1 vs. eksponen = 5, bahkan untuk versi yang dituliskan.
Peter Cordes
2
Saya mencobanya di godbolt (dengan waktu dikomentari, karena godbolt tidak menginstal Boost). Secara mengejutkan sebenarnya memanggil std::pow8 * loop kali (untuk eksponen> 2), kecuali Anda menggunakan -fno-math-errno. Kemudian itu dapat menarik panggilan pow keluar dari loop, seperti yang saya kira. Saya kira karena errno adalah global, keamanan utas mengharuskannya memanggil pow untuk mungkin mengatur errno beberapa kali ... exp = 1 dan exp = 2 cepat karena panggilan pow diangkat keluar dari loop hanya dengan -O3.. ( dengan - ffast-math , itu melakukan penjumlahan-8 di luar loop, juga.)
Peter Cordes
Saya memilih downvot sebelum saya menyadari bahwa saya memiliki -cepat-matematika di sesi godbolt yang saya gunakan. Bahkan tanpa itu, testpow <1> dan testpow <2> akan rusak, karena keduanya sejalan dengan powpanggilan yang dikeluarkan dari loop, jadi ada cacat besar di sana. Selain itu, sepertinya Anda sebagian besar menguji latensi penambahan FP, karena semua pengujian berjalan dalam jumlah waktu yang sama. Anda berharap test5menjadi lebih lambat dari test1, tetapi ternyata tidak. Menggunakan beberapa akumulator akan memisahkan rantai ketergantungan dan menyembunyikan latensi.
Peter Cordes
@PeterCordes, di mana Anda 5 tahun yang lalu? :-) Saya akan mencoba memperbaiki tolok ukur saya dengan menerapkan powke nilai yang selalu berubah (untuk mencegah ekspresi pow yang berulang ditarik keluar).
Emile Cormier
30

Itu pertanyaan yang salah. Pertanyaan yang tepat adalah: "Manakah yang lebih mudah dipahami oleh pembaca manusia kode saya?"

Jika kecepatan itu penting (nanti), jangan tanya, tapi ukur. (Dan sebelum itu, ukur apakah pengoptimalan ini benar-benar akan membuat perbedaan nyata.) Sementara itu, tulis kode sehingga paling mudah dibaca.

Mengedit
Hanya untuk membuat ini jelas (meskipun sudah seharusnya): pemercepat Terobosan biasanya datang dari hal-hal seperti menggunakan algoritma yang lebih baik , meningkatkan lokalitas data , mengurangi penggunaan memori dinamis , hasil pra-komputasi , dll Mereka jarang pernah datang dari mikro-mengoptimalkan panggilan fungsi tunggal , dan di mana mereka melakukannya, mereka melakukannya di sangat sedikit tempat , yang hanya dapat ditemukan dengan pembuatan profil yang hati-hati (dan memakan waktu), lebih sering daripada tidak pernah mereka dapat dipercepat dengan melakukan sangat non-intuitif hal-hal (seperti memasukkannoop pernyataan), dan apa yang merupakan pengoptimalan untuk satu platform terkadang merupakan pesimisasi untuk yang lain (itulah mengapa Anda perlu mengukur, daripada bertanya, karena kami tidak sepenuhnya tahu / memiliki lingkungan Anda).

Mari saya menggarisbawahi ini lagi: Bahkan dalam beberapa aplikasi di mana hal-hal seperti masalah, mereka tidak peduli di kebanyakan tempat mereka digunakan, dan itu sangat tidak mungkin bahwa Anda akan menemukan tempat di mana mereka peduli dengan melihat kode. Anda benar-benar perlu mengidentifikasi titik panas terlebih dahulu , karena jika tidak, mengoptimalkan kode hanya membuang-buang waktu .

Sekalipun operasi tunggal (seperti menghitung kuadrat dari beberapa nilai) membutuhkan 10% waktu eksekusi aplikasi (IME cukup jarang), dan bahkan jika pengoptimalannya menghemat 50% waktu yang diperlukan untuk operasi itu (IME adalah Bahkan jauh lebih jarang), Anda masih membuat aplikasi hanya membutuhkan waktu 5% lebih sedikit .
Pengguna Anda akan membutuhkan stopwatch untuk menyadarinya. (Saya kira dalam banyak kasus, kecepatan di bawah 20% tidak diketahui oleh sebagian besar pengguna. Dan itu adalah empat titik yang perlu Anda temukan.)

sbi
sumber
43
Ini mungkin jenis pertanyaan yang tepat. Mungkin dia tidak memikirkan proyek praktiknya sendiri, tetapi hanya tertarik pada cara kerja bahasa / kompiler ...
Andreas Rejbrand
137
Stackoverflow harus memiliki tombol yang menyisipkan penafian standar: "Saya sudah tahu bahwa pengoptimalan prematur itu jahat, tetapi saya menanyakan pertanyaan pengoptimalan ini untuk tujuan akademis atau saya telah mengidentifikasi baris / blok kode itu sebagai penghambat".
Emile Cormier
39
Saya tidak berpikir keterbacaan menjadi masalah di sini. Penulisan x * x versus pow (x, 2) tampaknya cukup jelas.
KillianDS
41
Penggunaan huruf tebal dan miring secara berlebihan, tidak enak dipandang.
stagas
24
Saya tidak sepenuhnya setuju dengan jawaban ini. Ini adalah pertanyaan yang valid untuk ditanyakan tentang kinerja. Performa terbaik yang dapat Anda capai terkadang merupakan persyaratan yang valid, dan sering kali menjadi alasan seseorang menggunakan c ++ daripada bahasa lain. Dan mengukur tidak selalu merupakan ide yang bagus. Saya dapat mengukur bubble sort dan quicksort dan menemukan bubble sort lebih cepat dengan 10 item saya karena saya tidak memiliki latar belakang untuk mengetahui bahwa jumlah item sangat penting dan kemudian menemukan dengan 1.000.000 item saya, itu adalah pilihan yang sangat buruk.
jcoder
17

x*xatau x*x*xakan lebih cepat dari pow, karena powharus menangani kasus umum, sedangkan x*xspesifik. Juga, Anda dapat memilih pemanggilan fungsi dan sejenisnya.

Namun, jika Anda mendapati diri Anda melakukan pengoptimalan mikro seperti ini, Anda perlu mendapatkan profiler dan melakukan beberapa profil yang serius. Kemungkinan yang luar biasa adalah Anda tidak akan pernah melihat adanya perbedaan di antara keduanya.

Anak anjing
sumber
7
Saya memikirkan hal yang sama sampai saya memutuskan untuk mengujinya. Saya baru saja menguji x*x*xvs menggandakan std::pow(double base, int exponent)dalam lingkaran waktu dan tidak dapat melihat perbedaan kinerja yang berarti secara statistik.
Emile Cormier
2
Pastikan itu tidak dioptimalkan oleh kompiler.
Ponkadoodle
1
@Emile: Periksa kode yang dihasilkan oleh kompilator. Pengoptimal terkadang melakukan beberapa hal yang rumit (dan tidak terlihat). Periksa juga kinerja di berbagai tingkat pengoptimalan: -O0, -O1, -O2 dan -O3 misalnya.
HANYA PENDAPAT SAYA yang benar
2
Anda tidak dapat berasumsi bahwa fungsi umum lebih lambat. Terkadang yang terjadi justru sebaliknya karena kode yang lebih sederhana lebih mudah untuk dioptimalkan oleh compiler.
cambunctious
5

Saya juga bertanya-tanya tentang masalah kinerja, dan berharap ini akan dioptimalkan oleh kompiler, berdasarkan jawaban dari @EmileCormier. Namun, saya khawatir bahwa kode pengujian yang dia tunjukkan akan tetap memungkinkan compiler untuk mengoptimalkan panggilan std :: pow (), karena nilai yang sama digunakan dalam panggilan setiap saat, yang akan memungkinkan compiler untuk menyimpan hasil dan menggunakannya kembali dalam loop - ini akan menjelaskan run-time yang hampir identik untuk semua kasus. Jadi saya juga memeriksanya.

Berikut kode yang saya gunakan (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;
}

Ini dikompilasi menggunakan:

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

Pada dasarnya, perbedaannya adalah argumen ke std :: pow () adalah penghitung perulangan. Seperti yang saya takutkan, perbedaan kinerja terlihat jelas. Tanpa tanda -O2, hasil di sistem saya (Arch Linux 64-bit, g ++ 4.9.1, Intel i7-4930) adalah:

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)

Dengan pengoptimalan, hasilnya sama-sama mengejutkan:

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)

Jadi sepertinya kompilator setidaknya mencoba untuk mengoptimalkan kasus std :: pow (x, 2), tetapi tidak kasus std :: pow (x, 3) (dibutuhkan ~ 40 kali lebih lama dari std :: pow (x, 2) kasus). Dalam semua kasus, ekspansi manual berkinerja lebih baik - tetapi terutama untuk casing daya 3 (60 kali lebih cepat). Ini pasti perlu diingat jika menjalankan std :: pow () dengan kekuatan integer lebih besar dari 2 dalam loop yang ketat ...

jdtournier
sumber
4

Cara yang paling efisien adalah dengan mempertimbangkan pertumbuhan eksponensial dari perkalian. Periksa kode ini untuk 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.dll
sumber
2

Jika eksponennya konstan dan kecil, perluas eksponennya untuk meminimalkan jumlah perkaliannya. (Misalnya, x^4tidak optimal x*x*x*x, tetapi di y*ymana y=x*x. Dan x^5adalah di y*y*xmana y=x*x. Dan seterusnya.) Untuk eksponen bilangan bulat konstan, tulis saja bentuk yang dioptimalkan sudah; dengan eksponen kecil, ini adalah pengoptimalan standar yang harus dilakukan baik kode telah diprofilkan atau belum. Bentuk yang dioptimalkan akan lebih cepat dalam persentase kasus yang begitu besar sehingga pada dasarnya selalu layak dilakukan.

(Jika Anda menggunakan Visual C ++, std::pow(float,int)melakukan pengoptimalan yang saya singgung, di mana urutan operasi terkait dengan pola bit eksponen. Saya tidak menjamin bahwa kompilator akan membuka gulungan loop untuk Anda, jadi masih layak dilakukan itu dengan tangan.)

[Sunting] BTW powmemiliki kecenderungan (tidak) mengejutkan untuk muncul di hasil profiler. Jika Anda tidak benar-benar membutuhkannya (yaitu, eksponennya besar atau bukan konstanta), dan Anda sama sekali khawatir tentang performa, maka yang terbaik adalah menulis kode yang optimal dan menunggu profiler memberi tahu Anda (secara mengejutkan ) membuang-buang waktu sebelum berpikir lebih jauh. (Alternatifnya adalah menelepon powdan meminta profiler memberi tahu Anda bahwa (tidak mengherankan) membuang-buang waktu - Anda memotong langkah ini dengan melakukannya secara cerdas.)

kafe
sumber
0

Saya telah sibuk dengan masalah yang sama, dan saya cukup bingung dengan hasilnya. Saya menghitung x⁻³ / ² untuk gravitasi Newtonian dalam situasi benda-n (percepatan yang dialami dari benda bermassa M lain yang terletak pada jarak vektor d): a = M G d*(d²)⁻³/²(di mana d² adalah hasil kali titik (skalar) dari d dengan sendirinya), dan saya pikir menghitung M*G*pow(d2, -1.5)akan lebih sederhana daripadaM*G/d2/sqrt(d2)

Triknya adalah hal itu benar untuk sistem kecil, tetapi seiring bertambahnya ukuran sistem, M*G/d2/sqrt(d2)menjadi lebih efisien dan saya tidak mengerti mengapa ukuran sistem memengaruhi hasil ini, karena mengulangi operasi pada data yang berbeda tidak. Seolah-olah ada kemungkinan pengoptimalan saat sistem tumbuh, tetapi tidak mungkin dilakukanpow

masukkan deskripsi gambar di sini

Truk
sumber