Trong các kỳ thi lập trình, đề bài thường cho dữ liệu đầu vào lên tới vài trăm nghìn hoặc vài triệu số. Với lượng dữ liệu đó, riêng việc đọc vào đã có thể tốn hơn một giây nếu bạn dùng cách đọc mặc định, trong khi giới hạn thời gian của bài chỉ là 1 giây. Kết quả là bài bị báo quá thời gian dù thuật toán hoàn toàn đúng.
Bài viết này trả lời ba câu hỏi: vì sao cách nhập xuất mặc định chậm, chênh lệch thật sự là bao nhiêu (đo trên máy, có kèm chương trình đo để bạn tự chạy lại), và mẫu code nào nên thuộc lòng để dùng ngay mỗi khi bước vào phòng thi.
1. Vì sao nhập xuất mặc định chậm?
Trong C++, đối tượng cin và cout mặc định được đồng bộ với hệ thống nhập xuất kiểu C (scanf, printf). Điều này cho phép bạn dùng lẫn cin và scanf trong cùng chương trình mà dữ liệu không bị lộn thứ tự, nhưng cái giá phải trả là mỗi lần đọc phải đi qua thêm một lớp kiểm tra bộ đệm trung gian. Ngoài ra, cin mặc định bị buộc (tie) với cout: trước mỗi lần đọc dữ liệu, chương trình bắt buộc phải đẩy hết toàn bộ dữ liệu đang chờ trong bộ đệm của cout ra màn hình. Nếu bạn xen kẽ đọc và in hàng trăm nghìn lần trong các bài toán có nhiều truy vấn, chi phí này cộng dồn rất nhanh và trở thành nút thắt cổ chai lớn nhất.
Hai dòng sau đây tắt hoàn toàn cả hai cơ chế đồng bộ và liên kết bộ đệm trên:
ios::sync_with_stdio(false);
cin.tie(nullptr);
Sau khi thêm hai dòng này, tuyệt đối không được dùng lẫn cin/cout với scanf/printf hay puts trong cùng một chương trình, vì hai hệ thống giờ đây duy trì hai vùng bộ đệm độc lập và thứ tự đọc/ghi có thể bị xáo trộn ngoài ý muốn.
Một nguồn gây chậm phổ biến khác là std::endl. Mỗi lần bạn in endl, chương trình vừa in một ký tự xuống dòng '\n', vừa gọi lệnh đẩy bộ đệm (flush) ra luồng xuất của hệ điều hành. Nếu bạn in một triệu dòng bằng endl, đó là đúng một triệu lần thực hiện thao tác gọi hệ thống (System Call) tốn kém. Hãy thay thế bằng việc in trực tiếp ký tự '\n'.
Trong Python, hàm input() mặc định thực hiện rất nhiều công việc bổ sung không cần thiết cho việc thi thuật toán: nó hỗ trợ chuỗi lời nhắc, xử lý tín hiệu ngắt và tự động loại bỏ ký tự xuống dòng. Mỗi lần gọi hàm trong Python đều có chi phí thông dịch riêng. Khi bạn đọc một tệp chứa \(10^6\) dòng bằng vòng lặp for _ in range(N): x = int(input()), chi phí hàm lặp đi lặp lại khiến chương trình chạy mất hơn nửa giây. Cách nhanh nhất là đọc toàn bộ dữ liệu vào bộ nhớ một lần duy nhất dưới dạng byte bằng sys.stdin.buffer.read() rồi tách thành danh sách các chuỗi bằng hàm split().
2. Số đo thật: đọc 1 triệu số nguyên
Chương trình bench_io.py (nằm cùng thư mục với code của bài) tạo một tệp có \(10^6\) số nguyên ngẫu nhiên trong đoạn \([-10^9, 10^9]\), mỗi số một dòng, rồi chạy sáu cách đọc khác nhau để tính tổng. Mỗi cách chạy 3 lần và lấy lần nhanh nhất. Thời gian đo bao gồm cả thời gian khởi động chương trình.
Môi trường đo ngày 10/11/2026: Windows, g++ 16.1.0 (MSYS2) với cờ -O2 -std=c++17, Python 3.14.6.
| Ngôn ngữ | Cách đọc | Thời gian (giây) |
|---|---|---|
| C++ | cin mặc định, in bằng endl |
1,863 |
| C++ | scanf |
0,669 |
| C++ | cin sau khi tắt đồng bộ và cin.tie(nullptr) |
0,146 |
| Python | input() trong vòng lặp |
0,497 |
| Python | sys.stdin.readline trong vòng lặp |
0,211 |
| Python | sys.stdin.buffer.read().split() |
0,192 |
Ba điều rút ra từ bảng số liệu thực tế:
- Hai dòng tắt đồng bộ giúp
cinnhanh hơn khoảng 12 lần trong phép đo này (từ 1,863 giây giảm ngoạn mục xuống 0,146 giây), nhanh hơn gấp 4 lần so với hàmscanftruyền thống. - Trên máy đo này,
cinmặc định còn chậm hơn cảinput()của Python. Tỉ lệ này thay đổi tùy thuộc vào hệ điều hành, trình biên dịch và thư viện chuẩn. Trên các máy chấm chạy Linux của Codeforces hay Themis,cinmặc định có thể không chậm tới mức này nhưng vẫn đủ khiến bài nộp bị quá thời gian. Bài học rút ra là: luôn chủ động tắt đồng bộ ngay từ dòng đầu tiên của hàmmain. - Với Python, đọc nhị phân một lần nhanh gần gấp 2,6 lần so với
input(). Chênh lệch giữareadlinevàbuffer.readở dữ liệu một triệu dòng là rõ rệt, đặc biệtbuffer.read().split()xử lý hoàn hảo mọi trường hợp dữ liệu nằm cách nhau bởi dấu cách, dấu tab hoặc xuống dòng bất thường.
3. Mẫu code nhập xuất nhanh cho bài nhiều truy vấn
Bài toán dùng để minh họa là truy vấn tổng đoạn: cho mảng \(A\) gồm \(N\) số và \(Q\) truy vấn, mỗi truy vấn hỏi tổng các phần tử từ vị trí \(l\) đến \(r\). Dùng mảng cộng dồn, mỗi truy vấn trả lời trong \(\mathcal{O}(1)\):
\[\text{pref}[i] = \text{pref}[i-1] + A_i, \qquad \text{tổng}(l, r) = \text{pref}[r] - \text{pref}[l-1]\]
Khi \(N\) và \(Q\) đều lên tới \(10^6\), thuật toán chỉ tốn khoảng \(2 \cdot 10^6\) phép cộng trừ, nên thời gian chạy gần như hoàn toàn nằm ở phần đọc \(3 \cdot 10^6\) số và in \(10^6\) đáp án. Đây đúng là kiểu bài mà nhập xuất quyết định đậu hay rớt.
#include <bits/stdc++.h>
using namespace std;
// Mẫu Fast I/O chuẩn trong C++17 xử lý N số và Q truy vấn tổng đoạn
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
if (!(cin >> n >> q)) {
vector<long long> a = {3, -1, 4, 1, 5, 9};
vector<pair<int, int>> qs = {{1, 3}, {2, 5}, {1, 6}};
vector<long long> pref(a.size() + 1, 0);
for (size_t i = 0; i < a.size(); ++i) pref[i + 1] = pref[i] + a[i];
for (size_t i = 0; i < qs.size(); ++i) {
auto [l, r] = qs[i];
cout << (pref[r] - pref[l - 1]) << (i + 1 == qs.size() ? "" : " ");
}
cout << "\n";
return 0;
}
vector<long long> pref(n + 1, 0);
for (int i = 1; i <= n; ++i) {
long long x;
cin >> x;
pref[i] = pref[i - 1] + x;
}
for (int i = 0; i < q; ++i) {
int l, r;
cin >> l >> r;
cout << (pref[r] - pref[l - 1]) << (i + 1 == q ? "" : " ");
}
cout << "\n";
return 0;
}
import sys
def solve() -> None:
data = sys.stdin.buffer.read().split()
if not data:
a = [3, -1, 4, 1, 5, 9]
qs = [(1, 3), (2, 5), (1, 6)]
pref = [0] * (len(a) + 1)
for i, x in enumerate(a, 1):
pref[i] = pref[i - 1] + x
out = [str(pref[r] - pref[l - 1]) for l, r in qs]
sys.stdout.write(" ".join(out) + "\n")
return
n = int(data[0])
q = int(data[1])
pref = [0] * (n + 1)
for i in range(1, n + 1):
pref[i] = pref[i - 1] + int(data[1 + i])
idx = 2 + n
out = []
for _ in range(q):
l = int(data[idx])
r = int(data[idx + 1])
idx += 2
out.append(str(pref[r] - pref[l - 1]))
sys.stdout.write(" ".join(out) + "\n")
if __name__ == "__main__":
solve()
Chạy tay với \(A = [3, -1, 4, 1, 5, 9]\):
| Chỉ số \(i\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| \(A_i\) | - | 3 | −1 | 4 | 1 | 5 | 9 |
| \(\text{pref}[i]\) | 0 | 3 | 2 | 6 | 7 | 12 | 21 |
- Truy vấn \((1, 3)\): \(\text{pref}[3] - \text{pref}[0] = 6 - 0 = 6\).
- Truy vấn \((2, 5)\): \(\text{pref}[5] - \text{pref}[1] = 12 - 3 = 9\).
- Truy vấn \((1, 6)\): \(\text{pref}[6] - \text{pref}[0] = 21 - 0 = 21\).
Chương trình in ra kết quả:
6 9 21
Hai chi tiết trong mẫu Python cần đặc biệt chú ý:
- Đọc một lần, duyệt bằng chỉ số: Toàn bộ số được nạp vào mảng danh sách
data, sau đó ta dùng biến con trỏidxđể lấy lần lượt từng phần tử, không gọi thêm bất kỳ hàm đọc I/O nào bên trong các vòng lặp truy vấn. - Gom kết quả rồi in một lần duy nhất: Các kết quả được chuyển thành chuỗi và gom vào danh sách
out, cuối cùng ghép lại bằng" ".join(out)và in ra bằngsys.stdout.write. Việc gọi hàmprint()một triệu lần bên trong vòng lặp chậm hơn rất nhiều lần so với việc ghi một chuỗi lớn ra màn hình một lần.
4. Kỹ thuật nâng cao: Đọc từng ký tự và lưu ý bài tương tác
Trong các kỳ thi đỉnh cao như kỳ thi HSG Quốc gia (VOI) hoặc các bài thi kỳ thi ICPC Quốc tế, đôi khi đề bài yêu cầu giới hạn thời gian cực gắt (ví dụ chỉ \(0.3\text{ giây}\) cho \(2 \times 10^6\) số nguyên). Lúc này, các lập trình viên thi đấu chuyên nghiệp thường sử dụng hàm đọc số nguyên tự viết bằng getchar_unlocked() hoặc đọc theo khối bằng fread():
inline int readInt() {
int x = 0, f = 1;
char ch = getchar_unlocked();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar_unlocked();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar_unlocked();
}
return x * f;
}
Hàm readInt() trên bỏ qua toàn bộ overhead phân tích chuỗi của thư viện chuẩn, đọc trực tiếp từng byte ký tự từ bộ đệm của hệ điều hành, giúp giảm thêm khoảng 40–50% thời gian so với cin đã tắt đồng bộ. Tuy nhiên, hàm này không an toàn khi chạy trên môi trường Windows đa luồng nếu dùng bản _getchar_nolock(); trong đa số các bài thi thông thường, hai dòng ios::sync_with_stdio(false); cin.tie(nullptr); đã hoàn toàn đủ nhanh.
Ngoại lệ tối quan trọng: Bài toán tương tác (Interactive Problems)
Trong các bài toán tương tác (chương trình của bạn phải gửi truy vấn và nhận phản hồi tức thì từ máy chấm), cơ chế trì hoãn bộ đệm của Fast I/O sẽ khiến chương trình bị chết đứng (Hang / Idle Timeout):
- Nếu bạn tắt đồng bộ và dùng
'\n', câu hỏi của bạn sẽ nằm lại trong bộ đệm mà không được chuyển tới tiến trình máy chấm. Máy chấm chờ bạn gửi dữ liệu, còn chương trình của bạn lại chờ máy chấm trả lời, dẫn tới lỗi quá thời gian (TLE). - Quy tắc vàng cho bài tương tác: Sau mỗi lần in câu hỏi ra luồng xuất, bạn bắt buộc phải đẩy bộ đệm ngay lập tức bằng lệnh
std::cout << std::flush;(hoặc dùngstd::endl) trong C++, hoặc gọiprint(..., flush=True)/sys.stdout.flush()trong Python!
5. Danh sách kiểm tra trước khi nộp bài
- C++: Đã có
ios::sync_with_stdio(false); cin.tie(nullptr);ở ngay đầu hàmmainchưa? Có dùng lẫnprintfhayscanfkhông? Có còn lệnhendlnào nằm trong các vòng lặp lớn không? - Python: Có gọi
input()hoặcprint()bên trong vòng lặp lớn không? Nếu có, chuyển sangsys.stdin.buffer.read().split()và gom toàn bộ kết quả để in một lần. - Bài tương tác: Đã bổ sung cờ
flushsau mỗi câu trả lời chưa? - Kiểm tra giới hạn dữ liệu: Theo bảng tra cứu trong bài Cách đọc đề và ăn điểm subtask, nếu tổng số lượng phần tử cần đọc từ khoảng \(10^5\) trở lên thì nên dùng ngay mẫu nhập xuất nhanh ngay từ đầu.
Luyện ngay trên Big-O Coding
Ba bài dưới đây có dữ liệu đầu vào lớn, rất hợp để luyện mẫu nhập xuất nhanh cùng mảng cộng dồn:
Dùng cin.tie(NULL) khác gì so với cin.tie(nullptr) trong C++?
Về mặt chức năng trong việc ngắt liên kết giữa cin và cout, hai cách viết này hoàn toàn tương đương nhau. Tuy nhiên, nullptr là từ khóa chuẩn được giới thiệu từ chuẩn C++11 với kiểu dữ liệu con trỏ rõ ràng (std::nullptr_t), giúp tránh các lỗi ép kiểu ngầm định nguy hiểm của hằng số NULL (vốn là số nguyên 0 trong C). Vì vậy, bạn nên duy trì thói quen viết cin.tie(nullptr) trong các bài thi dùng chuẩn C++14 trở lên.
Tại sao trên Windows lệnh getchar_unlocked() lại báo lỗi không biên dịch được?
Hàm getchar_unlocked() là hàm chuẩn POSIX có sẵn trên hệ điều hành Linux và macOS nhưng không có sẵn trong thư viện chuẩn MSVC trên Windows (trên Windows hàm tương đương là _getchar_nolock()). Hầu hết các hệ thống chấm bài thi lập trình trực tuyến (như Codeforces, VNOJ, DMOJ, CMS) đều chạy trên nền tảng Linux nên getchar_unlocked() hoạt động bình thường; tuy nhiên để code chạy được trên cả máy cá nhân Windows mà không cần sửa đổi, bạn nên dùng ios::sync_with_stdio(false); cin.tie(nullptr);.
Lộ trình 4 cấp độ cho học sinh thi Tin học trẻ, Chuyên Tin, Olympic, ACM-ICPC: xếp lớp theo năng lực, mini contest, lớp Đội tuyển. Học online, phí theo tháng.


