Почему чтение строк из stdin намного медленнее в C ++, чем в Python? - PullRequest
1684 голосов
/ 21 февраля 2012

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


(ответ TLDR: включите утверждение:cin.sync_with_stdio(false) или просто используйте fgets вместо.

Результаты 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 ++.Опять же, это для 100-строчного файла на быстром диске.Вот сравнение с несколькими решениями / подходами:

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 ]

1504 голосов
/ 21 февраля 2012

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

std::ios_base::sync_with_stdio(false);

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

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

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

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

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

151 голосов
/ 11 марта 2012

Просто из любопытства я взглянул на то, что происходит под капотом, и использовал 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
124 голосов
/ 07 мая 2017

Я на несколько лет отстал, но:

В 'Edit 4/5/6' оригинального поста вы используете конструкцию:

$ /usr/bin/time cat big_file | program_to_benchmark

Thisневерен в нескольких отношениях:

  1. Вы на самом деле определяете время выполнения `cat`, а не своего теста.Использование ЦП 'user' и 'sys', отображаемое `time`, относится к` cat`, а не к вашей тестовой программе.Хуже того, «реальное» время также не обязательно точное.В зависимости от реализации `cat` и конвейеров в вашей локальной ОС, вполне возможно, что` cat` записывает последний гигантский буфер и завершает работу задолго до того, как процесс чтения завершит свою работу.

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

Лучшая конструкция была бы:

$ /usr/bin/time program_to_benchmark < big_file

В этом выражении это shell , который открывает big_file, передавая его вашей программе (ну, на самом деле, `time`, которая затем выполняет вашу программу как подпроцесс) в качестве уже открытого дескриптора файла.На 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

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

  • на практике , на современных машинах добавленный `cat` в конвейере, вероятно, имеетникаких реальных последствий

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

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

- это говорит о том, что `cat` потребляла 74% процессора во время теста;и действительно, 1,34 / 1,83 составляет примерно 74%.Возможно, выполнение:

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

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

Тем не менее, я предсказываю, что вы сможете измерить разницу между `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)

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

Таким образом, на уровне эффективности `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`.

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

86 голосов
/ 21 февраля 2012

Я воспроизвел исходный результат на своем компьютере, используя 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 секунды.

36 голосов
/ 14 марта 2012

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] = '\0'; //make it null-terminated
file.close();

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

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

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

16 голосов
/ 11 марта 2012

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

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

    if (!cin.eof())
        line_count++;
};
15 голосов
/ 23 апреля 2014

Следующий код был для меня быстрее, чем другой код, размещенный здесь до сих пор: (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 раза.

13 голосов
/ 21 февраля 2012

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

11 голосов
/ 21 февраля 2012

Первый элемент ответа: <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;
}
10 голосов
/ 22 февраля 2012

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

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

...