Có sự khác biệt hiệu suất giữa i++
và ++i
nếu giá trị kết quả không được sử dụng?
Có sự khác biệt hiệu suất giữa i++
và ++i
nếu giá trị kết quả không được sử dụng?
Câu trả lời:
Tóm tắt điều hành: Không.
i++
có thể có khả năng chậm hơn ++i
, vì giá trị cũ i
có thể cần được lưu lại để sử dụng sau này, nhưng trong thực tế, tất cả các trình biên dịch hiện đại sẽ tối ưu hóa điều này.
Chúng ta có thể chứng minh điều này bằng cách xem mã cho hàm này, cả với ++i
và i++
.
$ cat i++.c
extern void g(int i);
void f()
{
int i;
for (i = 0; i < 100; i++)
g(i);
}
Các tệp giống nhau, ngoại trừ ++i
và i++
:
$ diff i++.c ++i.c
6c6
< for (i = 0; i < 100; i++)
---
> for (i = 0; i < 100; ++i)
Chúng tôi sẽ biên dịch chúng và cũng có được trình biên dịch được tạo:
$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c
Và chúng ta có thể thấy rằng cả các tệp đối tượng và trình biên dịch được tạo ra đều giống nhau.
$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e
$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22
++i
thay vì sử dụng tốt i++
. Hoàn toàn không có lý do gì để không và nếu phần mềm của bạn đi qua một chuỗi công cụ không tối ưu hóa nó thì phần mềm của bạn sẽ hiệu quả hơn. Xem xét nó chỉ dễ gõ ++i
như nó là để gõ i++
, thực sự không có lý do gì để không sử dụng ++i
ở nơi đầu tiên.
Từ hiệu quả so với ý định của Andrew Koenig:
Đầu tiên, rõ ràng
++i
là hiệu quả hơni++
, ít nhất là khi các biến số nguyên có liên quan.
Và:
Vì vậy, câu hỏi người ta nên đặt ra không phải là hoạt động nào trong hai thao tác này nhanh hơn, mà là hoạt động nào trong hai thao tác này thể hiện chính xác hơn những gì bạn đang cố gắng thực hiện. Tôi gửi rằng nếu bạn không sử dụng giá trị của biểu thức, không bao giờ có lý do để sử dụng
i++
thay vì++i
, vì không bao giờ có lý do để sao chép giá trị của biến, tăng biến và sau đó ném bản sao đi.
Vì vậy, nếu giá trị kết quả không được sử dụng, tôi sẽ sử dụng ++i
. Nhưng không phải vì nó hiệu quả hơn: bởi vì nó nói chính xác ý định của tôi.
i++
cùng một cách tôi muốn đang i += n
hay i = i + n
, ví dụ, dưới dạng mục tiêu động từ đối tượng , với mục tiêu toán hạng bên trái của động từ điều hành. Trong trường hợp i++
, không có đối tượng bên phải , nhưng quy tắc vẫn được áp dụng, giữ mục tiêu ở bên trái của toán tử động từ .
Một câu trả lời tốt hơn là ++i
đôi khi sẽ nhanh hơn nhưng không bao giờ chậm hơn.
Mọi người dường như đang cho rằng đó i
là một loại tích hợp thông thường như int
. Trong trường hợp này sẽ không có sự khác biệt có thể đo lường được.
Tuy nhiên nếu i
là loại phức tạp thì bạn cũng có thể tìm thấy một sự khác biệt có thể đo lường được. Đối với i++
bạn phải tạo một bản sao của lớp học của bạn trước khi tăng nó. Tùy thuộc vào những gì liên quan đến một bản sao, nó thực sự có thể chậm hơn vì với ++it
bạn chỉ có thể trả về giá trị cuối cùng.
Foo Foo::operator++()
{
Foo oldFoo = *this; // copy existing value - could be slow
// yadda yadda, do increment
return oldFoo;
}
Một sự khác biệt nữa là với ++i
bạn có tùy chọn trả về một tham chiếu thay vì một giá trị. Một lần nữa, tùy thuộc vào những gì liên quan đến việc tạo một bản sao của đối tượng của bạn, điều này có thể chậm hơn.
Một ví dụ trong thế giới thực về nơi điều này có thể xảy ra sẽ là việc sử dụng các trình vòng lặp. Sao chép một trình vòng lặp có thể không phải là một cổ chai trong ứng dụng của bạn, nhưng vẫn nên thực hành thói quen sử dụng ++i
thay vì i++
nơi mà kết quả không bị ảnh hưởng.
Câu trả lời ngắn:
Không bao giờ có bất kỳ sự khác biệt giữa i++
và ++i
về tốc độ. Một trình biên dịch tốt không nên tạo mã khác nhau trong hai trường hợp.
Câu trả lời dài:
Điều mà mọi câu trả lời khác không đề cập đến là sự khác biệt giữa ++i
so với i++
chỉ có ý nghĩa trong biểu thức mà nó được tìm thấy.
Trong trường hợp for(i=0; i<n; i++)
, i++
một mình trong biểu thức riêng của nó: có một điểm thứ tự trước i++
và có một điểm sau nó. Do đó, mã máy duy nhất được tạo là "tăng i
theo 1
" và được xác định rõ cách thức điều này được giải trình tự liên quan đến phần còn lại của chương trình. Vì vậy, nếu bạn muốn thay đổi nó thành tiền tố ++
, nó sẽ không quan trọng dù chỉ một chút, bạn sẽ vẫn chỉ lấy mã máy "tăng i
bởi 1
".
Sự khác biệt giữa ++i
và i++
chỉ quan trọng trong các biểu thức như array[i++] = x;
so với array[++i] = x;
. Một số người có thể lập luận và nói rằng hậu tố sẽ chậm hơn trong các hoạt động như vậy bởi vì thanh ghi nơi i
cư trú phải được tải lại sau đó. Nhưng sau đó lưu ý rằng trình biên dịch có thể tự do đặt hàng các hướng dẫn của bạn theo bất kỳ cách nào nó thích, miễn là nó không "phá vỡ hành vi của máy trừu tượng" như tiêu chuẩn C gọi nó.
Vì vậy, trong khi bạn có thể cho rằng array[i++] = x;
được dịch sang mã máy là:
i
trong thanh ghi A.i
trong thanh ghi A // không hiệu quả vì hướng dẫn thêm ở đây, chúng tôi đã thực hiện điều này một lần.i
.trình biên dịch cũng có thể tạo mã hiệu quả hơn, chẳng hạn như:
i
trong thanh ghi A.i
.Chỉ vì bạn là một lập trình viên C được đào tạo để nghĩ rằng hậu tố ++
xảy ra ở cuối, mã máy không phải được đặt hàng theo cách đó.
Vì vậy, không có sự khác biệt giữa tiền tố và hậu tố ++
trong C. Bây giờ, những gì bạn với tư cách là một lập trình viên C nên khác nhau, là những người không nhất quán sử dụng tiền tố trong một số trường hợp và hậu tố trong các trường hợp khác, mà không có lý do tại sao. Điều này cho thấy rằng họ không chắc chắn về cách C hoạt động hoặc họ có kiến thức không chính xác về ngôn ngữ. Đây luôn là một dấu hiệu xấu, đến lượt nó cho thấy rằng họ đang đưa ra các quyết định đáng ngờ khác trong chương trình của họ, dựa trên sự mê tín hoặc "giáo điều tôn giáo".
"Tiền tố ++
luôn luôn nhanh hơn" thực sự là một giáo điều sai lầm phổ biến như vậy đối với các lập trình viên C sẽ là.
Lấy một lá từ Scott Meyers, Hiệu quả hơn c ++ Mục 6: Phân biệt giữa các hình thức tiền tố và hậu tố của các hoạt động tăng và giảm .
Phiên bản tiền tố luôn được ưu tiên hơn tiền tố liên quan đến các đối tượng, đặc biệt là liên quan đến các trình vòng lặp.
Lý do cho điều này nếu bạn nhìn vào mẫu cuộc gọi của các nhà khai thác.
// Prefix
Integer& Integer::operator++()
{
*this += 1;
return *this;
}
// Postfix
const Integer Integer::operator++(int)
{
Integer oldValue = *this;
++(*this);
return oldValue;
}
Nhìn vào ví dụ này, thật dễ dàng để thấy toán tử tiền tố sẽ luôn hiệu quả hơn so với hậu tố. Do sự cần thiết của một đối tượng tạm thời trong việc sử dụng hậu tố.
Đây là lý do tại sao khi bạn thấy các ví dụ sử dụng các trình vòng lặp, chúng luôn sử dụng phiên bản tiền tố.
Nhưng như bạn chỉ ra cho int thì thực sự không có gì khác biệt vì tối ưu hóa trình biên dịch có thể diễn ra.
Đây là một quan sát bổ sung nếu bạn lo lắng về tối ưu hóa vi mô. Các vòng lặp giảm dần có thể 'có thể' hiệu quả hơn các vòng lặp tăng dần (tùy thuộc vào kiến trúc tập lệnh, ví dụ ARM), được đưa ra:
for (i = 0; i < 100; i++)
Trên mỗi vòng lặp, bạn sẽ có một hướng dẫn cho:
1
vào i
. i
có nhỏ hơn a không 100
.i
nhỏ hơn a 100
.Trong khi đó một vòng lặp giảm dần:
for (i = 100; i != 0; i--)
Vòng lặp sẽ có một hướng dẫn cho mỗi:
i
, thiết lập cờ trạng thái thanh ghi CPU.Z==0
).Tất nhiên điều này chỉ hoạt động khi giảm xuống không!
Ghi nhớ từ Hướng dẫn dành cho nhà phát triển hệ thống ARM.
Xin đừng để câu hỏi "cái nào nhanh hơn" là yếu tố quyết định nên sử dụng cái nào. Có thể bạn sẽ không bao giờ quan tâm đến điều đó, và bên cạnh đó, thời gian đọc của lập trình viên đắt hơn nhiều so với thời gian của máy.
Sử dụng bất cứ điều gì có ý nghĩa nhất đối với con người đọc mã.
Trước hết: Sự khác biệt giữa i++
và không thể phủ nhận ++i
ở C.
Để các chi tiết.
++i
nhanh hơnTrong C ++, ++i
iff hiệu quả hơn i
là một loại đối tượng có toán tử gia tăng quá tải.
Tại sao?
Trong ++i
, đối tượng được tăng đầu tiên và sau đó có thể được chuyển dưới dạng tham chiếu const cho bất kỳ chức năng nào khác. Điều này là không thể nếu biểu thức là foo(i++)
bởi vì bây giờ việc tăng cần phải được thực hiện trước đó foo()
được gọi, nhưng giá trị cũ cần được truyền vào foo()
. Do đó, trình biên dịch buộc phải tạo một bản sao i
trước khi nó thực thi toán tử gia tăng trên bản gốc. Các lệnh gọi hàm tạo / hàm hủy bổ sung là phần xấu.
Như đã lưu ý ở trên, điều này không áp dụng cho các loại cơ bản.
i++
có thể nhanh hơnNếu không có hàm tạo / hàm hủy nào cần được gọi, đó luôn là trường hợp trong C, ++i
và i++
phải nhanh như nhau, phải không? Không. Chúng hầu như nhanh như nhau, nhưng có thể có những khác biệt nhỏ, mà hầu hết những người trả lời khác đã hiểu sai.
Làm thế nào có i++
thể nhanh hơn?
Điểm quan trọng là phụ thuộc dữ liệu. Nếu giá trị cần được tải từ bộ nhớ, hai thao tác tiếp theo cần được thực hiện với nó, tăng nó và sử dụng nó. Với ++i
, việc tăng cần phải được thực hiện trước khi giá trị có thể được sử dụng. Với i++
, việc sử dụng không phụ thuộc vào mức tăng và CPU có thể thực hiện thao tác sử dụng song song với thao tác tăng. Sự khác biệt là nhiều nhất là một chu kỳ CPU, vì vậy nó thực sự không thể tin được, nhưng nó ở đó. Và đó là cách khác để nhiều người mong đợi.
++i
hoặc i++
được sử dụng trong một biểu thức khác, việc thay đổi giữa chúng sẽ thay đổi ngữ nghĩa của biểu thức, do đó, bất kỳ khả năng tăng / giảm hiệu suất nào có thể xảy ra. Nếu chúng là độc lập, nghĩa là, kết quả của hoạt động không được sử dụng ngay lập tức, thì bất kỳ trình biên dịch tử tế nào cũng sẽ biên dịch nó thành cùng một thứ, ví dụ như một INC
lệnh lắp ráp.
i++
và ++i
có thể được sử dụng thay thế cho nhau trong hầu hết mọi tình huống có thể bằng cách điều chỉnh các hằng số vòng lặp bởi một, vì vậy chúng gần tương đương với những gì chúng làm cho lập trình viên. 2) Mặc dù cả hai đều biên dịch theo cùng một lệnh, việc thực thi chúng khác nhau đối với CPU. Trong trường hợp i++
, CPU có thể tính toán mức tăng song song với một số lệnh khác sử dụng cùng một giá trị (CPU thực sự làm điều này!), Trong khi với ++i
CPU phải lên lịch cho lệnh khác sau khi tăng.
if(++foo == 7) bar();
và if(foo++ == 6) bar();
tương đương về chức năng. Tuy nhiên, lần thứ hai có thể nhanh hơn một chu kỳ, bởi vì sự so sánh và gia tăng có thể được tính toán song song bởi CPU. Không phải là chu kỳ duy nhất này quan trọng nhiều, nhưng sự khác biệt là có.
<
ví dụ vs <=
) ++
thường được sử dụng, do đó, việc chuyển đổi giữa mặc dù thường dễ dàng có thể.
@Mark Mặc dù trình biên dịch được phép tối ưu hóa bản sao tạm thời (dựa trên ngăn xếp) của biến và gcc (trong các phiên bản gần đây) đang làm như vậy, không có nghĩa là tất cả các trình biên dịch sẽ luôn làm như vậy.
Tôi vừa thử nghiệm nó với các trình biên dịch mà chúng tôi sử dụng trong dự án hiện tại của chúng tôi và 3 trong số 4 không tối ưu hóa nó.
Không bao giờ giả sử trình biên dịch làm cho đúng, đặc biệt là nếu mã có thể nhanh hơn, nhưng không bao giờ chậm hơn là dễ đọc.
Nếu bạn không có triển khai thực sự ngu ngốc đối với một trong các toán tử trong mã của mình:
Alwas thích ++ i hơn i ++.
Trong C, trình biên dịch nói chung có thể tối ưu hóa chúng giống nhau nếu kết quả không được sử dụng.
Tuy nhiên, trong C ++ nếu sử dụng các loại khác cung cấp toán tử ++ của riêng họ, phiên bản tiền tố có thể sẽ nhanh hơn phiên bản hậu tố. Vì vậy, nếu bạn không cần ngữ nghĩa hậu tố, tốt hơn là sử dụng toán tử tiền tố.
Tôi có thể nghĩ về một tình huống mà postfix chậm hơn so với gia tăng tiền tố:
Hãy tưởng tượng một bộ xử lý với thanh ghi A
được sử dụng làm bộ tích lũy và đó là thanh ghi duy nhất được sử dụng trong nhiều hướng dẫn (một số bộ vi điều khiển nhỏ thực sự như thế này).
Bây giờ hãy tưởng tượng chương trình sau đây và bản dịch của chúng thành một hội nghị giả định:
Gia tăng tiền tố:
a = ++b + c;
; increment b
LD A, [&b]
INC A
ST A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
Gia tăng hậu tố:
a = b++ + c;
; load b
LD A, [&b]
; add with c
ADD A, [&c]
; store in a
ST A, [&a]
; increment b
LD A, [&b]
INC A
ST A, [&b]
Lưu ý cách giá trị của b
bị buộc phải tải lại. Với gia tăng tiền tố, trình biên dịch chỉ có thể tăng giá trị và tiếp tục sử dụng nó, có thể tránh tải lại vì giá trị mong muốn đã có trong thanh ghi sau khi tăng. Tuy nhiên, với gia tăng postfix, trình biên dịch phải xử lý hai giá trị, một giá trị cũ và một giá trị tăng dần mà tôi hiển thị ở trên dẫn đến kết quả là có thêm một lần truy cập bộ nhớ.
Tất nhiên, nếu giá trị của gia số không được sử dụng, chẳng hạn như một i++;
câu lệnh đơn , trình biên dịch có thể (và không) chỉ đơn giản tạo ra một lệnh tăng bất kể sử dụng tiền tố hay tiền tố.
Là một lưu ý phụ, tôi muốn đề cập rằng một biểu thức trong đó b++
không thể chuyển đổi thành biểu thức ++b
mà không cần bất kỳ nỗ lực bổ sung nào (ví dụ: bằng cách thêm a - 1
). Vì vậy, so sánh hai nếu chúng là một phần của một số biểu thức là không thực sự hợp lệ. Thông thường, nơi bạn sử dụng b++
bên trong một biểu thức bạn không thể sử dụng ++b
, do đó, ngay cả khi ++b
có khả năng hiệu quả hơn, điều đó chỉ đơn giản là sai. Tất nhiên ngoại lệ là nếu biểu thức đang cầu xin nó (ví dụ a = b++ + 1;
có thể thay đổi thành a = ++b;
).
Tôi đã đọc qua hầu hết các câu trả lời ở đây và nhiều ý kiến, và tôi không thấy bất kỳ tài liệu tham khảo nào về một trường hợp mà tôi có thể nghĩ về nơi nào i++
hiệu quả hơn ++i
(và có lẽ đáng ngạc nhiên --i
là hiệu quả hơn i--
). Đó là cho trình biên dịch C cho DEC PDP-11!
PDP-11 có các hướng dẫn lắp ráp để giảm trước đăng ký và tăng sau, nhưng không phải là cách khác. Các hướng dẫn cho phép bất kỳ đăng ký "mục đích chung" nào được sử dụng như một con trỏ ngăn xếp. Vì vậy, nếu bạn sử dụng một cái gì đó giống như *(i++)
nó có thể được biên dịch thành một hướng dẫn lắp ráp duy nhất, trong khi *(++i)
không thể.
Đây rõ ràng là một ví dụ rất bí truyền, nhưng nó cung cấp ngoại lệ khi tăng sau hiệu quả hơn (hoặc tôi nên nói là , vì không có nhiều nhu cầu về mã PDP-11 C ngày nay).
--i
và i++
.
Tôi luôn thích tăng trước, tuy nhiên ...
Tôi muốn chỉ ra rằng ngay cả trong trường hợp gọi hàm toán tử ++, trình biên dịch sẽ có thể tối ưu hóa tạm thời nếu hàm được nội tuyến. Vì toán tử ++ thường ngắn và thường được triển khai trong tiêu đề, nên nó có khả năng được nội tuyến.
Vì vậy, đối với các mục đích thực tế, có thể không có nhiều sự khác biệt giữa hiệu suất của hai hình thức. Tuy nhiên, tôi luôn thích tăng trước vì có vẻ tốt hơn để thể hiện trực tiếp những gì tôi đang cố gắng nói, thay vì dựa vào trình tối ưu hóa để tìm ra nó.
Ngoài ra, cung cấp cho trình tối ưu hóa ít hơn để thực hiện có nghĩa là trình biên dịch chạy nhanh hơn.
C của tôi là một chút gỉ, vì vậy tôi xin lỗi trước. Tốc độ, tôi có thể hiểu kết quả. Nhưng, tôi bối rối không biết làm thế nào cả hai tệp xuất hiện cùng một hàm băm MD5. Có thể một vòng lặp for chạy giống nhau, nhưng 2 dòng mã sau đây sẽ tạo ra sự lắp ráp khác nhau?
myArray[i++] = "hello";
đấu với
myArray[++i] = "hello";
Cái đầu tiên ghi giá trị vào mảng, sau đó tăng i. Gia số thứ hai sau đó tôi ghi vào mảng. Tôi không phải là chuyên gia lắp ráp, nhưng tôi không thấy cách thực thi giống nhau sẽ được tạo ra bởi 2 dòng mã khác nhau này.
Chỉ hai xu của tôi.
foo[i++]
thành foo[++i]
không thay đổi bất cứ điều gì khác rõ ràng sẽ thay đổi ngữ nghĩa chương trình, nhưng trên một số bộ xử lý khi sử dụng trình biên dịch không có logic tối ưu hóa vòng lặp, tăng p
và q
một lần rồi chạy một vòng lặp thực hiện, ví dụ như *(p++)=*(q++);
sẽ nhanh hơn sử dụng vòng lặp thực hiện *(++pp)=*(++q);
. Đối với các vòng lặp rất chặt chẽ trên một số bộ xử lý, chênh lệch tốc độ có thể là đáng kể (hơn 10%), nhưng đó có lẽ là trường hợp duy nhất trong C khi tăng sau nhanh hơn về mặt vật chất so với tăng trước.