Який із цих двох методів у С більш ефективний? А як щодо:
pow(x,3)
vs.
x*x*x // etc?
Який із цих двох методів у С більш ефективний? А як щодо:
pow(x,3)
vs.
x*x*x // etc?
Відповіді:
Я перевірив різницю продуктивності між x*x*...
vs pow(x,i)
для малих, i
використовуючи цей код:
#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";
}
Результати:
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
Зауважте, що я накопичую результат кожного розрахунку порошку, щоб переконатися, що компілятор не оптимізує його.
Якщо я використовую std::pow(double, double)
версію, і loops = 1000000l
я отримую:
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
Це на Intel Core Duo, на якому працює 64-бітний Ubuntu 9.10. Складено за допомогою gcc 4.4.1 з оптимізацією -o2.
Так що в C так, x*x*x
буде швидше, ніж немає pow(x, 3)
, оскільки немає pow(double, int)
перевантаження. У C ++ це буде приблизно однаково. (Якщо припустити, що методологія мого тестування є правильною.)
Це є у відповідь на коментар Ан Марма:
Навіть якщо using namespace std
директива була видана, якщо другим параметром до pow
є int
, то std::pow(double, int)
перевантаження з <cmath>
буде викликано замість ::pow(double, double)
з <math.h>
.
Цей тестовий код підтверджує таку поведінку:
#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 * циклів (для експонента> 2), якщо ви не використовуєте -fno-math-errno
. Тоді він може витягнути виклик пороху з циклу, як я думав, що це буде. Я думаю, оскільки errno є глобальним, безпека потоку вимагає, щоб він викликав pow, можливо, встановив errno кілька разів ... exp = 1 і exp = 2 є швидкими, оскільки виклик pow піднімається з циклу просто -O3
.. ( з - ffast-math , він також робить суму-8 поза циклом.)
pow
виклик, що піднімається з циклу, тому там є великий недолік. Крім того, схоже, ви в основному випробовуєте затримку додавання FP, оскільки весь тест виконується за один і той же час. Ви б очікували, що test5
це буде повільніше test1
, але це не так. Використання декількох акумуляторів розділить ланцюг залежностей і приховає затримку.
pow
до значення, що постійно змінюється (щоб запобігти витягуванню повторного вираження порошку).
Це неправильне питання. Правильним питанням було б: "Який із читачів мого коду легше зрозуміти для читачів?"
Якщо швидкість має значення (пізніше), не питайте, а міряйте. (А перед цим виміряйте, чи оптимізація цього насправді матиме помітну різницю.) До цього записуйте код так, щоб його було легше читати.
Редагуйте
лише для того, щоб це було зрозуміло (хоча це вже повинно було бути): проривні скорочення зазвичай виходять з таких речей, як використання кращих алгоритмів , поліпшення локальності даних , зменшення використання динамічної пам'яті , попередніх обчислень результатів тощо. Вони рідко коли-небудь з'являються мікрооптимізація однофункціональних дзвінків , і там, де вони роблять, вони роблять це в дуже небагатьох місцях , що було б виявлено лише ретельним (і трудомістким) профілюванням , частіше, ніж ніколи, їх можна прискорити, роблячи дуже неінтуїтивні речі (наприклад, вставленняnoop
заяви), а оптимізація для однієї платформи іноді песимізація для іншої (саме тому вам потрібно вимірювати замість того, щоб запитувати, тому що ми не повністю знаємо / не маємо вашого оточення).
Дозвольте мені підкреслити це ще раз: Навіть в тих небагатьох випадках , коли такі речі важливі, вони не мають значення в більшості місць вони використовуються, і це дуже малоймовірно , що ви знайдете місця , де вони мають значення, дивлячись на код. Вам дійсно потрібно спочатку визначити «гарячі точки» , оскільки в іншому випадку оптимізація коду - це лише марна трата часу .
Навіть якщо одна операція (як, наприклад, обчислення площі деякого значення) займає 10% часу виконання програми (що IME досить рідко), і навіть якщо оптимізація економить 50% часу, необхідного для цієї операції (який IME є навіть набагато рідше), ви все-таки зробили додаток зайняти лише 5% менше часу .
Вашим користувачам знадобиться секундомір, щоб це навіть помітити. (Я припускаю , що в більшості випадків нічого під 20% прискорення проходить непомітно для більшості користувачів. А що є чотири таких місць , які потрібно знайти.)
x*x
або x*x*x
буде швидше pow
, оскільки pow
має справу з загальним випадком, тоді x*x
як конкретний. Також ви можете ухилитись від виклику функції і подібних.
Однак, якщо ви виявите, що мікрооптимізація схожа на це, вам потрібно придбати профайлер і зробити серйозне профілювання. Переважна ймовірність полягає в тому, що ви ніколи не помітите різниці між ними.
x*x*x
vs double std::pow(double base, int exponent)
у тимчасовому циклі і не бачу статистично значущої різниці в продуктивності.
Я також цікавився питанням про продуктивність, і сподівався, що компілятор буде оптимізовано, на основі відповіді @EmileCormier. Однак я побоювався, що тестовий код, який він показав, все-таки дозволить компілятору оптимізувати виклик std :: pow (), оскільки в виклику кожен раз використовуються однакові значення, що дозволить компілятору зберігати результати та повторне використання його в циклі - це пояснило б майже однаковий час виконання для всіх випадків. Тож я і в це заглянув.
Ось код, який я використав (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;
}
Це було складено за допомогою:
g++ -std=c++11 [-O2] test_pow.cpp -o test_pow
По суті, різниця є аргументом, щоб std :: pow () - це лічильник циклу. Як я побоювався, різниця у виконанні яскраво виражена. Без прапора -O2 результати в моїй системі (Arch Linux 64-біт, g ++ 4.9.1, Intel i7-4930) були:
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)
Оптимізація результатів була однаково вражаючою:
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)
Так виглядає, що компілятор намагається хоча б оптимізувати випадок std :: pow (x, 2), але не випадок std :: pow (x, 3) (це займає ~ 40 разів довше, ніж std :: pow (х, 2) відмінок). У всіх випадках розширення вручну вийшло краще, але особливо для 3-ти корпусного (60 разів швидшого). Це, безумовно, варто мати на увазі, якщо запускати std :: pow () з цілими потужностями більше 2 у тісному циклі ...
Найефективніший спосіб - розглянути експоненціальне зростання множень. Перевірте цей код на 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;
}
Якщо показник постійний і малий, розгорніть його, мінімізуючи кількість множень. (Наприклад, x^4
не оптимальне x*x*x*x
, але y*y
де y=x*x
і. x^5
Є y*y*x
де y=x*x
і так далі.) . Для постійних цілочисельних індексів, просто виписують оптимізовану форму вже; з малими експонентами, це стандартна оптимізація, яку слід проводити, чи був код профільований чи ні. Оптимізована форма буде швидшою в настільки великому відсотку випадків, що це в принципі завжди варто робити.
(Якщо ви використовуєте Visual C ++, std::pow(float,int)
здійснює оптимізацію, на яку я натякаю, завдяки чому послідовність операцій пов'язана з бітовою схемою експонента. Я не гарантую, що компілятор розкрутить цикл для вас, хоча, тому все-таки варто робити це вручну.)
[редагувати] BTW pow
має (не) дивну тенденцію до обрізання результатів профілера. Якщо він вам абсолютно не потрібен (тобто показник великий чи не постійний), і ви взагалі стурбовані продуктивністю, тоді краще виписати оптимальний код і дочекатися, коли профілер скаже вам, що це (на диво ) витрачати час, перш ніж думати далі. (Альтернатива - зателефонувати, pow
і профайлер скаже вам, що витрачаєте час (не дивно) - ви вирізаєте цей крок, роблячи це розумно.)
Я був зайнятий подібною проблемою, і я дуже спантеличений результатами. Я обчислював x⁻³ / ² для гравітації ньютонів у ситуації з n-тілами (прискорення, яке зазнало інше тіло масою M, розташоване у векторному відстані d): a = M G d*(d²)⁻³/²
(де d² - крапка (скалярний) добуток d сама по собі), і я думав, що обчислити M*G*pow(d2, -1.5)
буде простіше, ніжM*G/d2/sqrt(d2)
Хитрість полягає в тому, що це стосується малих систем, але, оскільки системи збільшуються в розмірах, вони M*G/d2/sqrt(d2)
стають більш ефективними, і я не розумію, чому розмір системи впливає на цей результат, тому що повторення операції на різних даних не означає. Це як би можливі оптимізації в міру зростання системи, але які неможливіpow
x
цільна чи плаваюча точка?