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

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


(TLDR ответ: включить заявление:cin.sync_with_stdio(false) или просто использовать .

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


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 << "Read " << line_count << " lines in " << sec << " seconds.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Compiled with:
// 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_time)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
       lines_per_sec))

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

$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889

$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. 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 "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP:   Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in  1 seconds. LPS: 5570000

крошечные бенчмарк добавление и резюме

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

Implementation      Lines per second
python (default)           3,571,428
cin (default/naive)          819,672
cin (no sync)             12,500,000
fgets                     14,285,714
wc (not fair comparison)  54,644,808

10 ответов


по умолчанию cin синхронизируется с stdio,что заставляет его избегать буферизации ввода. Если вы добавите это в верхнюю часть своего основного, вы увидите гораздо лучшую производительность:

std::ios_base::sync_with_stdio(false);

обычно, когда входной поток буферизуется, вместо чтения одного символа за раз, поток будет считываться большими кусками. Это уменьшает количество системных вызовов, которые обычно являются относительно дорогими. Однако, начиная с FILE* на основе stdio и iostreams частое отдельные реализации и, следовательно, отдельные буферы, это может привести к проблеме, если оба используются вместе. Например:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

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

чтобы избежать этого, по умолчанию, потоки синхронизируются с stdio. Один из распространенных способов добиться этого - есть cin прочитайте каждый символ по одному, используя stdio функции. К сожалению, это накладные расходы. Для небольших объемов ввода это не большая проблема, но когда вы читаете миллионы строк, штраф за производительность значителен.

к счастью, дизайнеры библиотеки решили, что вы также должны быть в состоянии отключить эту функцию, чтобы получить улучшенную производительность, если вы знали, что вы делаете, поэтому они предоставили sync_with_stdio метод.


просто из любопытства я посмотрел на то, что происходит под капотом, и я использовал dtruss / strace по каждому тесту.

C++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

системных вызовов sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

Python

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

системных вызовов sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29

я отстаю здесь на несколько лет, но:

в "Edit 4/5/6" исходного сообщения вы используете конструкцию:

$ /usr/bin/time cat big_file | program_to_benchmark

это неправильно в несколько различных способов:

  1. вы на самом деле синхронизируете выполнение "кошки", а не своего эталона. "Пользователь" и " sys "CPU, отображаемые "время", являются "cat", а не вашей эталонной программой. Хуже того, "Реальное" время также не обязательно точно. В зависимости от реализация " cat "и конвейеров в вашей локальной ОС, возможно, что" cat " пишет окончательный гигантский буфер и выходит задолго до того, как процесс чтения завершит свою работу.

  2. использование " cat " является ненужным и на самом деле контрпродуктивным; вы добавляете движущиеся части. Если вы были в достаточно старой системе (т. е. с одним процессором и-в некоторых поколениях компьютеров-ввод / вывод быстрее, чем CPU) - простой факт, что " cat " работал, мог существенно окрашивать результаты. Вы также подвержены любой входной и выходной буферизации и другой обработке "cat". (Это, вероятно, заработает вам 'Бесполезное Использование Кошки' награда если бы я был Рэндал Шварц.

лучше конструкция:

$ /usr/bin/time program_to_benchmark < big_file

в этом заявлении, это shell который открывает big_file, передавая его в вашу программу (ну, на самом деле в " время`, которое затем выполняет вашу программу как подпроцесс) как уже открытый файловый дескриптор. 100% чтения файлов является строго ответственностью программы, которую вы пытаетесь проверить. Это дает вам реальное чтение его производительности без ложных осложнений.

я упомяну два возможных, но на самом деле неправильных "исправления", которые также можно было бы рассмотреть (но я "нумерую" их по-разному, поскольку это не вещи, которые были неправильными в исходном сообщении):

A. вы смогли "исправить" это путем приурочивать только ваше программа:

$ cat big_file | /usr/bin/time program_to_benchmark

B. или по времени всего трубопровода:

$ /usr/bin/time sh -c 'cat big_file | program_to_benchmark'

это неправильно по тем же причинам, что и #2: они все еще используют " cat " без необходимости. Я упоминаю их по нескольким причинам:

  • они более "естественны" для людей, которым не совсем комфортно с возможностями перенаправления ввода-вывода оболочки POSIX

  • могут быть случаи, когда "кошка"и (например: файл для чтения требуется какая-то привилегия для доступа, и вы не хотите предоставлять эту привилегию программе для бенчмаркинга: `sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output`)

  • на практике, на современных машинах добавленная "кошка" в трубопроводе, вероятно, не имеет реального значения

но я говорю это последнее с некоторым колебанием. Если мы рассмотрим последний результат в 'Edit 5' --

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

-- это утверждает, что "кошка" потребляла 74% процессора во время теста; и действительно, 1.34/1.83 составляет приблизительно 74%. Возможно, пробежка:

$ /usr/bin/time wc -l < temp_big_file

взял бы только оставшиеся .49 секунд! Вероятно, нет: "cat" здесь должен был заплатить за системные вызовы read () (или эквивалент), которые передали файл с "диска" (фактически буферный кэш), а также запись канала для доставки их в "wc". Правильный тест все равно должен был бы сделать эти вызовы read() ; только напишите к трубе и от трубы звонков было бы спасено, и следует довольно дешево.

тем не менее, я предсказываю, что вы сможете измерить разницу между "cat file | wc-l" и "wc-l

на самом деле я сделал несколько быстрых тестов с 1.5 файл гигабайта мусора, в системе Linux 3.13 (Ubuntu 14.04), получая эти результаты (на самом деле это "лучшие из 3" результатов; после затравки кэша, конечно):

$ time wc -l < /tmp/junk
real 0.280s user 0.156s sys 0.124s (total cpu 0.280s)
$ time cat /tmp/junk | wc -l
real 0.407s user 0.157s sys 0.618s (total cpu 0.775s)
$ time sh -c 'cat /tmp/junk | wc -l'
real 0.411s user 0.118s sys 0.660s (total cpu 0.778s)

обратите внимание, что два результата конвейера утверждают, что заняли больше времени процессора (user+sys), чем в реальном времени. Это потому, что я использую встроенную команду "time" оболочки (Bash), которая знает конвейер; и я на многоядерной машине, где отдельные процессы в конвейере могут использовать отдельные ядра, накопление времени процессора быстрее, чем в реальном времени. Используя /usr/bin / time, я вижу меньшее время процессора, чем в реальном времени, показывая, что он может только время одного элемента конвейера, переданного ему в командной строке. Кроме того, выход оболочки дает миллисекунд, в то время как в /usr/Бен/время только дает hundreths секунды.

таким образом, на уровне эффективности "wc-l "" cat " имеет огромное значение: 409 / 283 = 1.453 или 45.3% больше реального времени и 775 / 280 = 2.768 или колоссальный 177% больше используемого процессора! На мой случайные он-был-есть-в-в-время испытательного бокса.

Я должен добавить, что есть, по крайней мере, еще одна существенная разница между этими стилями тестирования, и я не могу сказать, является ли это преимуществом или ошибкой; вы должны решить это сами:

когда вы запускаете "cat big_file//usr / bin / time my_program", ваша программа получает входные данные из трубы, точно в темпе, отправленном "cat", и кусками не больше, чем написано "cat".

когда вы бежите ' / usr / bin / time my_program или во многих случаях библиотеки ввода-вывода языка, на котором он был написан, могут выполнять различные действия при представлении файлового дескриптора, ссылающегося на обычный файл. Он может использовать mmap(2) для отображения входного файла в адресное пространство вместо явных системных вызовов read(2). Эти различия могут оказать гораздо большее влияние на результаты тестирования, чем небольшая стоимость запуска двоичного файла "cat".

конечно, это интересный тестовый результат, если одна и та же программа выполняет значительно по-разному между двумя случаями. Это показывает, что, действительно, программа или ее библиотеки ввода-вывода are делать что-то интересное, например, использовать mmap(). Таким образом, на практике было бы неплохо выполнить контрольные показатели в обоих направлениях; возможно, дисконтирование результата "кошки" каким-то небольшим фактором, чтобы "простить" стоимость запуска самой "кошки".


я воспроизвел исходный результат на своем компьютере с помощью g++ на Mac.

добавление следующих операторов в версию c++ непосредственно перед while цикл приносит его встроенным с Python версия:

std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));

sync_with_stdio улучшил скорость до 2 секунд, а установка большего буфера снизила ее до 1 секунды.


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

вот пример:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = ''; //make it null-terminated
file.close();

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

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

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


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

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

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

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

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


следующий код был быстрее для меня, чем другой код размещен здесь до сих пор: (Visual Studio 2013, 64-разрядный, 500 МБ файл с длиной строки равномерно в [0, 1000)).

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

Он превосходит все мои попытки Python более чем в 2 раза.


первый элемент ответа: <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 << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

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

кстати, не знал об этой синхронизации, хорошо. Но вы все равно должны попробовать fgets.