21

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

15

Я хотел сравнить скорость чтения строк из стандартного ввода, используя Python и C++, и был поражен тем, что мой код на C++ работает на порядок медленнее, чем эквивалентный код на Python. Поскольку мой C++ немного ржавый, а я еще не стал экспертом в Python, прошу вас сказать, делаю ли я что-то неправильно или неправильно понимаю ситуацию.

Краткий ответ: Добавьте инструкцию cin.sync_with_stdio(false) или просто используйте fgets вместо этого.

Краткие результаты: Прокрутите до самого конца моего вопроса и посмотрите на таблицу.

Код на C++:

#include <iostream>
#include <time.h>

using namespace std;

int main() {
    string input_line;
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    while (cin) {
        getline(cin, input_line);
        if (!cin.eof())
            line_count++;
    };

    sec = (int) time(NULL) - start;
    cerr << "Прочитано " << line_count << " строк за " << sec << " секунд.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Компилировалось с помощью:
// g++ -O3 -o readline_test_cpp foo.cpp

Эквивалент на Python:

#!/usr/bin/env python
import time
import sys

count = 0
start = time.time()

for line in sys.stdin:
    count += 1

delta_sec = int(time.time() - start)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Прочитано {0} строк за {1} секунд. LPS: {2}".format(count, delta_sec, lines_per_sec))

Вот мои результаты:

$ cat test_lines | ./readline_test_cpp
Прочитано 5570000 строк за 9 секунд. LPS: 618889

$ cat test_lines | ./readline_test.py
Прочитано 5570000 строк за 1 секунд. LPS: 5570000

Я должен заметить, что я тестировал это как на Mac OS X v10.6.8 (Snow Leopard), так и на Linux 2.6.32 (Red Hat Linux 6.2). Первый — это MacBook Pro, а второй — весьма мощный сервер, хотя это не слишком актуально.

$ for i in {1..5}; do echo "Тестовый запуск $i в `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Тестовый запуск 1 в Mon Feb 20 21:29:28 EST 2012
CPP:   Прочитано 5570001 строк за 9 секунд. LPS: 618889
Python:Прочитано 5570000 строк за 1 секунд. LPS: 5570000
Тестовый запуск 2 в Mon Feb 20 21:29:39 EST 2012
CPP:   Прочитано 5570001 строк за 9 секунд. LPS: 618889
Python:Прочитано 5570000 строк за 1 секунд. LPS: 5570000
Тестовый запуск 3 в Mon Feb 20 21:29:50 EST 2012
CPP:   Прочитано 5570001 строк за 9 секунд. LPS: 618889
Python:Прочитано 5570000 строк за 1 секунд. LPS: 5570000
Тестовый запуск 4 в Mon Feb 20 21:30:01 EST 2012
CPP:   Прочитано 5570001 строк за 9 секунд. LPS: 618889
Python:Прочитано 5570000 строк за 1 секунд. LPS: 5570000
Тестовый запуск 5 в Mon Feb 20 21:30:11 EST 2012
CPP:   Прочитано 5570001 строк за 10 секунд. LPS: 557000
Python:Прочитано 5570000 строк за  1 секунд. LPS: 5570000

Добавление и краткое резюме по небольшому бенчмарку Для полноты картины я подумал, что стоит обновить скорость чтения для того же файла на том же компьютере с оригинальным (с синхронизацией) кодом C++. Сравнение выглядит следующим образом, с несколькими решениями/подходами:

Реализация Строк в секунду
python (по умолчанию) 3,571,428
cin (по умолчанию/наивно) 819,672
cin (без синхронизации) 12,500,000
fgets 14,285,714
wc (несправедливое сравнение) 54,644,808

5 ответ(ов)

0

getline, операторы потоков и scanf могут быть удобны, если вам не важна скорость загрузки файла или если вы загружаете небольшие текстовые файлы. Но, если производительность для вас имеет значение, лучше всего загрузить весь файл в память (при условии, что он помещается).

Вот пример:

// открываем файл в двоичном режиме
std::fstream file(filename, std::ios::in | std::ios::binary);
if (!file) return NULL;

// читаем размер...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

// читаем в буфер памяти, затем закрываем его.
char *filebuf = new char[length + 1];
file.read(filebuf, length);
filebuf[length] = '\0'; // делаем его нуль-терминированным
file.close();

Если хотите, можете обернуть поток вокруг этого буфера для более удобного доступа, как вот здесь:

std::istrstream header(&filebuf[0], length);

Кроме того, если у вас есть контроль над форматом файла, рассмотрите возможность использования плоского бинарного формата вместо текстового. Это более надежный способ чтения и записи, поскольку вам не придется иметь дело со всеми неопределенностями, связанными с пробелами. Он также меньше по размеру и гораздо быстрее в обработке.

0

Причина, по которой количество строк в версии на C++ на одну больше, чем в версии на Python, заключается в том, что флаг eof устанавливается только при попытке считывания за пределами конца файла. Поэтому правильный цикл будет выглядеть так:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};
0

В вашем втором примере (с использованием scanf()) причиной, по которой это всё ещё медленнее, может быть то, что scanf("%s") разбирает строку и ищет любой символ пробела (пробел, табуляция, символ новой строки).

Кроме того, да, CPython использует некоторый механизм кэширования, чтобы избежать чтения с жесткого диска.

0

Первый элемент ответа: <iostream> действительно медленный. Черт возьми, как медленный! Я получаю значительный прирост производительности с помощью scanf, как показано ниже, но всё равно это в два раза медленнее, чем Python.

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Прочитано " << line_count << " строк за " << sec << " секунд." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Скорость обработки: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

Если вам нужно улучшить производительность ввода-вывода в C++, то стоит рассмотреть использование scanf, так как он быстрее, чем стандартные средства ввода из <iostream>. Однако имейте в виду, что в некоторых случаях такие подобные нижние уровни могут быть в два раза медленнее даже, чем интерпретируемые языки, такие как Python.

0

Ну, я вижу, что во втором решении вы переключились с cin на scanf, что было первым предложением, которое я хотел вам сделать (поскольку cin очень медленный). Теперь, если вы перейдете с scanf на fgets, вы увидите еще одно улучшение производительности: fgets — самая быстрая функция C++ для ввода строк.

Кстати, я не знал про эту синхронизацию, любопытно. Но все равно стоит попробовать fgets.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь