A: Joe Zbiciak, lập trình từ hồi tiểu học
==========
*E hèm*
Đúng là bạn nên dùng ++i thay vì i++ trong C++ khi tăng biến một cách độc lập, vì cách dùng đầu tiên có thể có chút lợi thế về hiệu suất đấy.
Tuy vậy, chẳng có gì đảm bảo là sẽ nhanh hơn đâu. Thực tế thì, với những trình biên dịch cùng vi xử lý hiện đại, quá trình này sẽ chẳng đáng kể là bao đối với hầu hết các ngôn ngữ và kiểu dữ liệu. Điều đó nói lên rằng, có những trường hợp mà việc này sẽ tạo ra được sự khác biệt đấy.
Hãy bắt đầu từ đầu nào: ++i và i++ mang ý nghĩa khác nhau.
· ++i tăng giá trị biến i và trả về giá trị mới được tính toán.
· i++ tăng giá trị biến i và trả về giá trị trước đó.
Trên đây tôi chỉ nói hơi giản lược chút thôi. Toán tử ++ thực sự chỉ mang ý nghĩa là tăng khi dùng với những kiểu dữ liệu cơ bản mà thôi. Khi dùng với các object của các lớp, nó sẽ gọi ra một trong hai cách dùng operator++ đối với lớp đó. Về mặt ngôn ngữ, nó là một “phép tăng”, dù nó không thực sự cộng thêm một vào thứ gì đấy đâu.
Trong bất kỳ trường hợp nào, sự khác biệt sẽ xảy ra đối với giá trị trả về sau đó: Liệu đó là giá trị mới đã được tăng hay là giá trị cũ? Đối với i++, trình biên dịch (hoặc operator++) sẽ cần phải lưu lại một bản copy của giá trị ban đầu để trả về sau khi tăng biến đó lên. Bản copy đó có thể sẽ có một cái giá kha khá đấy.
Nếu bạn không sử dụng giá trị được trả về từ phép tăng đó, thì hai cách biểu diễn sẽ gần như là một, bởi chẳng có gì dùng giá trị trả về từ biểu thức đó cả. Trong phần lớn trường hợp, trình biên dịch có thể và sẽ xóa bản copy do i++ chỉ thị nếu bạn không cần tới nó. Tự ngó đi:
int i;
voidpreinc() { ++i; }
voidpostinc() { i++; }
Cả hai hàm này đều tạo ra các đoạn mã giống nhau:
preinc():
add DWORD PTR i[rip], 1
ret
postinc():
add DWORD PTR i[rip], 1
ret
Vì thế, đối với những kiểu dữ liệu cơ bản, nếu không dùng giá trị được trả về từ một biểu thức tăng, thì sẽ chẳng có khác biệt gì về hiệu suất liên quan tới việc tăng-trước (pre-increment) hay tăng-sau (post-increment).
Nhưng còn những loại object khác thì sao?
Các container và thuật toán của C++ đều phụ thuộc vào một khái niệm khá giống pointer có tên là iterator. Iterator cho phép bạn duyệt qua dữ liệu theo một cách có thứ tự. Đó là một phép trừu tượng hóa (abstraction) trên cả pointer.
Ví dụ, iterator cho phép bạn sử dụng các biểu thức như *i++ để truy cập vào một object trong một container và chuyển tới object tiếp theo. ++ ở đây không đơn giản là “cộng thêm 1” đâu. Trong std::map, nó cần phải duyệt qua một Rb-tree đó (Red-black tree).
Do vậy, với iterator, bạn có thể thấy sự khác biệt lớn giữa i++ và ++ivì những chi phí từ việc copy một iterator không phải nhỏ. Và, nếu copy-constructor (hàm tạo sao chép) đối với iterator có hiệu ứng lề (side effect), trình biên dịch có thể không tối ưu được nó nữa.
Ví dụ:
#include <atomic>
classclown {
static std::atomic_ullong allocations;
int value;
public:
clown() : value(0) { allocations++; }
clown(const clown& rhs) : value(rhs.value) {
allocations++;
}
clown& operator= (const clown& rhs) {
value = rhs.value;
allocations++;
}
// pre-increment:
clown& operator++() { value++; return *this; }
// post-increment:
clown& operator++(int) {
clown tmp = *this;
operator++(); // implement in terms of pre-increment
return tmp;
}
};
clown car;
voidpreinc() { ++car; }
voidpostinc() { car++; }
Lưu ý rằng post-increment được thực hiện giống như pre-incremeent, và thêm bước copy để lưu lại giá trị ban đầu. Đây là cách thực hiện post-increment thông thường.
Giờ thì preinc() và postinc() sẽ được viết khác nhau:
preinc():
add DWORD PTR car[rip], 1
ret
postinc():
lock add QWORD PTR _ZN5clown11allocationsE[rip], 1
add DWORD PTR car[rip], 1
ret
Để ý thấy lock add không? Đó là vì atomic increment trong copy constructor đấy. Copy constructor đó được gọi trong operator++(int) (post-increment đó), do ta cần tạo một bản copy của object để trả về sau khi tăng.
Tất nhiên, đây là một trường hợp được vẽ ra từ trước. Nhưng thực tế là: Thi thoảng, việc copy có thể rất tốn kém, và không phải lúc nào trình biên dịch cũng loại bỏ việc đó được. i++ sẽ ngầm tạo ra bản copy còn ++i thì không.
Trình biên dịch sẽ loại bỏ những đoạn dead code nó có thể, và vì thế trong phần lớn thời gian, bạn sẽ không cảm nhận được sự khác biệt giữa ++i và i++ đối với các phép tăng độc lập đâu. Dẫu vậy, khi bắt đầu làm việc với iterator hay các object khác, bạn sẽ cảm nhận được chút chênh lệch về hiệu suất đấy.
Theo: Vũ Cường
