Apa dari kedua metode ini yang lebih efisien dalam C? Dan bagaimana dengan:
pow(x,3)
vs.
x*x*x // etc?
Apa dari kedua metode ini yang lebih efisien dalam C? Dan bagaimana dengan:
pow(x,3)
vs.
x*x*x // etc?
Jawaban:
Saya menguji perbedaan kinerja antara x*x*...
vs pow(x,i)
kecil i
menggunakan 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*x
akan 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 std
perintah dikeluarkan, jika parameter kedua ke pow
adalah 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;
}
std::pow
8 * 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.)
pow
panggilan 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 test5
menjadi lebih lambat dari test1
, tetapi ternyata tidak. Menggunakan beberapa akumulator akan memisahkan rantai ketergantungan dan menyembunyikan latensi.
pow
ke nilai yang selalu berubah (untuk mencegah ekspresi pow yang berulang ditarik keluar).
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.)
x*x
atau x*x*x
akan lebih cepat dari pow
, karena pow
harus menangani kasus umum, sedangkan x*x
spesifik. 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.
x*x*x
vs menggandakan std::pow(double base, int exponent)
dalam lingkaran waktu dan tidak dapat melihat perbedaan kinerja yang berarti secara statistik.
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 ...
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;
}
Jika eksponennya konstan dan kecil, perluas eksponennya untuk meminimalkan jumlah perkaliannya. (Misalnya, x^4
tidak optimal x*x*x*x
, tetapi di y*y
mana y=x*x
. Dan x^5
adalah di y*y*x
mana 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 pow
memiliki 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 pow
dan meminta profiler memberi tahu Anda bahwa (tidak mengherankan) membuang-buang waktu - Anda memotong langkah ini dengan melakukannya secara cerdas.)
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
x
integral atau floating point?