Почему чтение строк из stdin намного медленнее в C++, чем в Python?
Я хотел сравнить строки чтения строкового ввода из 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 10.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 ответов
По умолчанию, 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
питон
./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
Я здесь на несколько лет позади, но:
В "Редактировании 4/5/6" исходного поста вы используете конструкцию:
$ /usr/bin/time cat big_file | program_to_benchmark
Это неправильно по нескольким причинам:
На самом деле вы определяете время выполнения `cat`, а не ваш тест. Использование ЦП 'user' и 'sys', отображаемое `time`, относится к` cat`, а не к вашей тестовой программе. Хуже того, "реальное" время также не обязательно точное. В зависимости от реализации `cat` и конвейеров в вашей локальной ОС, вполне возможно, что` cat` записывает окончательный гигантский буфер и завершает работу задолго до того, как процесс чтения завершит свою работу.
Использование `cat` является ненужным и фактически контрпродуктивным; вы добавляете движущиеся части. Если вы работали в достаточно старой системе (т. Е. С одним ЦП и - в некоторых поколениях компьютеров - ввод-вывод быстрее, чем ЦП) - сам факт того, что `cat` работал, может существенно повлиять на результаты. Вы также подвержены любой буферизации ввода и вывода и другой обработке, которую может выполнять `cat`. (Это, вероятно, принесло бы вам награду "Бесполезное использование кошки", если бы я был Рэндал Шварц.
Лучшая конструкция будет:
$ /usr/bin/time program_to_benchmark < big_file
В этом утверждении это оболочка, которая открывает big_file, передавая его вашей программе (ну, на самом деле, `time`, которая затем выполняет вашу программу как подпроцесс) как уже открытый файловый дескриптор. Ответственность за чтение файла лежит исключительно на программе, которую вы пытаетесь сравнить. Это дает вам реальное прочтение его производительности без ложных осложнений.
Я упомяну два возможных, но на самом деле неправильных, "исправления", которые также могут быть рассмотрены (но я "нумерую" их по-разному, поскольку это не те вещи, которые были неверны в оригинальном посте):
О. Вы можете "исправить" это, синхронизируя только вашу программу:
$ cat big_file | /usr/bin/time program_to_benchmark
Б. или путем синхронизации всего трубопровода:
$ /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` в конвейер, вероятно, не имеет реальных последствий
Но я говорю это последнее с некоторой нерешительностью. Если мы рассмотрим последний результат в "Редактировать 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), получив эти результаты (на самом деле это результаты "best of 3"; после заполнения кеша, конечно): Обратите внимание, что результаты двух конвейеров утверждают, что они заняли больше процессорного времени (user + 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(). Таким образом, на практике может быть полезно выполнить тесты в обоих направлениях; возможно, мы не будем учитывать результат `cat`, чтобы" простить "стоимость запуска самого` cat`.$ 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)
Я воспроизвел исходный результат на своем компьютере, используя 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] = '\0'; //make it null-terminated
file.close();
Если вы хотите, вы можете обернуть поток вокруг этого буфера для более удобного доступа, например так:
std::istrstream header(&filebuf[0], length);
Кроме того, если вы контролируете файл, рассмотрите возможность использования плоского двоичного формата данных вместо текста. Надежнее читать и писать, потому что вам не нужно иметь дело со всеми неопределенностями пробелов. Это также меньше и намного быстрее, чтобы разобрать.
Следующий код был для меня быстрее, чем другой код, опубликованный здесь: (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 раза.
Кстати, причина того, что количество строк для версии C++ на 1 больше, чем количество для версии Python, заключается в том, что флаг eof устанавливается только тогда, когда предпринята попытка чтения за пределами eof. Таким образом, правильный цикл будет:
while (cin) {
getline(cin, input_line);
if (!cin.eof())
line_count++;
};
Во втором примере (с scanf()) причина, по которой это все еще медленнее, может заключаться в том, что scanf("%s") анализирует строку и ищет любой символ пробела (пробел, табуляция, символ новой строки).
Кроме того, да, CPython выполняет некоторое кэширование, чтобы избежать чтения с жесткого диска.
Первый элемент ответа: <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
в fgets
Вы бы увидели еще одно повышение производительности: fgets
это самая быстрая функция C++ для ввода строк.
Кстати, не знал об этой вещи синхронизации, приятно. Но вы все равно должны попробовать fgets
,