Điều gì là hiệu quả hơn? Sử dụng pow để vuông hay chỉ nhân nó với chính nó?


119

Phương pháp nào trong hai phương pháp này trong C hiệu quả hơn? Và làm thế nào về:

pow(x,3)

vs.

x*x*x // etc?

9
xtích phân hay dấu phẩy động?
Matthew Flaschen

6
Bạn có thể thử viết một chương trình thực hiện hai thao tác trên và thời gian thực thi mất bao lâu với một thư viện cấu hình. Điều đó sẽ cung cấp cho bạn một câu trả lời tốt về thời gian thực hiện.
J. Polfer

3
Khi bạn nói hiệu quả, bạn đang nói đến thời gian hay không gian (tức là sử dụng bộ nhớ)?
J. Polfer

4
@sheepsimulator: +1 vì đã giúp tôi tiết kiệm thời gian cần thiết để (một lần nữa) chỉ ra rằng viết một bài kiểm tra nhanh sẽ cung cấp cho bạn câu trả lời chính xác nhanh hơn so với việc bạn sẽ nhận được câu trả lời có khả năng mơ hồ hoặc không chính xác từ SO.
CHỈ LÀ Ý KIẾN chính xác của TÔI,

5
@kirill_igum nếu đó là các giá trị dấu phẩy động không phải là lỗi thì số học dấu phẩy động không có tính liên kết.
effeffe

Câu trả lời:


82

Tôi đã kiểm tra sự khác biệt hiệu suất giữa x*x*...so pow(x,i)với nhỏ ibằng cách sử dụng mã này:

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

Kết quả là:

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

Lưu ý rằng tôi tích lũy kết quả của mọi phép tính pow để đảm bảo trình biên dịch không tối ưu hóa nó.

Nếu tôi sử dụng std::pow(double, double)phiên bản và loops = 1000000l, tôi nhận được:

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

Đây là trên Intel Core Duo chạy Ubuntu 9.10 64bit. Được biên dịch bằng cách sử dụng gcc 4.4.1 với tối ưu hóa -o2.

Vì vậy, trong C, có x*x*xsẽ nhanh hơn pow(x, 3), bởi vì không có pow(double, int)quá tải. Trong C ++, nó sẽ gần giống như vậy. (Giả sử phương pháp trong thử nghiệm của tôi là đúng.)


Đây là phản hồi cho nhận xét của An Markm:

Ngay cả khi một using namespace stdchỉ thị đã được ban hành, nếu tham số thứ hai powlà an int, thì std::pow(double, int)quá tải từ <cmath>sẽ được gọi thay vì ::pow(double, double)từ <math.h>.

Mã kiểm tra này xác nhận rằng hành vi:

#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
điều này có nghĩa là việc chèn một std "using namespace" sẽ chọn tùy chọn C và điều này sẽ gây bất lợi cho thời gian chạy?
Andreas

Trong cả hai vòng thời gian của bạn, phép tính pow có thể chỉ xảy ra một lần. gcc -O2 sẽ không có vấn đề gì khi kéo biểu thức bất biến vòng lặp ra khỏi vòng lặp. Vì vậy, bạn chỉ đang kiểm tra xem trình biên dịch hoạt động tốt như thế nào trong việc chuyển một vòng lặp cộng hằng số thành một nhân hoặc chỉ tối ưu hóa một vòng lặp hằng số cộng. Có một lý do khiến các vòng lặp của bạn có cùng tốc độ với exponent = 1 so với exponent = 5, ngay cả đối với phiên bản viết ra.
Peter Cordes

2
Tôi đã thử nó trên godbolt (với thời gian đã nhận xét, vì godbolt không được cài đặt Boost). Đáng ngạc nhiên là nó thực sự gọi std::pow8 * lần lặp (cho số mũ> 2), trừ khi bạn sử dụng -fno-math-errno. Sau đó, nó có thể kéo lệnh gọi pow ra khỏi vòng lặp, như tôi nghĩ. Tôi đoán vì errno là toàn cục, nên an toàn luồng yêu cầu nó gọi pow để có thể đặt errno nhiều lần ... exp = 1 và exp = 2 rất nhanh vì lệnh gọi pow được đưa ra khỏi vòng lặp chỉ -O3.. ( với - ffast-môn toán , nó tổng số 8 bên ngoài vòng lặp, quá).
Peter Cordes

Tôi đã phản đối trước khi tôi nhận ra rằng tôi đã bật -ffast-toán trong phiên giao dịch chốt cửa mà tôi đang sử dụng. Ngay cả khi không có điều đó, testpow <1> và testpow <2> vẫn bị hỏng, bởi vì chúng phù hợp với lệnh powgọi bị kéo ra khỏi vòng lặp, vì vậy có một lỗ hổng lớn ở đó. Ngoài ra, có vẻ như bạn chủ yếu đang kiểm tra độ trễ của việc bổ sung FP, vì tất cả các thử nghiệm đều chạy trong cùng một khoảng thời gian. Bạn mong đợi test5sẽ chậm hơn test1, nhưng không phải vậy. Sử dụng nhiều bộ tích lũy sẽ chia nhỏ chuỗi phụ thuộc và ẩn độ trễ.
Peter Cordes

@PeterCordes, bạn đã ở đâu 5 năm trước? :-) Tôi sẽ thử sửa điểm chuẩn của mình bằng cách áp dụng powcho một giá trị luôn thay đổi (để ngăn biểu thức pow lặp lại không bị kéo ra ngoài).
Emile Cormier

30

Đó là loại câu hỏi sai. Câu hỏi đúng sẽ là: "Cái nào dễ hiểu hơn đối với những người đọc mã của tôi?"

Nếu tốc độ là vấn đề (sau này), đừng hỏi mà hãy đo lường. (Và trước đó, hãy đo lường xem việc tối ưu hóa điều này có thực sự tạo ra bất kỳ sự khác biệt đáng chú ý nào không.) Sau đó, hãy viết mã sao cho nó dễ đọc nhất.

Chỉnh sửa
Chỉ để làm rõ điều này (mặc dù lẽ ra là vậy): Tốc độ đột phá thường đến từ những thứ như sử dụng các thuật toán tốt hơn , cải thiện vị trí của dữ liệu , giảm việc sử dụng bộ nhớ động , kết quả tính toán trước , v.v. Chúng hiếm khi đến từ tối ưu hóa vi mô các lệnh gọi chức năng đơn lẻ và chúng thực hiện ở đâu, chúng thực hiện như vậy ở rất ít nơi , điều này sẽ chỉ được tìm thấy bằng cách lập hồ sơ cẩn thận (và tốn thời gian), thường xuyên hơn không bao giờ chúng có thể được tăng tốc bằng cách làm rất không trực quan những thứ (như chènnoop và tối ưu hóa cho một nền tảng đôi khi là một sự bi quan cho nền tảng khác là gì (đó là lý do tại sao bạn cần đo lường, thay vì hỏi vì chúng tôi không hoàn toàn biết / có môi trường của bạn).

Hãy để tôi nhấn mạnh này một lần nữa: Ngay cả trong số ít những ứng dụng nơi mà mọi thứ như vậy có vấn đề, họ không quan trọng trong hầu hết các nơi mà họ đang sử dụng, và nó là rất không chắc rằng bạn sẽ tìm thấy nơi họ có vấn đề bằng cách nhìn vào mã. Bạn thực sự cần phải xác định các điểm nóng trước , bởi vì nếu không tối ưu hóa mã chỉ là một sự lãng phí thời gian .

Ngay cả khi một thao tác đơn lẻ (như tính bình phương của một giá trị nào đó) chiếm 10% thời gian thực thi của ứng dụng (IME là khá hiếm) và ngay cả khi tối ưu hóa nó sẽ tiết kiệm 50% thời gian cần thiết cho thao tác đó (IME là thậm chí nhiều, hiếm hơn nhiều), bạn vẫn làm cho ứng dụng chỉ mất 5% thời gian .
Người dùng của bạn sẽ cần một chiếc đồng hồ bấm giờ để nhận thấy điều đó. (Tôi đoán trong hầu hết các trường hợp, bất kỳ thứ gì tăng tốc dưới 20% đều không được hầu hết người dùng chú ý. Và đó là bốn điểm bạn cần tìm.)


43
Nó có thể là loại câu hỏi phù hợp. Có lẽ ông không được suy nghĩ về dự án thực tế của mình, nhưng chỉ quan tâm đến việc làm thế nào các langauge / công trình biên dịch ...
Andreas Rejbrand

137
Stackoverflow phải có một nút chèn tuyên bố từ chối trách nhiệm tiêu chuẩn: "Tôi đã biết rằng tối ưu hóa quá sớm là xấu, nhưng tôi đang đặt câu hỏi tối ưu hóa này cho mục đích học thuật hoặc tôi đã xác định dòng / khối mã đó là một nút cổ chai".
Emile Cormier

39
Tôi không nghĩ rằng khả năng đọc là một vấn đề ở đây. Viết x * x so với pow (x, 2) có vẻ khá rõ ràng.
KillianDS

41
Sử dụng quá nhiều nét đậm và nghiêng, không dễ dàng cho mắt.
stagas

24
Tôi không hoàn toàn đồng ý với câu trả lời này. Đó là một câu hỏi hợp lệ để hỏi về hiệu suất. Hiệu suất tốt nhất mà bạn có thể đạt được đôi khi là một yêu cầu hợp lệ và thường là lý do khiến ai đó sử dụng c ++ chứ không phải ngôn ngữ khác. Và đo lường không phải lúc nào cũng là một ý tưởng hay. Tôi có thể đo lường sắp xếp bong bóng và sắp xếp nhanh và tìm bong bóng sắp xếp nhanh hơn với 10 mục của mình bởi vì tôi không có cơ sở để biết rằng số lượng mục rất quan trọng và sau này tìm thấy với 1.000.000 mục của tôi, đó là một lựa chọn rất tồi.
jcoder

17

x*xhoặc x*x*xsẽ nhanh hơn pow, vì powphải giải quyết trường hợp chung, trong khi đó x*xlà trường hợp cụ thể. Ngoài ra, bạn có thể bỏ qua lệnh gọi hàm và tương tự như vậy.

Tuy nhiên, nếu bạn thấy mình đang tối ưu hóa vi mô như thế này, bạn cần phải có một hồ sơ và thực hiện một số hồ sơ nghiêm túc. Khả năng cao là bạn sẽ không bao giờ nhận thấy bất kỳ sự khác biệt nào giữa hai điều này.


7
Tôi cũng nghĩ như vậy cho đến khi tôi quyết định thử nghiệm nó. Tôi vừa thử nghiệm x*x*xso với gấp đôi std::pow(double base, int exponent)trong một vòng lặp thời gian và không thể thấy sự khác biệt về hiệu suất có ý nghĩa thống kê.
Emile Cormier

2
Đảm bảo rằng nó không bị trình biên dịch tối ưu hóa.
Ponkadoodle

1
@Emile: Kiểm tra mã do trình biên dịch tạo ra. Các trình tối ưu hóa đôi khi thực hiện một số việc phức tạp (và không rõ ràng). Cũng kiểm tra hiệu suất ở các mức tối ưu hóa khác nhau: -O0, -O1, -O2 và -O3 chẳng hạn.
CHỈ LÀ Ý KIẾN CHÍNH XÁC CỦA TÔI

2
Bạn không thể cho rằng các chức năng tổng quát chậm hơn. Đôi khi điều ngược lại là đúng bởi vì mã đơn giản hơn sẽ dễ dàng hơn để trình biên dịch tối ưu hóa.
ăn ngon miệng

5

Tôi cũng băn khoăn về vấn đề hiệu suất và hy vọng điều này sẽ được trình biên dịch tối ưu hóa, dựa trên câu trả lời từ @EmileCormier. Tuy nhiên, tôi lo lắng rằng mã kiểm tra mà anh ấy hiển thị vẫn cho phép trình biên dịch tối ưu hóa lệnh gọi std :: pow (), vì các giá trị giống nhau được sử dụng trong lệnh gọi mọi lúc, điều này sẽ cho phép trình biên dịch lưu trữ kết quả và sử dụng lại nó trong vòng lặp - điều này sẽ giải thích thời gian chạy gần như giống nhau cho mọi trường hợp. Vì vậy, tôi cũng đã xem xét nó.

Đây là mã tôi đã sử dụng (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;
}

Điều này được biên dịch bằng cách sử dụng:

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

Về cơ bản, sự khác biệt là đối số của std :: pow () là bộ đếm vòng lặp. Như tôi lo sợ, sự khác biệt về hiệu suất là rõ rệt. Nếu không có cờ -O2, kết quả trên hệ thống của tôi (Arch Linux 64-bit, g ++ 4.9.1, Intel i7-4930) là:

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)

Với tối ưu hóa, kết quả cũng ấn tượng không kém:

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)

Vì vậy, có vẻ như trình biên dịch ít nhất cố gắng tối ưu hóa trường hợp std :: pow (x, 2), nhưng không phải trường hợp std :: pow (x, 3) (mất khoảng 40 lần so với trường hợp std :: pow (x, 2) trường hợp). Trong mọi trường hợp, mở rộng thủ công hoạt động tốt hơn - nhưng đặc biệt đối với trường hợp nguồn 3 (nhanh hơn 60 lần). Điều này chắc chắn đáng lưu ý nếu chạy std :: pow () với lũy thừa số nguyên lớn hơn 2 trong một vòng lặp chặt chẽ ...


4

Cách hiệu quả nhất là xem xét sự tăng trưởng theo cấp số nhân của các phép nhân. Kiểm tra mã này cho 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

Nếu số mũ không đổi và nhỏ, hãy mở rộng nó ra, giảm thiểu số phép nhân. (Ví dụ, x^4không phải là tối ưu x*x*x*x, nhưng y*ynơi y=x*x. Và x^5y*y*xnơiy=x*x . Và như vậy.) Đối với số mũ nguyên không đổi, chỉ cần viết ra dạng đã tối ưu hóa rồi; với số mũ nhỏ, đây là một tối ưu hóa tiêu chuẩn nên được thực hiện cho dù mã đã được cấu hình hay chưa. Biểu mẫu được tối ưu hóa sẽ nhanh hơn trong một tỷ lệ lớn các trường hợp mà về cơ bản nó luôn đáng làm.

(Nếu bạn sử dụng Visual C ++, hãy std::pow(float,int)thực hiện tối ưu hóa mà tôi ám chỉ, theo đó chuỗi hoạt động có liên quan đến mẫu bit của số mũ. Tuy nhiên, tôi không đảm bảo rằng trình biên dịch sẽ giải nén vòng lặp cho bạn, vì vậy nó vẫn đáng làm nó bằng tay.)

[sửa] BTW powcó một xu hướng (không) đáng ngạc nhiên là cắt xén các kết quả của hồ sơ. Nếu bạn không thực sự cần nó (tức là, số mũ lớn hoặc không phải là hằng số) và bạn hoàn toàn quan tâm đến hiệu suất, thì tốt nhất hãy viết ra mã tối ưu và đợi trình biên dịch cho bạn biết nó (thật ngạc nhiên ) lãng phí thời gian trước khi nghĩ xa hơn. (Cách thay thế là gọi điện powvà yêu cầu trình biên dịch cho bạn biết việc đó (không có gì đáng ngạc nhiên) đang lãng phí thời gian - bạn đang cắt bỏ bước này bằng cách thực hiện nó một cách thông minh.)


0

Tôi đã bận rộn với một vấn đề tương tự và tôi khá khó hiểu với kết quả. Tôi đang tính x⁻³ / ² cho lực hấp dẫn Newton trong trường hợp n vật thể (gia tốc truyền từ một vật thể khác có khối lượng M nằm ở vectơ khoảng cách d): a = M G d*(d²)⁻³/²(trong đó d² là tích chấm (vô hướng) của chính d), và tôi nghĩ việc tính toán M*G*pow(d2, -1.5)sẽ đơn giản hơnM*G/d2/sqrt(d2)

Bí quyết là nó đúng với các hệ thống nhỏ, nhưng khi các hệ thống phát triển về kích thước, M*G/d2/sqrt(d2)trở nên hiệu quả hơn và tôi không hiểu tại sao kích thước của hệ thống lại ảnh hưởng đến kết quả này, bởi vì việc lặp lại thao tác trên các dữ liệu khác nhau thì không. Như thể có khả năng tối ưu hóa khi hệ thống phát triển, nhưng không thể vớipow

nhập mô tả hình ảnh ở đây

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.