Що ефективніше? Використовуючи Pow до квадрата або просто помножте його на себе?


119

Який із цих двох методів у С більш ефективний? А як щодо:

pow(x,3)

vs.

x*x*x // etc?

9
Це xцільна чи плаваюча точка?
Метью Флашен

6
Ви можете спробувати написати програму, яка виконує вищевказані дві операції, і час, який триває виконання з бібліотекою профілювання. Це дасть хорошу відповідь з точки зору часу виконання.
Дж. Полфер

3
Якщо ви говорите про ефективну, ви маєте на увазі час або простір (тобто використання пам'яті)?
Дж. Полфер

4
@sheepsimulator: +1, щоб заощадити мені час, необхідний (знову) зазначити, що написання швидкого тесту дасть вам швидку остаточну відповідь, ніж ви отримаєте потенційно розпливчасту або неправильну відповідь.
ДАЙТЕ МОЕ правильне ДУМКА

5
@kirill_igum, якщо це значення з плаваючою комою, що не є помилкою, арифметика з плаваючою комою не асоціативна.
efeffe

Відповіді:


82

Я перевірив різницю продуктивності між 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;
}

1
чи означає це, що при вставці "STD простору імен" вибирається варіант C, і це буде згубним для часу виконання?
Андреас

В обох циклах часу обчислення порогу, ймовірно, відбувається лише один раз. gcc -O2 не повинно мати жодних проблем з підняттям циклу-інваріантного виразу з циклу. Отже, ви просто тестуєте, наскільки добре компілятор перетворює цикл додавання-константи в множення або просто оптимізує цикл додавання-константи. Існує причина, що ваші петлі мають однакову швидкість з експонентом = 1 проти експонента = 5, навіть для виписаної версії.
Пітер Кордес

2
Я спробував це на godbolt (із зазначенням часу, оскільки у Godbolt не встановлено Boost). Дивно, але насправді викликає std::pow8 * циклів (для експонента> 2), якщо ви не використовуєте -fno-math-errno. Тоді він може витягнути виклик пороху з циклу, як я думав, що це буде. Я думаю, оскільки errno є глобальним, безпека потоку вимагає, щоб він викликав pow, можливо, встановив errno кілька разів ... exp = 1 і exp = 2 є швидкими, оскільки виклик pow піднімається з циклу просто -O3.. ( з - ffast-math , він також робить суму-8 поза циклом.)
Пітер Кордес

Я подав заяву до того, як зрозумів, що я маю -спокійну математику на сесії Godbolt, яку я використовував. Навіть без цього, testpow <1> і testpow <2> порушені, тому що вони вбудовані у powвиклик, що піднімається з циклу, тому там є великий недолік. Крім того, схоже, ви в основному випробовуєте затримку додавання FP, оскільки весь тест виконується за один і той же час. Ви б очікували, що test5це буде повільніше test1, але це не так. Використання декількох акумуляторів розділить ланцюг залежностей і приховає затримку.
Пітер Кордес

@PeterCordes, де ти був 5 років тому? :-) Я спробую виправити свій орієнтир, застосувавши powдо значення, що постійно змінюється (щоб запобігти витягуванню повторного вираження порошку).
Еміль Корм'є

30

Це неправильне питання. Правильним питанням було б: "Який із читачів мого коду легше зрозуміти для читачів?"

Якщо швидкість має значення (пізніше), не питайте, а міряйте. (А перед цим виміряйте, чи оптимізація цього насправді матиме помітну різницю.) До цього записуйте код так, щоб його було легше читати.

Редагуйте
лише для того, щоб це було зрозуміло (хоча це вже повинно було бути): проривні скорочення зазвичай виходять з таких речей, як використання кращих алгоритмів , поліпшення локальності даних , зменшення використання динамічної пам'яті , попередніх обчислень результатів тощо. Вони рідко коли-небудь з'являються мікрооптимізація однофункціональних дзвінків , і там, де вони роблять, вони роблять це в дуже небагатьох місцях , що було б виявлено лише ретельним (і трудомістким) профілюванням , частіше, ніж ніколи, їх можна прискорити, роблячи дуже неінтуїтивні речі (наприклад, вставленняnoop заяви), а оптимізація для однієї платформи іноді песимізація для іншої (саме тому вам потрібно вимірювати замість того, щоб запитувати, тому що ми не повністю знаємо / не маємо вашого оточення).

Дозвольте мені підкреслити це ще раз: Навіть в тих небагатьох випадках , коли такі речі важливі, вони не мають значення в більшості місць вони використовуються, і це дуже малоймовірно , що ви знайдете місця , де вони мають значення, дивлячись на код. Вам дійсно потрібно спочатку визначити «гарячі точки» , оскільки в іншому випадку оптимізація коду - це лише марна трата часу .

Навіть якщо одна операція (як, наприклад, обчислення площі деякого значення) займає 10% часу виконання програми (що IME досить рідко), і навіть якщо оптимізація економить 50% часу, необхідного для цієї операції (який IME є навіть набагато рідше), ви все-таки зробили додаток зайняти лише 5% менше часу .
Вашим користувачам знадобиться секундомір, щоб це навіть помітити. (Я припускаю , що в більшості випадків нічого під 20% прискорення проходить непомітно для більшості користувачів. А що є чотири таких місць , які потрібно знайти.)


43
Це може бути правильним питанням. Можливо, він не замислюється над власним практичним проектом, але його просто цікавить, як працює
лангаж

137
У Stackoverflow повинна бути кнопка, яка вставляє стандартну відмову: "Я вже знаю, що передчасна оптимізація - це зло, але я задаю це питання оптимізації в академічних цілях або я вже визначив цей рядок / блок коду як вузьке місце".
Еміль Корм'є

39
Я не думаю, що читабельність тут є проблемою. Написання x * x проти pow (x, 2) здаються обома цілком зрозумілими.
KillianDS

41
Надмірне використання жирного шрифту та курсиву, нелегке для очей.
стагас

24
Я не повністю згоден з цією відповіддю. Справедливе запитання щодо продуктивності. Найкраща ефективність, яку ви можете досягти, - іноді є дійсною вимогою, а часто причиною того, що хтось використовував c ++, а не іншу мову. І вимірювання не завжди є хорошою ідеєю. Я міг би виміряти сортування бульбашок і швидкосполучення та швидше знайти пухирці з моїми 10 предметами, тому що я не мав передумови знати, що кількість предметів має велике значення, і пізніше я знайду свої 1000 000 предметів, це був дуже поганий вибір.
jcoder

17

x*xабо x*x*xбуде швидше pow, оскільки powмає справу з загальним випадком, тоді x*xяк конкретний. Також ви можете ухилитись від виклику функції і подібних.

Однак, якщо ви виявите, що мікрооптимізація схожа на це, вам потрібно придбати профайлер і зробити серйозне профілювання. Переважна ймовірність полягає в тому, що ви ніколи не помітите різниці між ними.


7
Я думав те саме, поки не вирішив перевірити це. Я щойно тестував x*x*xvs double std::pow(double base, int exponent)у тимчасовому циклі і не бачу статистично значущої різниці в продуктивності.
Еміль Корм'є

2
Переконайтесь, що компілятор не оптимізується.
Ponkadoodle

1
@Emile: Перевірте код, сформований компілятором. Оптимізатори іноді роблять складні (і непомітні) речі. Також перевіряйте продуктивність на різних рівнях оптимізації: -O0, -O1, -O2 та -O3, наприклад.
ДАЙТЕ МОЕ правильне ДУМКА

2
Ви не можете припустити, що узагальнені функції повільніші. Іноді відбувається навпаки, оскільки більш простий код компілятору легше оптимізувати.
камбунгівний

5

Я також цікавився питанням про продуктивність, і сподівався, що компілятор буде оптимізовано, на основі відповіді @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 у тісному циклі ...


4

Найефективніший спосіб - розглянути експоненціальне зростання множень. Перевірте цей код на 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;
}

2

Якщо показник постійний і малий, розгорніть його, мінімізуючи кількість множень. (Наприклад, 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і профайлер скаже вам, що витрачаєте час (не дивно) - ви вирізаєте цей крок, роблячи це розумно.)


0

Я був зайнятий подібною проблемою, і я дуже спантеличений результатами. Я обчислював 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

введіть тут опис зображення

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.