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?
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?
Câu trả lời:
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ỏ i
bằ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*x
sẽ 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 std
chỉ thị đã được ban hành, nếu tham số thứ hai pow
là 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;
}
std::pow
8 * 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á).
pow
gọ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 test5
sẽ 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ễ.
pow
cho 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).
Đó 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.)
x*x
hoặc x*x*x
sẽ nhanh hơn pow
, vì pow
phải giải quyết trường hợp chung, trong khi đó x*x
là 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.
x*x*x
so 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ê.
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ẽ ...
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;
}
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^4
không phải là tối ưu x*x*x*x
, nhưng y*y
nơi y=x*x
. Và x^5
là y*y*x
nơ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 pow
có 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 pow
và 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.)
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
x
tích phân hay dấu phẩy động?