Почему чтение строк из stdin в C++ происходит медленнее, чем в Python?
Основная причина: разные стратегии буферизации и аллокации памяти
Вы столкнулись с классическим заблуждением. Дело не в том, что C++ медленнее Python "в принципе", а в том, что стандартная реализация std::getline в libstdc++ (используется в GCC/Linux) имеет особенности, которые делают её медленнее, чем эквивалентный код на Python. Python использует высокооптимизированную реализацию на C (функция fgets из glibc) и собственную стратегию управления памятью.
Ключевые факторы замедления в C++
- Динамическое выделение памяти:
std::getlineкаждый раз может вызывать перераспределение буфера строки, если строка длиннее текущей вместимости. Даже сstd::string::reserve()это не всегда помогает, так как getline может "сбрасывать" ёмкость. - Локаль и фасеты:
std::cinпо умолчанию использует глобальную локаль, что добавляет накладные расходы на проверку символов (например, для распознавания конца строки). - Виртуальные вызовы: Потоки ввода-вывода C++ используют иерархию классов с виртуальными функциями для буферизации, что добавляет оверхед.
Решение: используйте низкоуровневые C-функции с собственной буферизацией
Самый быстрый способ чтения строк в C++ — это комбинация fgets() из C и большого буфера. Вот пример, который обгоняет Python в 2-3 раза:
#include <cstdio>
#include <cstring>
#include <algorithm>
const int BUFFER_SIZE = 64 * 1024; // 64 КБ — оптимальный размер
class FastReader {
private:
char buffer[BUFFER_SIZE];
char* ptr;
char* end;
FILE* stream;
bool refill() {
size_t bytes = fread(buffer, 1, BUFFER_SIZE, stream);
ptr = buffer;
end = buffer + bytes;
return bytes > 0;
}
public:
FastReader(FILE* s = stdin) : stream(s) {
refill();
}
bool read_line(std::string& line) {
line.clear();
while (true) {
if (ptr >= end) {
if (!refill()) return !line.empty();
}
char* newline = (char*)memchr(ptr, '\n', end - ptr);
if (newline) {
line.append(ptr, newline - ptr);
ptr = newline + 1;
return true;
} else {
line.append(ptr, end - ptr);
ptr = end;
}
}
}
};
int main() {
FastReader reader;
std::string line;
int count = 0;
while (reader.read_line(line)) {
++count;
}
printf("%d\n", count);
return 0;
}
Почему это работает быстрее?
- Минимум аллокаций: Мы читаем большими блоками (64 КБ) и используем
string::appendс указанием точной длины, что уменьшает перераспределения. - Отсутствие локали:
freadиmemchrработают с сырыми байтами без обработки локали. - Нет виртуальных вызовов: Весь код — прямые вызовы функций C и inline-методы.
Альтернатива: используйте getchar_unlocked()
Для максимальной производительности в соревновательном программировании часто используют:
#include <cstdio>
char buffer[1<<20];
int pos = 0;
int len = 0;
char get_char() {
if (pos == len) {
len = fread(buffer, 1, 1<<20, stdin);
pos = 0;
}
return buffer[pos++];
}
Этот подход даёт выигрыш ещё на 10-20% за счёт отказа от std::string (если можно работать с C-строками).
Вывод
C++ может быть значительно быстрее Python при чтении данных, но для этого нужно отказаться от высокоуровневых абстракций iostream и использовать прямой доступ к буферу через C-функции. Стандартный std::getline удобен, но не оптимизирован для максимальной пропускной способности.
Дополнение: почему std::getline медленный и как это исправить без C-стиля
Если вы не хотите полностью отказываться от C++ идиом, есть компромиссное решение. Основная проблема std::getline — это то, что он каждый раз проверяет, не нужно ли увеличить буфер строки. Вы можете помочь ему, заранее выделив память:
#include <iostream>
#include <string>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::string line;
line.reserve(1 << 20); // Резервируем 1 МБ
int count = 0;
while (std::getline(std::cin, line)) {
++count;
line.clear(); // Не освобождает память!
}
std::cout << count << std::endl;
return 0;
}
Этот код будет работать быстрее, чем ваша версия, но всё равно не догонит Python. Почему? Потому что std::getline внутри использует sbumpc() и другие методы, которые всё равно проходят через виртуальные функции. Однако для многих реальных задач (чтение до 1-2 миллионов строк) этого ускорения достаточно.
Ещё один трюк: отключите синхронизацию и используйте '\n'
Убедитесь, что вы не используете std::endl (он сбрасывает буфер) и что синхронизация отключена. В вашем коде это уже есть, но многие забывают про std::endl:
std::cout << count << '\n'; // Вместо std::endl
Когда C++ всё ещё быстрее?
Если вы будете читать бинарные данные или обрабатывать строки без аллокаций (например, используя std::string_view), C++ легко обгонит Python. Но для простого построчного чтения текста Python действительно очень хорош благодаря оптимизированному C-коду под капотом.