benchmarking команды - Почему чтение строк из stdin происходит намного медленнее на C++, чем Python?



список питона (9)

Первый элемент ответа: <iostream> медленный. Проклятье медленно. Я получаю огромное повышение производительности с помощью scanf как 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;
}

Я хотел сравнить строки чтения ввода строки из 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

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

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


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

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

$ /usr/bin/time cat big_file | program_to_benchmark

Это неправильно по-разному:

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

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

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

$ /usr/bin/time program_to_benchmark < big_file

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

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

О. Вы можете «исправить» это по времени только своей программой:

$ 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`)

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

Но я говорю это последнее с некоторым колебанием. Если мы рассмотрим последний результат в «Редактировании 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

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

Тем не менее, я предсказываю, что вы сможете измерить разницу между `cat file | wc -l` и `wc -l <файл` и найдите заметную (2-значную процентную) разницу. Каждый из более медленных тестов будет платить аналогичное наказание в абсолютном времени; который, однако, будет составлять меньшую часть его большего общего времени.

На самом деле, я сделал несколько быстрых тестов с файлом мусора размером 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), чем в реальном времени. Это связано с тем, что я использую встроенную команду «время» оболочки (Bash), которая понимает конвейер; и я на многоядерной машине, где отдельные процессы в конвейере могут использовать отдельные ядра, накапливая время процессора быстрее, чем в реальном времени. Используя / usr / bin / time, я вижу меньшее время процессора, чем в реальном времени, - показывая, что он может использовать только один элемент конвейера, переданный ему в командной строке. Кроме того, вывод оболочки дает миллисекунды, в то время как / usr / bin / time дает только 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 <big_file`, ваша программа получает открытый файловый дескриптор в фактический файл. Ваша программа или, во многих случаях, библиотеки ввода-вывода языка, на котором она была написана, может принимать различные действия при представлении файлового дескриптора, ссылающегося на обычный файл. Он может использовать mmap (2) для сопоставления входного файла в его адресное пространство, вместо использования явных системных вызовов read (2). Эти различия могут иметь гораздо больший эффект на результаты теста, чем небольшие затраты на запуск двоичного кода `cat`.

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


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);

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


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

Добавляя следующие операторы к версии C ++ перед тем, while цикл while вставляет его в строку с версией Python :

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

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


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


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

питон

./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

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

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

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

Используйте ifstream для чтения данных из файла:

std::ifstream input( "filename.ext" );

Если вам действительно нужно читать строки за строкой, сделайте следующее:

for( std::string line; getline( input, line ); )
{
    ...for each line in input...
}

Но вам, вероятно, просто нужно извлечь координатные пары:

int x, y;
input >> x >> y;

Обновить:

В вашем коде вы используете ofstream myfile; , однако o in ofstream означает output . Если вы хотите прочитать из файла (ввод), используйте ifstream . Если вы хотите как читать, так и писать, используйте fstream .





python c++ benchmarking iostream getline