Chúng tôi có câu hỏi là có một sự khác biệt hiệu suất giữa i++
và ++i
trong C ?
Câu trả lời cho C ++ là gì?
Chúng tôi có câu hỏi là có một sự khác biệt hiệu suất giữa i++
và ++i
trong C ?
Câu trả lời cho C ++ là gì?
Câu trả lời:
[Tóm tắt điều hành: Sử dụng ++i
nếu bạn không có lý do cụ thể để sử dụng i++
.]
Đối với C ++, câu trả lời phức tạp hơn một chút.
Nếu i
là một loại đơn giản (không phải là một thể hiện của lớp C ++), thì câu trả lời được đưa ra cho C ("Không có sự khác biệt về hiệu năng") , vì trình biên dịch đang tạo mã.
Tuy nhiên, nếu i
là một thể hiện của lớp C ++, thì i++
và ++i
đang thực hiện các cuộc gọi đến một trong các operator++
hàm. Đây là một cặp tiêu chuẩn của các chức năng này:
Foo& Foo::operator++() // called for ++i
{
this->data += 1;
return *this;
}
Foo Foo::operator++(int ignored_dummy_value) // called for i++
{
Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler
++(*this);
return tmp;
}
Vì trình biên dịch không tạo mã, mà chỉ gọi một operator++
hàm, không có cách nào để tối ưu hóa tmp
biến và hàm tạo sao chép liên quan của nó. Nếu hàm tạo sao chép đắt tiền, thì điều này có thể có tác động hiệu suất đáng kể.
Đúng. Có.
Toán tử ++ có thể hoặc không thể được định nghĩa là một hàm. Đối với các kiểu nguyên thủy (int, double, ...) các toán tử được tích hợp sẵn, do đó trình biên dịch có thể sẽ tối ưu hóa mã của bạn. Nhưng trong trường hợp một đối tượng xác định toán tử ++ thì mọi thứ lại khác.
Hàm toán tử ++ (int) phải tạo một bản sao. Đó là bởi vì postfix ++ dự kiến sẽ trả về một giá trị khác với giá trị mà nó giữ: nó phải giữ giá trị của nó trong một biến tạm thời, tăng giá trị của nó và trả về temp. Trong trường hợp toán tử ++ (), tiền tố ++, không cần tạo bản sao: đối tượng có thể tự tăng và sau đó chỉ cần trả về chính nó.
Dưới đây là một minh họa về điểm:
struct C
{
C& operator++(); // prefix
C operator++(int); // postfix
private:
int i_;
};
C& C::operator++()
{
++i_;
return *this; // self, no copy created
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
Mỗi khi bạn gọi toán tử ++ (int), bạn phải tạo một bản sao và trình biên dịch không thể làm bất cứ điều gì về nó. Khi được lựa chọn, sử dụng toán tử ++ (); Bằng cách này, bạn không lưu một bản sao. Nó có thể có ý nghĩa trong trường hợp có nhiều gia số (vòng lặp lớn?) Và / hoặc các đối tượng lớn.
C t(*this); ++(*this); return t;
của dòng này Trong dòng thứ hai, bạn đang tăng đúng con trỏ này, vậy làm thế nào để t
được cập nhật nếu bạn tăng số này. Không phải các giá trị này đã được sao chép vào t
?
The operator++(int) function must create a copy.
không có nó không phải là. Không có nhiều bản sao hơnoperator++()
Đây là một điểm chuẩn cho trường hợp khi các toán tử gia tăng ở các đơn vị dịch khác nhau. Trình biên dịch với g ++ 4.5.
Bỏ qua các vấn đề phong cách cho bây giờ
// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
int main () {
Something s;
for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
std::clock_t a = clock();
for (int i=0; i<1024*1024*30; ++i) ++s;
a = clock() - a;
for (int i=0; i<1024*1024*30; ++i) s++; // warm up
std::clock_t b = clock();
for (int i=0; i<1024*1024*30; ++i) s++;
b = clock() - b;
std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
<< ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
return 0;
}
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
for (auto it=data.begin(), end=data.end(); it!=end; ++it)
++*it;
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
Kết quả (thời gian tính bằng giây) với g ++ 4.5 trên máy ảo:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 1.70 2.39
-DPACKET_SIZE=50 -O3 0.59 1.00
-DPACKET_SIZE=500 -O1 10.51 13.28
-DPACKET_SIZE=500 -O3 4.28 6.82
Bây giờ chúng ta hãy lấy tập tin sau:
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> data;
};
Something& Something::operator++()
{
return *this;
}
Something Something::operator++(int)
{
Something ret = *this;
++*this;
return ret;
}
Nó không làm gì trong sự gia tăng. Điều này mô phỏng trường hợp khi tăng có độ phức tạp không đổi.
Kết quả bây giờ rất khác nhau:
Flags (--std=c++0x) ++i i++
-DPACKET_SIZE=50 -O1 0.05 0.74
-DPACKET_SIZE=50 -O3 0.08 0.97
-DPACKET_SIZE=500 -O1 0.05 2.79
-DPACKET_SIZE=500 -O3 0.08 2.18
-DPACKET_SIZE=5000 -O3 0.07 21.90
Nếu bạn không cần giá trị trước đó, hãy tạo thói quen sử dụng gia tăng trước. Hãy nhất quán ngay cả với các loại dựng sẵn, bạn sẽ quen với nó và không có nguy cơ bị mất hiệu suất không cần thiết nếu bạn thay thế một loại dựng sẵn bằng một loại tùy chỉnh.
i++
nói increment i, I am interested in the previous value, though
.++i
nói increment i, I am interested in the current value
hay increment i, no interest in the previous value
. Một lần nữa, bạn sẽ quen với nó, ngay cả khi bạn không ngay bây giờ.Tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Như là bi quan sớm.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
, đừng bận tâm đến cấu trúc cây thực tế (BSP, kd, Quadtree, Octree Grid, v.v.). Một ví dụ iterator sẽ cần phải duy trì một số quốc gia, ví dụ như parent node
, child node
, index
và các công cụ như thế. Nói chung, lập trường của tôi là, ngay cả khi chỉ có một vài ví dụ tồn tại, ...
Hoàn toàn không đúng khi nói rằng trình biên dịch không thể tối ưu hóa bản sao biến tạm thời trong trường hợp hậu tố. Một thử nghiệm nhanh với VC cho thấy, ít nhất, nó có thể làm điều đó trong một số trường hợp nhất định.
Trong ví dụ sau, mã được tạo giống hệt với tiền tố và hậu tố, ví dụ:
#include <stdio.h>
class Foo
{
public:
Foo() { myData=0; }
Foo(const Foo &rhs) { myData=rhs.myData; }
const Foo& operator++()
{
this->myData++;
return *this;
}
const Foo operator++(int)
{
Foo tmp(*this);
this->myData++;
return tmp;
}
int GetData() { return myData; }
private:
int myData;
};
int main(int argc, char* argv[])
{
Foo testFoo;
int count;
printf("Enter loop count: ");
scanf("%d", &count);
for(int i=0; i<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
Cho dù bạn làm ++ testFoo hay testFoo ++, bạn vẫn sẽ nhận được mã kết quả tương tự. Trong thực tế, không cần đọc số đếm từ người dùng, trình tối ưu hóa đã khiến mọi thứ giảm xuống mức không đổi. Vậy đây:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
Kết quả như sau:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
Vì vậy, trong khi đó chắc chắn là phiên bản postfix có thể chậm hơn, nhưng cũng có thể là trình tối ưu hóa sẽ đủ tốt để thoát khỏi bản sao tạm thời nếu bạn không sử dụng nó.
Các Google C ++ Style Guide nói:
Preincrement và Preecrement
Sử dụng mẫu tiền tố (++ i) của các toán tử tăng và giảm với các trình vòng lặp và các đối tượng mẫu khác.
Định nghĩa: Khi một biến được tăng (++ i hoặc i ++) hoặc giảm (--i hoặc i--) và giá trị của biểu thức không được sử dụng, người ta phải quyết định xem có phải là prerement (decrement) hay postincrement (decrement) hay không.
Ưu điểm: Khi giá trị trả về bị bỏ qua, biểu mẫu "trước" (++ i) không bao giờ kém hiệu quả hơn biểu mẫu "bài" (i ++) và thường hiệu quả hơn. Điều này là do tăng sau (hoặc giảm) yêu cầu một bản sao của i được thực hiện, đó là giá trị của biểu thức. Nếu tôi là một iterator hoặc loại không vô hướng khác, sao chép tôi có thể tốn kém. Vì hai loại gia tăng hoạt động giống nhau khi giá trị bị bỏ qua, tại sao không luôn luôn tăng trước?
Nhược điểm: Truyền thống được phát triển, trong C, sử dụng tăng sau khi giá trị biểu thức không được sử dụng, đặc biệt là trong các vòng lặp. Một số tìm thấy tăng sau dễ đọc hơn, vì "chủ đề" (i) đi trước "động từ" (++), giống như trong tiếng Anh.
Quyết định: Đối với các giá trị vô hướng đơn giản (không phải đối tượng), không có lý do nào để thích một dạng và chúng tôi cho phép một trong hai. Đối với các trình vòng lặp và các loại mẫu khác, hãy sử dụng mức tăng trước.
Tôi muốn chỉ ra một bài viết xuất sắc của Andrew Koenig trên Code Talk rất gần đây.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Effic-versus-intent.html&Itemid=29
Tại công ty chúng tôi cũng sử dụng quy ước của ++ iter để có tính nhất quán và hiệu suất khi áp dụng. Nhưng Andrew nêu lên chi tiết quá mức liên quan đến ý định so với hiệu suất. Có những lúc chúng ta muốn sử dụng iter ++ thay vì ++ iter.
Vì vậy, trước tiên hãy quyết định ý định của bạn và nếu trước hoặc sau không quan trọng thì hãy đi trước vì nó sẽ có một số lợi ích về hiệu suất bằng cách tránh tạo thêm đối tượng và ném nó.
@Ketan
... làm tăng chi tiết quá mức liên quan đến ý định và hiệu suất. Có những lúc chúng ta muốn sử dụng iter ++ thay vì ++ iter.
Rõ ràng bài đăng và tiền gia tăng có ngữ nghĩa khác nhau và tôi chắc chắn rằng mọi người đều đồng ý rằng khi kết quả được sử dụng, bạn nên sử dụng toán tử thích hợp. Tôi nghĩ câu hỏi là người ta nên làm gì khi kết quả bị loại bỏ (như trong for
các vòng lặp). Câu trả lời cho câu hỏi này (IMHO) là, vì các cân nhắc về hiệu suất là không đáng kể, bạn nên làm những gì tự nhiên hơn. Đối với bản thân tôi ++i
thì tự nhiên hơn nhưng kinh nghiệm của tôi cho tôi biết rằng tôi thuộc thiểu số và việc sử dụng i++
sẽ gây ra ít chi phí kim loại hơn cho hầu hết mọi người đọc mã của bạn.
Sau tất cả, đó là lý do ngôn ngữ không được gọi là " ++C
". [*]
[*] Chèn thảo luận bắt buộc về việc ++C
trở thành một tên hợp lý hơn.
Khi không sử dụng giá trị trả về, trình biên dịch được đảm bảo không sử dụng tạm thời trong trường hợp ++ i . Không được bảo đảm là nhanh hơn, nhưng đảm bảo không bị chậm hơn.
Khi sử dụng giá trị trả về, i ++ cho phép bộ xử lý đẩy cả phần tăng và phần bên trái vào đường ống do chúng không phụ thuộc vào nhau. ++ tôi có thể trì hoãn đường ống vì bộ xử lý không thể khởi động phía bên trái cho đến khi hoạt động tăng trước đã uốn khúc suốt. Một lần nữa, một gian hàng đường ống không được đảm bảo, vì bộ xử lý có thể tìm thấy những thứ hữu ích khác để gắn vào.
Mark: Chỉ muốn chỉ ra rằng các toán tử ++ là ứng cử viên tốt để được nội tuyến và nếu trình biên dịch chọn làm như vậy, bản sao dự phòng sẽ bị loại bỏ trong hầu hết các trường hợp. (ví dụ: các loại POD, mà các trình vòng lặp thường là.)
Điều đó nói rằng, vẫn còn phong cách tốt hơn để sử dụng ++ iter trong hầu hết các trường hợp. :-)
Sự khác biệt hiệu năng giữa ++i
và i++
sẽ rõ ràng hơn khi bạn nghĩ về các toán tử như các hàm trả về giá trị và cách chúng được thực hiện. Để dễ hiểu hơn những gì đang xảy ra, các ví dụ mã sau sẽ sử dụng int
như thể nó là một struct
.
++i
tăng biến, sau đó trả về kết quả. Điều này có thể được thực hiện tại chỗ và với thời gian CPU tối thiểu, chỉ cần một dòng mã trong nhiều trường hợp:
int& int::operator++() {
return *this += 1;
}
Nhưng điều tương tự không thể được nói về i++
.
Tăng sau ,, i++
thường được xem là trả về giá trị ban đầu trước khi tăng. Tuy nhiên, một chức năng chỉ có thể trả về một kết quả khi nó được hoàn thành . Kết quả là, cần phải tạo một bản sao của biến chứa giá trị ban đầu, tăng biến, sau đó trả về bản sao giữ giá trị ban đầu:
int int::operator++(int& _Val) {
int _Original = _Val;
_Val += 1;
return _Original;
}
Khi không có sự khác biệt về chức năng giữa tăng trước và tăng sau, trình biên dịch có thể thực hiện tối ưu hóa sao cho không có sự khác biệt về hiệu năng giữa hai. Tuy nhiên, nếu một loại dữ liệu tổng hợp như một struct
hoặc class
có liên quan, thì hàm tạo sao chép sẽ được gọi sau tăng dần và sẽ không thể thực hiện tối ưu hóa này nếu cần một bản sao sâu. Như vậy, tăng trước thường nhanh hơn và cần ít bộ nhớ hơn tăng sau.
@Mark: Tôi đã xóa câu trả lời trước đó của mình vì nó hơi lộn xộn và xứng đáng là một downvote cho điều đó một mình. Tôi thực sự nghĩ rằng đó là một câu hỏi hay theo nghĩa là nó hỏi những gì trong tâm trí của rất nhiều người.
Câu trả lời thông thường là ++ i nhanh hơn i ++, và không còn nghi ngờ gì nữa, nhưng câu hỏi lớn hơn là "khi nào bạn nên quan tâm?"
Nếu tỷ lệ thời gian CPU dành cho việc tăng vòng lặp nhỏ hơn 10%, thì bạn có thể không quan tâm.
Nếu tỷ lệ thời gian CPU dành cho việc tăng các vòng lặp lớn hơn 10%, bạn có thể xem các câu lệnh nào đang thực hiện việc lặp đó. Xem nếu bạn chỉ có thể tăng số nguyên thay vì sử dụng các trình vòng lặp. Rất có thể là bạn có thể, và trong khi nó có thể ở một khía cạnh nào đó ít mong muốn hơn, rất có thể bạn sẽ tiết kiệm được tất cả thời gian dành cho những người lặp đó.
Tôi đã thấy một ví dụ trong đó việc tăng vòng lặp tiêu thụ tốt hơn 90% thời gian. Trong trường hợp đó, việc tăng số nguyên thực hiện giảm thời gian thực hiện bằng cách đó. (tức là tốt hơn tốc độ tăng gấp 10 lần)
@wilmustell
Trình biên dịch có thể bỏ qua tạm thời. Nguyên văn từ các chủ đề khác:
Trình biên dịch C ++ được phép loại bỏ tạm thời dựa trên ngăn xếp ngay cả khi làm như vậy thay đổi hành vi chương trình. Liên kết MSDN cho VC 8:
http://msdn.microsoft.com/en-us/l Library / ms364057 (VS.80) .aspx
Một lý do tại sao bạn nên sử dụng ++ i ngay cả trên các loại tích hợp trong đó không có lợi thế về hiệu suất là để tạo thói quen tốt cho chính bạn.
Cả hai đều nhanh như vậy;) Nếu bạn muốn nó là phép tính giống nhau cho bộ xử lý, thì đó chỉ là thứ tự thực hiện khác nhau.
Ví dụ: đoạn mã sau:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Sản xuất lắp ráp sau:
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
Bạn thấy rằng đối với một ++ và b ++, đó là một bản ghi nhớ bao gồm, vì vậy nó hoạt động tương tự;)
Câu hỏi dự định là về khi kết quả không được sử dụng (điều đó rõ ràng từ câu hỏi cho C). Ai đó có thể khắc phục điều này vì câu hỏi là "wiki cộng đồng" không?
Về tối ưu hóa sớm, Knuth thường được trích dẫn. Đúng rồi. nhưng Donald Knuth sẽ không bao giờ bảo vệ với mật mã khủng khiếp mà bạn có thể thấy trong những ngày này. Bạn đã bao giờ thấy a = b + c trong số các số nguyên Java (không phải int) chưa? Đó là số tiền cho 3 chuyển đổi đấm bốc / unboxing. Tránh những thứ như thế là quan trọng. Và vô dụng viết i ++ thay vì ++ tôi cũng mắc lỗi tương tự. EDIT: Khi phresnel đưa nó vào một bình luận, điều này có thể được tóm tắt là "tối ưu hóa sớm là xấu, cũng như bi quan sớm".
Ngay cả việc mọi người quen với i ++ hơn là một di sản C đáng tiếc, gây ra bởi một sai lầm về khái niệm của K & R (nếu bạn tuân theo lập luận về ý định, đó là một kết luận hợp lý và bảo vệ K & R vì họ là K & R là vô nghĩa, họ là tuyệt vời, nhưng chúng không tuyệt vời như các nhà thiết kế ngôn ngữ, vô số lỗi trong thiết kế C tồn tại, từ get () đến strcpy (), đến API strncpy () (cần có API strlcpy () kể từ ngày 1) ).
Btw, tôi là một trong những người không sử dụng đủ C ++ để tìm ++ tôi khó chịu khi đọc. Tuy nhiên, tôi sử dụng điều đó vì tôi thừa nhận rằng nó đúng.
++i
khó chịu hơn i++
(thực tế, tôi thấy nó mát hơn), nhưng phần còn lại của bài viết của bạn nhận được sự thừa nhận đầy đủ của tôi. Có thể thêm một điểm "tối ưu hóa sớm là xấu xa, cũng như bi quan sớm"
strncpy
phục vụ một mục đích trong các hệ thống tập tin mà họ đang sử dụng tại thời điểm đó; tên tệp là bộ đệm 8 ký tự và nó không phải kết thúc bằng null. Bạn không thể đổ lỗi cho họ vì đã không nhìn thấy 40 năm trong tương lai của sự tiến hóa ngôn ngữ.
strlcpy()
được chứng minh bằng thực tế là nó chưa được phát minh.
Đã đến lúc cung cấp cho mọi người những viên ngọc khôn ngoan;) - có một mẹo đơn giản để làm cho việc tăng tiền tố C ++ hoạt động khá giống với việc tăng tiền tố (Bản thân tôi đã phát hiện ra nó, nhưng tôi cũng thấy nó trong mã người khác, vì vậy tôi không một mình).
Về cơ bản, mẹo là sử dụng lớp người trợ giúp để hoãn tăng dần sau khi trở về và RAII đến để giải cứu
#include <iostream>
class Data {
private: class DataIncrementer {
private: Data& _dref;
public: DataIncrementer(Data& d) : _dref(d) {}
public: ~DataIncrementer() {
++_dref;
}
};
private: int _data;
public: Data() : _data{0} {}
public: Data(int d) : _data{d} {}
public: Data(const Data& d) : _data{ d._data } {}
public: Data& operator=(const Data& d) {
_data = d._data;
return *this;
}
public: ~Data() {}
public: Data& operator++() { // prefix
++_data;
return *this;
}
public: Data operator++(int) { // postfix
DataIncrementer t(*this);
return *this;
}
public: operator int() {
return _data;
}
};
int
main() {
Data d(1);
std::cout << d << '\n';
std::cout << ++d << '\n';
std::cout << d++ << '\n';
std::cout << d << '\n';
return 0;
}
Phát minh là cho một số mã lặp tùy chỉnh nặng, và nó cắt giảm thời gian chạy. Chi phí tiền tố so với tiền tố là một tham chiếu bây giờ và nếu đây là toán tử tùy chỉnh di chuyển nặng, tiền tố và hậu tố mang lại cùng thời gian chạy cho tôi.
++i
nhanh hơn i++
bởi vì nó không trả về một bản sao cũ của giá trị.
Nó cũng trực quan hơn:
x = i++; // x contains the old value of i
y = ++i; // y contains the new value of i
Ví dụ C này in "02" thay vì "12" mà bạn có thể mong đợi:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}