Tại sao ++i không return i; luôn mà phải tạo thêm đối tượng, i+1 đã thành hình rồi còn gì nữa.
Khác biệt giữa i++ và ++i
Return giá trị của 1 số thì nó phải copy số đó ra một biến ở địa chỉ khác. Nó giống như việc truyền tham trị.
Vậy return *this;
thì sao Không phải return là cứ phải instantiate đâu, có phải immutable đâu mà cứ +1 (khác với op+) là phải tạo mới nhỉ.
Mã đó mình viết không phải mã C++ mà chỉ giúp hình dung trình tự làm việc mà thôi. Phải nhìn vào mã ASM mới hiểu cặn kẽ được. Chỉ một câu lệnh i++ hay ++i nhìn rất đơn giản nhưng bên trong CPU nó phải thực hiện nhiều việc hơn thế. Ở dưới đó không còn cái gì là *this, object, private…
Bạn cứ hiểu đơn giản là địa chỉ nơi lưu giá trị return của i và địa chỉ của i là 2 nơi khác nhau. Không thể không copy.
mời vô đây làm chi đây
++i trả về i đâu cần tạo ra biến temp làm gì. Viết thử cái hàm là thấy ngay thôi mà:
#include <iostream>
void f(int& i) { i += 2; }
int main()
{
int i = 1;
//f(i++); // <--- báo lỗi ngay vì i++ trả về giá trị tạm thời, ko thay đổi được nên ko truyền vào f dưới dạng tham trị được
std::cout << i << "\n";
f(++i); //vô tư
std::cout << i << "\n"; //in ra 4
}
Các bạn nghĩ rằng CPU có thể tăng một phát giá trị của i (nằm trên RAM) cái rụp ấy nhỉ ?
Để tăng i nó phải làm gì ? Nó sẽ phải copy giá trị của i vào thanh ghi đặc biệt. Sau đó dùng các bộ xử lý số học trong CPU để tăng giá trị trong thanh ghi đặc biệt này. Sau đó nó copy giá trị trong thanh ghi ngược trở lại i.
Các bạn thử giải thích cái gì xảy ra dưới CPU khi return i ??
Bạn có thể dùng vi kiến trúc CPU để chứng minh một cách cụ thể hơn không?
Mình dùng mã giả giải thích cách CPU thực hiện. Mã thật thì phải disasembly mới thấy.
int a = i++;
MOV W,i ; copy giá trị i vào thanh ghi đặc biệt W.
MOV a,W ; copy giá trị của W vào a
INC W ; tăng giá trị thanh ghi W.
MOV i,W ; copy giá trị thanh gi W vào i
int a = ++i;
MOV W,i ; copy giá trị i vào thanh ghi đặc biệt W.
INC W ; tăng giá trị thanh ghi W.
MOV a,W ; copy giá trị của W vào a
MOV i,W ; copy giá trị thanh gi W vào i
Bạn để ý sự khác nhau là dòng lệnh set giá trị cho a nằm trước hoặc sau quá trình tăng W.
Ở i++, lệnh set a nằm trước lệnh tăng W nên a mới có giá trị của i trước khi tăng. Ở ++ i thì ngược lại.
Tuy nhiên ngày mai mình disasm thì sẽ rõ ràng hơn.
https://godbolt.org/g/KUQJAn lên đây disasm luôn cho lẹ
code:
int a(int& i)
{
return i++;
}
int b(int& i)
{
return ++i;
}
compile ra:
a(int&):
mov eax, DWORD PTR [rdi]
lea edx, [rax+1]
mov DWORD PTR [rdi], edx
ret
b(int&):
mov eax, DWORD PTR [rdi]
add eax, 1
mov DWORD PTR [rdi], eax
ret
i++ phải xài eax và edx
++i chỉ xài eax
suy ra i++ cần thêm 1 biến temp phụ là edx, còn ++i ko cần
int a(int& i)
{
int a = i++;
return a;
}
int b(int& i)
{
int a = ++i;
return a;
}
code này nó cũng compile ra y hệt cái code trên
eax và edx là thanh ghi dùng chung (general register) nó nằm trong cấu tạo chip rồi không phải là biến nên tất nhiên không có cấp phát hay thu hồi. Nó tương đương với cái W trong mã giả của mình.
Một điều thấy rõ nữa là i++ và ++i đều sử dụng 4 lệnh ám. Nếu như thời gian thực hiện từng mã là khác nhau thì có thể i++ hoặc ++i sẽ nhanh hơn. Còn không thì thời gian thực thi i++ và ++i là bằng nhau.
thì đúng là built-in i++ và ++i tốc độ khác biệt ko có bao nhiêu mà. Nhưng i++ bị buộc phải dùng thêm 1 biến phụ, nếu overload post-increment thì sẽ “chậm” hơn pre-increment trả về thẳng i luôn
ví dụ
struct MyIter {
int* ptr;
MyIter& operator++() { ptr += 2; return *this; }
MyIter operator++(int) { MyIter temp = *this; ptr += 2; return temp; } //chậm hơn vì phải copy *this
};
mà cũng ko rõ compiler nó optimize được tới mức nào. Vì hoang tưởng nên mình luôn xài ++i khi phải lựa chọn giữa it++ và ++it nếu tác dụng y hệt nhau. ++it có thể ko lẹ hơn nhưng chắc chắn nó ko chậm hơn it++.
Cái i++ và ++i hồi xưa cái thời mình còn khao khát hiệu năng cũng đem ra test chán chê rồi. Cho thực hiện cả trăm triệu lần với i++ rồi ++i kết quả là ngang nhau.
Nếu thực sự có hơn nhau thì vênh 1 lệnh asm nhân với cả trăm triệu lần như thế nó phải vênh nhau dữ lắm chứ không thể ngang nhau được.
Cái đoạn disasm đều dùng 4 lệnh là chắc 69 % là nó bằng nhau rồi. Đây là lệnh đơn giản nên chắc là compiler tối ưu nhất rồi.
Chắc là test với primitive phải không, chứ dùng object là thấy rõ như ban ngày rồi. Object chừng 100.000 slot ++ phát là chênh ngay.
Bạn có thể nói cách test của bạn để mình test thử.
Hiện tại mình test nhiều cách thì kết quả ++I chậm hơn một chút.
#include <iostream>
#include <Windows.h>
using namespace std;
#define DATA_LENGHT 50000000
class Test {
public:
Test();
~Test();
int *data;
int val;
void Proc1();
void Proc2();
};
Test::Test() {
val = 0;
data = new int[DATA_LENGHT+1];
}
Test::~Test() {
delete data;
}
void Test::Proc1() {
for (int i = 0; i < DATA_LENGHT; i++){
val = data[i] ++;
}
}
void Test::Proc2() {
for (int i = 0; i < DATA_LENGHT; ++i) {
val = ++data[i];
}
}
void TestMode1() {
Test* OBJ1 = new Test();
Test* OBJ2 = new Test();
int LOOP = 100;
unsigned long *time1 = new unsigned long[LOOP];
unsigned long *time2 = new unsigned long[LOOP];
unsigned long start = 0;
unsigned long end = 0;
for (int i = 0; i < 4; i++) {
start = GetTickCount();
OBJ1->Proc1();
OBJ2->Proc1();
OBJ1->Proc2();
OBJ2->Proc2();
end = GetTickCount();
}
for (int i = 0; i < LOOP; i++) {
start = GetTickCount();
OBJ1->Proc1();
end = GetTickCount();
time1[i] = (end - start);
start = GetTickCount();
OBJ2->Proc2();
end = GetTickCount();
time2[i] = (end - start);
}
unsigned long totaltime1 = 0;
unsigned long totaltime2 = 0;
for (int i = 0; i < LOOP; i++) {
std::cout << "I++ =";
std::cout << time1[i];
std::cout << "\t++I =";
std::cout << time2[i] << endl;
totaltime1 += time1[i];
totaltime2 += time2[i];
}
std::cout << "\nTOTAL ";
std::cout << LOOP;
std::cout << " TIMES MODE 1------------------------------------------------ " << endl;
std::cout << "I++ :";
std::cout << totaltime1 << endl;
std::cout << "++I :";
std::cout << totaltime2 << endl;
system("pause");
}
void TestMode2() {
Test* OBJ1 = new Test();
Test* OBJ2 = new Test();
int LOOP = 100;
unsigned long *time1 = new unsigned long[LOOP];
unsigned long *time2 = new unsigned long[LOOP];
unsigned long start = 0;
unsigned long end = 0;
for (int i = 0; i < 4; i++) {
start = GetTickCount();
OBJ1->Proc1();
OBJ2->Proc1();
OBJ1->Proc2();
OBJ2->Proc2();
end = GetTickCount();
}
for (int i = 0; i < LOOP; i++) {
start = GetTickCount();
OBJ1->Proc2();
end = GetTickCount();
time1[i] = (end - start);
start = GetTickCount();
OBJ2->Proc1();
end = GetTickCount();
time2[i] = (end - start);
}
unsigned long totaltime1 = 0;
unsigned long totaltime2 = 0;
for (int i = 0; i < LOOP; i++) {
std::cout << "++I =";
std::cout << time1[i];
std::cout << "\tI++ =";
std::cout << time2[i] << endl;
totaltime1 += time1[i];
totaltime2 += time2[i];
}
std::cout << "\nTOTAL ";
std::cout << LOOP;
std::cout << " TIMES MODE 2: ------------------------------------------------ " << endl;
std::cout << "++I :";
std::cout << totaltime1 << endl;
std::cout << "I++ :";
std::cout << totaltime2 << endl;
system("pause");
}
int main() {
TestMode1();
TestMode2();
}
asm:
30: for (int i = 0; i < DATA_LENGHT; i++){
00F61001 BE 0C 00 00 00 mov esi,0Ch
31: val = data[i] ++;
00F61006 8B 11 mov edx,dword ptr [ecx]
00F61008 8B 44 32 F4 mov eax,dword ptr [edx+esi-0Ch]
00F6100C 89 41 04 mov dword ptr [ecx+4],eax
00F6100F FF 44 32 F4 inc dword ptr [edx+esi-0Ch]
00F61013 8B 11 mov edx,dword ptr [ecx]
00F61015 8B 44 32 F8 mov eax,dword ptr [edx+esi-8]
00F61019 89 41 04 mov dword ptr [ecx+4],eax
00F6101C FF 44 32 F8 inc dword ptr [edx+esi-8]
00F61020 8B 11 mov edx,dword ptr [ecx]
00F61022 8B 44 32 FC mov eax,dword ptr [edx+esi-4]
00F61026 89 41 04 mov dword ptr [ecx+4],eax
00F61029 FF 44 32 FC inc dword ptr [edx+esi-4]
00F6102D 8B 11 mov edx,dword ptr [ecx]
00F6102F 8B 04 32 mov eax,dword ptr [edx+esi]
00F61032 89 41 04 mov dword ptr [ecx+4],eax
00F61035 FF 04 32 inc dword ptr [edx+esi]
00F61038 8B 11 mov edx,dword ptr [ecx]
00F6103A 8B 44 32 04 mov eax,dword ptr [edx+esi+4]
00F6103E 89 41 04 mov dword ptr [ecx+4],eax
00F61041 FF 44 32 04 inc dword ptr [edx+esi+4]
00F61045 83 C6 14 add esi,14h
00F61048 81 FE 0C 84 D7 17 cmp esi,17D7840Ch
00F6104E 7C B6 jl Test::Proc1+6h (0F61006h)
00F61050 5E pop esi
32: }
33: }
36: for (int i = 0; i < DATA_LENGHT; ++i) {
00F61060 BA 0C 00 00 00 mov edx,0Ch
37: val = ++data[i];
00F61065 8B 01 mov eax,dword ptr [ecx]
00F61067 FF 44 02 F4 inc dword ptr [edx+eax-0Ch]
00F6106B 8B 44 02 F4 mov eax,dword ptr [edx+eax-0Ch]
00F6106F 89 41 04 mov dword ptr [ecx+4],eax
00F61072 8B 01 mov eax,dword ptr [ecx]
00F61074 FF 44 02 F8 inc dword ptr [edx+eax-8]
00F61078 8B 44 02 F8 mov eax,dword ptr [edx+eax-8]
00F6107C 89 41 04 mov dword ptr [ecx+4],eax
37: val = ++data[i];
00F6107F 8B 01 mov eax,dword ptr [ecx]
00F61081 FF 44 02 FC inc dword ptr [edx+eax-4]
00F61085 8B 44 02 FC mov eax,dword ptr [edx+eax-4]
00F61089 89 41 04 mov dword ptr [ecx+4],eax
00F6108C 8B 01 mov eax,dword ptr [ecx]
00F6108E FF 04 02 inc dword ptr [edx+eax]
00F61091 8B 04 02 mov eax,dword ptr [edx+eax]
00F61094 89 41 04 mov dword ptr [ecx+4],eax
00F61097 8B 01 mov eax,dword ptr [ecx]
00F61099 FF 44 02 04 inc dword ptr [edx+eax+4]
00F6109D 8B 44 02 04 mov eax,dword ptr [edx+eax+4]
00F610A1 83 C2 14 add edx,14h
00F610A4 89 41 04 mov dword ptr [ecx+4],eax
00F610A7 81 FA 0C 84 D7 17 cmp edx,17D7840Ch
00F610AD 7C B6 jl Test::Proc2+5h (0F61065h)
38: }
39: }
00F610AF C3 ret
Results:
Lần 2 là đảo thực hiện ++I trước I++ cho nên ++I vẫn chậm hơn
Mình chưa hiểu I là object là như thế nào.
Nếu vậy mình test trên C# thì I là object. Kết quả cũng vậy thôi.
Mà GetTickCount có sai số lên đến 8ms cho mỗi lần đo nên không thuyết phục lắm đâu
Code bạn đưa là -O0 chứ không phải -O2 (-O2 chỉ cần 4 câu asm) -> càng không thuyết phục.
Bởi vì các hàm gettime đều tương đối, nên mình với test 100 lần, mỗi lần lặp 50 triệu lần rồi mới lấy kết quả để giảm sai số.
Tuy nhiên cả 2 lần test đều cho kết quả ngang nhau.
Asm trên là đoạn asm cho cả một hàm xử lý dữ lieu bên trong 1 object nên nó mới nhiều mã asm như vậy.
Bản than I++ hay ++I cũng chỉ 4 asm lệnh thôi.
đo tốc độ xài std::chrono::stead_clock::now()
ấy, xài GetTickCount làm gì
mà thật khó tin cái proc1 và proc2 ko bị optimized thành 1 dòng code? 1 vòng for với i chạy từ 0 tới 1 giá trị define cho trước, val
chả làm gì trong 49.999999 triệu lần trước chỉ cần giá trị sau cùng, vậy mà ko optimize được?
à nó còn phải ++ thêm giá trị cho data, vậy chắc ko optimize được @_@ Chắc là do thời gian tương đối thôi =)
edit: chạy thử thấy đúng là val = data[i]++
nhanh hơn thật, nhưng khi để data[i]++
ko mà ko gán cho val
thì lại xấp xỉ như nhau, có gì uẩn khúc ở đây, oan ức quá =)
đổi thành val += ...
thì ++i nhanh hơn chút xíu Vậy cái vấn đề val = ở đây là sao
edit:
╭╴C:\Users\tri\Desktop
╰╼ a
TOTAL 100 TIMES MODE 2: ------------------------------------------------
++I :962060
I++ :973266
TOTAL 100 TIMES MODE 1------------------------------------------------
I++ :964262
++I :967017
chắc hên xui, 10 lần chạy cũng có 1 lần ++i nhanh hơn, nhưng hoàn toàn chậm hơn trên MSVC ko biết tại sao
Proc1 và proc2 thực hiện 2 cách khác nhau nên không thể optimize thành 1 là đúng rồi.
Việc gán val thực hiện mà không optimize là nó làm đúng, nhiều khi cần như vậy tùy yêu cầu thôi.
Thời gian có thể nhanh chậm hơn nhau chút xíu vì bản chat nó như vậy rồi (xung CPU lúc cao lúc thấp,CPU switch nhiều công việc mỗi lúc lại khác nhau…)
Nhưng kết quả same same nhau cùng với việc view asm thì chắc là cũng đủ để thấy việc nhanh chậm hơn nhau giữa ++I và I++ là không có cơ sở.
Ở trên bạn disasm đã thấy việc thực hiện đều mất 4 lệnh asm. Chỉ cần vênh nhau 1 lệnh asm thì thời gian sẽ vênh nhau 20-25% rồi chứ không lệch 1 chút như vậy.