Chức năng nhập dữ liệu nhanh (fast I/O) là một kỹ thuật phổ biến trong lập trình thi đấu nhằm giảm độ trễ khi đọc đầu vào. Một triển khai tiêu biểu như sau:
inline int fast_int_read() {
char c;
int result = 0;
while ((c = getchar()) < 33); // Bỏ qua ký tự trắng
do {
result = result * 10 + (c - '0');
c = getchar();
} while (c >= '0' && c <= '9');
return result;
}
Phiên bản này đã áp dụng nhiều kỹ thuật: khai báo inline, so sánh trực tiếp với giá trị ASCII thay vì dùng isspace(), và sử dụng toán tử gán kết hợp để nén biểu thức tính toán.
Để đánh giá mức độ tối ưu thực tế, ta phân tích mã máy sinh ra bởi trình biên dịch — phương pháp khách quan hơn việc suy luận dựa trên kinh nghiệm hay lời đồn.
Điều kiện thực nghiệm
- Trình biên dịch: MinGW-w64 g++ phiên bản 8.1.0
- Chuẩn C++: C++11
- Lệnh sinh mã lắp ráp:
g++ -S -O2 input_test.cpp -o input_test.s
Kiểm chứng các giả thuyết phổ biến
1. Toán tử logic ngắn mạch (&&) so với toán tử bit (&)
Một số lập trình viên cho rằng thay thế && bằng & trong điều kiện kiểm tra sẽ tăng tốc do tránh nhảy có điều kiện. Tuy nhiên, với đoạn mã kiểm tra:
// Dùng &&
while (c >= '0' && c <= '9')
// Dùng &
while ((c >= '0') & (c <= '9'))
Kết quả mã lắp ráp tương ứng lần lượt là 73 dòng và 76 dòng. Việc loại bỏ cơ chế ngắn mạch không giúp giảm kích thước hay số lệnh — ngược lại, nó tạo thêm phép tính logic và ràng buộc thứ tự đánh giá, làm tăng độ dài mã.
Các cặp khác cũng được kiểm tra:
||và|: sinh ra mã lắp ráp hoàn toàn giống nhau (31 dòng)!và^ 1:!sinh mã ngắn hơn rõ rệt (17 dòng vs 24 dòng), do trình biên dịch nhận diện mẫu chuẩn và thay thế bằng lệnhtest+sete
2. Hiệu lực của từ khóa inline
Khi loại bỏ inline và biên dịch ở mức tối ưu -O2, hàm fast_int_read vẫn được chèn nội tuyến tự động. Mã lắp ráp thu được chỉ còn 70 dòng — ít hơn 3 dòng so với phiên bản có inline.
Điều này cho thấy inline không phải lúc nào cũng cải thiện hiệu năng: trình biên dịch hiện đại thường đưa ra quyết định tốt hơn con người dựa trên chi phí gọi hàm, kích thước thân hàm và ngữ cảnh sử dụng.
3. Biến thể biểu thức tính toán chữ số
Biểu thức gốc result = result * 10 + (c - '0') sinh ra 73 dòng mã lắp ráp. Khi tách thành hai câu lệnh riêng biệt:
result *= 10;
result += c - '0';
Mã lắp ráp giảm xuống 69 dòng. Nguyên nhân nằm ở khả năng tái sử dụng thanh ghi và lược bỏ phép cộng trung gian — trình biên dịch dễ dàng nhận diện và tối ưu từng bước gán đơn giản hơn là một biểu thức phức tạp.
Tuy nhiên, việc tiếp tục tách result += c - '0' thành result = result + c - '0' không mang lại thay đổi nào — mã lắp ráp giữ nguyên.
4. Tiền tố ++i so với hậu tố i++
Với vòng lặp đơn thuần:
for (int i = 1; i <= 100; i++) { }
Cả hai dạng đều sinh ra đúng 30 dòng mã lắp ráp — trình biên dịch loại bỏ hoàn toàn biến điều khiển nếu giá trị của nó không được sử dụng.
Nhưng khi giá trị trả về được lưu vào biến khác:
int j;
for (int i = 1; i <= 100; j = i++) { }
Mã lắp ráp dài 33 dòng. Thay bằng j = ++i giảm còn 32 dòng, do tránh tạo bản sao tạm của i trước khi tăng — đây là khác biệt duy nhất có ý nghĩa về mặt hiệu năng trong ngữ cảnh cụ thể.