Avatar ·

Почему чтение строк из stdin в C++ происходит медленнее, чем в Python?

Всем привет! Я начинающий C++ разработчик, и у меня возникла странная проблема. Я решал задачу на Codeforces, где нужно было прочитать 10 миллионов целых чисел из стандартного ввода. Написал код на C++ с использованием std::cin и std::getline, но программа работала мучительно долго — около 30 секунд. Решил для проверки написать аналогичный скрипт на Python с обычным input() и sys.stdin.read().split(), и он выполнился всего за 3-4 секунды! Как такое возможно? Ведь C++ считается более производительным языком. Вот мой код на C++: ```cpp #include #include #include int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::vector numbers; std::string line; while (std::getline(std::cin, line)) { // Парсим числа из строки size_t pos = 0; while (pos < line.size()) { size_t next = line.find(' ', pos); if (next == std::string::npos) next = line.size(); numbers.push_back(std::stoi(line.substr(pos, next - pos))); pos = next + 1; } } std::cout << "Прочитано " << numbers.size() << " чисел" << std::endl; return 0; } ``` Я уже отключил синхронизацию с stdio и отвязал cin от cout, но это не помогло. В чём может быть дело? Почему C++ проигрывает Python в такой простой задаче? Может, я что-то делаю не так? <<>>

Почему ваш код медленный и как это исправить

Проблема не в C++ как таковом, а в вашем подходе к чтению и обработке данных. Вы совершаете несколько типичных ошибок, которые убивают производительность. Давайте разберём их по порядку.

Основные проблемы в вашем коде

  • Многократные выделения памяти: line.substr() создаёт новую строку для каждого числа. Это приводит к огромному количеству аллокаций и копирований.
  • Медленный парсинг: std::stoi каждый раз заново разбирает строку, что дорого.
  • Использование getline: Хотя это удобно, getline сам по себе медленнее прямого чтения через оператор >>.

Правильное решение: используйте прямой ввод и быстрый парсинг

Вот оптимизированная версия, которая будет работать быстрее Python:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    // Читаем сразу весь поток в буфер
    std::cin >> std::noskipws;
    std::string input(std::istreambuf_iterator<char>(std::cin), {});
    
    // Ручной быстрый парсинг чисел
    std::vector<int> numbers;
    numbers.reserve(10000000); // Резервируем место заранее
    
    const char* ptr = input.data();
    const char* end = ptr + input.size();
    
    while (ptr < end) {
        // Пропускаем пробелы
        while (ptr < end && (*ptr == ' ' || *ptr == '\n')) ++ptr;
        if (ptr >= end) break;
        
        // Парсим число
        int num = 0;
        bool negative = false;
        if (*ptr == '-') {
            negative = true;
            ++ptr;
        }
        while (ptr < end && *ptr >= '0' && *ptr <= '9') {
            num = num * 10 + (*ptr - '0');
            ++ptr;
        }
        if (negative) num = -num;
        numbers.push_back(num);
    }
    
    std::cout << "Прочитано " << numbers.size() << " чисел" << std::endl;
    return 0;
}

Почему это работает быстро?

  • Однократное чтение: Мы читаем весь stdin в память одним махом через istreambuf_iterator. Это минимизирует количество системных вызовов.
  • Ручной парсинг: Вместо stoi и substr мы используем простой цикл с арифметикой указателей. Это в десятки раз быстрее.
  • Предварительное резервирование: reserve() предотвращает множественные переаллокации вектора.

Альтернатива: используйте scanf (но осторожно)

Можно также использовать scanf, который работает быстрее cin >>:

#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers;
    numbers.reserve(10000000);
    int num;
    while (scanf("%d", &num) == 1) {
        numbers.push_back(num);
    }
    printf("Прочитано %zu чисел\n", numbers.size());
    return 0;
}

Этот вариант тоже будет быстрым, но уступает ручному парсингу на 10-20%.

Почему Python быстрее в вашем случае?

Python использует sys.stdin.buffer.read(), который читает всё сразу, а затем split() и map(int, ...) обрабатывают данные нативно на C-уровне. Ваш C++ код делал то же самое, но с постоянными накладными расходами на создание строк. После оптимизации C++ обгонит Python в 2-3 раза.

Вывод: C++ не медленный — медленным может быть ваш код. Используйте правильные инструменты: читайте большими блоками, избегайте лишних аллокаций и используйте ручной парсинг для критичных участков.

<<>>

Дополнительный совет: используйте memory-mapped файлы для максимальной производительности

Если вам нужно читать действительно огромные объёмы данных (сотни мегабайт или гигабайты), можно пойти ещё дальше и использовать memory-mapped файлы (mmap). Это позволяет отобразить файл (или stdin) прямо в адресное пространство процесса, избегая даже копирования данных из буфера ядра.

Вот пример для Linux:

#include <iostream>
#include <vector>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // Отображаем stdin как файл
    int fd = STDIN_FILENO;
    off_t size = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);
    
    void* mapped = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        std::cerr << "mmap failed\n";
        return 1;
    }
    
    const char* data = static_cast<const char*>(mapped);
    // Далее парсим data так же, как в первом ответе
    
    munmap(mapped, size);
    return 0;
}

Важно: Этот метод не работает с piped stdin (например, при перенаправлении через |), только с файлами. Но если вы читаете из файла напрямую, mmap даёт прирост ещё на 30-50% по сравнению с обычным чтением.

Для Windows используйте CreateFileMapping и MapViewOfFile.

Однако для большинства задач достаточно оптимизации из первого ответа — ручной парсинг с резервированием памяти даёт отличные результаты без усложнения кода.