Я хотел сравнить строки чтения строкового ввода из 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
<iostream>
производительность - отстой. Не в первый раз это случается. 2) Python достаточно умен, чтобы не копировать данные в цикле for, потому что вы его не используете. Вы можете повторно протестировать, пытаясь использоватьscanf
иchar[]
. В качестве альтернативы вы можете попробовать переписать цикл так, чтобы что-то было сделано со строкой (например, сохранить 5-ю букву и объединить ее в результате).cin.eof()
!! Поместитеgetline
вызов в оператор «если».wc -l
быстрый, потому что читает поток более чем одной строкой одновременно (это может бытьfread(stdin)/memchr('\n')
комбинация). Результаты Python в том же порядке, например,wc-l.py
Ответы:
По умолчанию
cin
синхронизируется со stdio, что позволяет избежать буферизации ввода. Если вы добавите это в начало вашего основного списка, вы увидите гораздо лучшую производительность:Обычно, когда входной поток буферизуется, вместо чтения по одному символу за раз, поток будет читаться большими кусками. Это уменьшает количество системных вызовов, которые обычно относительно дороги. Однако, поскольку
FILE*
основаныstdio
иiostreams
часто имеют отдельные реализации и, следовательно, отдельные буферы, это может привести к проблеме, если оба будут использоваться вместе. Например:Если было прочитано больше входных данных,
cin
чем фактически требовалось, тогда второе целочисленное значение не было бы доступно дляscanf
функции, которая имеет свой собственный независимый буфер. Это привело бы к неожиданным результатам.Чтобы избежать этого, по умолчанию потоки синхронизируются с
stdio
. Один из распространенных способов добиться этого -cin
читать каждый символ по одному за раз, используяstdio
функции. К сожалению, это вносит много накладных расходов. Для небольших объемов ввода это не большая проблема, но когда вы читаете миллионы строк, снижение производительности является значительным.К счастью, разработчики библиотеки решили, что вы также должны иметь возможность отключить эту функцию, чтобы повысить производительность, если вы знали, что делаете, и поэтому предоставили
sync_with_stdio
метод.источник
fscanf
вызовом, потому что это просто не делает так много работы, как Python. Python должен выделять память для строки, возможно, несколько раз, поскольку существующее распределение считается неадекватным - точно так же, как в подходе C ++std::string
. Эта задача почти наверняка связана с вводом / выводом, и слишком много FUD обходится вокруг стоимости созданияstd::string
объектов в C ++ или использования<iostream>
само по себе.sync_with_stdio()
это статическая функция-член, и вызов этой функции для любого объекта потока (напримерcin
) включает или выключает синхронизацию для всех стандартных объектов iostream.Просто из любопытства я взглянул на то, что происходит под капотом, и я использовал dtruss / strace в каждом тесте.
C ++
Системные вызовы
sudo dtruss -c ./a.out < in
питон
Системные вызовы
sudo dtruss -c ./a.py < in
источник
Я здесь на несколько лет позади, но:
В «Редактировании 4/5/6» исходного поста вы используете конструкцию:
Это неправильно по нескольким причинам:
Вы на самом деле сроки выполнения
cat
, а не ваш эталон. Отображаемое использование ЦП 'user' и 'sys'time
относится кcat
не вашей тестовой программе. Хуже того, «реальное» время также не обязательно точное. В зависимости от реализацииcat
и конвейеров в вашей локальной ОС, возможно, что будетcat
записан окончательный гигантский буфер и он завершится задолго до того, как процесс чтения завершит свою работу.Использование
cat
является ненужным и фактически контрпродуктивным; вы добавляете движущиеся части. Если вы работали в достаточно старой системе (т. Е. С одним ЦП и - в некоторых поколениях компьютеров - вводом-выводом быстрее, чем ЦП) - сам по себе тот факт, чтоcat
он работал, может существенно повлиять на результаты. Вы также подвержены любой буферизации ввода и вывода и другой обработкеcat
. (Это, вероятно, принесло бы вам награду «Бесполезное использование кошки», если бы я был Рэндал Шварц.Лучшая конструкция была бы:
В этом утверждении это оболочка, которая открывает big_file, передавая его вашей программе (ну, собственно, к
time
которой затем выполняет вашу программу как подпроцесс) как уже открытый дескриптор файла. Ответственность за чтение файла лежит исключительно на программе, которую вы пытаетесь сравнить. Это дает вам реальное прочтение его производительности без ложных осложнений.Я упомяну два возможных, но на самом деле неправильных, «исправления», которые также могут быть рассмотрены (но я «нумерую» их по-разному, поскольку это не те вещи, которые были неверны в оригинальном посте):
О. Вы можете «исправить» это, синхронизируя только вашу программу:
Б. или путем синхронизации всего трубопровода:
Это неправильно по тем же причинам, что и №2: они все еще используются
cat
без необходимости. Я упоминаю их по нескольким причинам:они более «естественны» для людей, которым не совсем удобны средства перенаправления ввода / вывода оболочки POSIX
могут быть случаи , когда
cat
это необходимо (например: файл для чтения требует какой - то привилегии доступа, и вы не хотите , чтобы предоставить эту привилегию программы , которая будет протестированные:sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output
)на практике на современных машинах добавленное
cat
в конвейер, вероятно, не имеет реального значения.Но я говорю это последнее с некоторым колебанием. Если мы рассмотрим последний результат в «Редактировать 5» -
- это утверждает, что
cat
во время теста потребляется 74% процессора; и действительно, 1,34 / 1,83 составляет примерно 74%. Возможно пробег:заняло бы только оставшиеся 49 секунд! Вероятно, нет:
cat
здесь нужно было платить заread()
системные вызовы (или эквивалентные), которые передавали файл с «диска» (фактически буферный кэш), а также за канал записи для их доставкиwc
. Правильный тест все равно должен был бы сделать теread()
звонки; только вызовы write-to-pipe и read-from-pipe были бы сохранены, и они должны быть довольно дешевыми.Тем не менее, я предсказываю , вы могли бы измерить разницу между
cat file | wc -l
иwc -l < file
и найти заметную (2-значный процент) разницу. Каждый из более медленных тестов будет платить аналогичный штраф в абсолютном времени; что, однако, составило бы меньшую долю его большего общего времени.На самом деле я провел несколько быстрых тестов с мусорным файлом объемом 1,5 гигабайта в системе Linux 3.13 (Ubuntu 14.04), получив эти результаты (на самом деле это результаты «best of 3»; после заполнения кеша, конечно):
Обратите внимание, что результаты двух конвейеров утверждают, что они заняли больше процессорного времени (user + sys), чем реальное время настенных часов. Это потому, что я использую встроенную в оболочку команду 'time' 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 < big_file
ваша программа получает дескриптор открытого файла к реальному файлу. Ваша программа - или во многих случаях библиотеки ввода / вывода того языка, на котором она была написана, - может выполнять различные действия при представлении файлового дескриптора, ссылающегося на обычный файл. Он может использоватьmmap(2)
для отображения входного файла в его адресное пространство вместо использования явныхread(2)
системных вызовов. Эти различия могут оказать гораздо большее влияние на результаты теста, чем небольшая стоимость запускаcat
двоичного файла.Конечно, это интересный результат теста, если одна и та же программа работает в разных случаях значительно по-разному. Это показывает , что, действительно, программа или его библиотеки ввода / вывода будут делать что - то интересное, как использование
mmap()
. Так что на практике было бы неплохо выполнить тесты в обоих направлениях; возможно, дисконтированиеcat
результата каким-то небольшим фактором, чтобы «простить» стоимость работыcat
самого себя.источник
$ < big_file time my_program
$ time < big_file my_program
это должно работать в любой оболочке POSIX (т.е. не `csh `а я не уверен насчет экзотики типа` rc`:)time
измеряет весь конвейер, а не первую программу.time seq 2 | while read; do sleep 1; done
отпечатки 2 сек,/usr/bin/time seq 2 | while read; do sleep 1; done
отпечатки 0 сек.Я воспроизвел исходный результат на своем компьютере, используя g ++ на Mac.
Добавление следующих операторов в версию C ++ непосредственно перед тем, как
while
цикл приводит его в соответствие с версией Python :sync_with_stdio улучшил скорость до 2 секунд, а установка большего буфера снизила его до 1 секунды.
источник
getline
Операторы потокаscanf
могут быть удобны, если вам не важно время загрузки файла или если вы загружаете небольшие текстовые файлы. Но если вам важна производительность, вам нужно просто поместить в буфер весь файл (при условии, что он уместится).Вот пример:
Если вы хотите, вы можете обернуть поток вокруг этого буфера для более удобного доступа, например так:
Кроме того, если вы контролируете файл, рассмотрите возможность использования плоского двоичного формата данных вместо текста. Надежнее читать и писать, потому что вам не нужно разбираться со всеми неопределенностями пробелов. Это также меньше и намного быстрее, чтобы разобрать.
источник
Следующий код был для меня быстрее, чем другой код, опубликованный здесь: (Visual Studio 2013, 64-битный, 500 МБ файл с длиной строки равномерно в [0, 1000)).
Он превосходит все мои попытки Python более чем в 2 раза.
источник
read
системные вызовы в статический буфер длиныBUFSIZE
или через эквивалентные соответствующиеmmap
системные вызовы, а затем перебирает этот буфер, считая символы новой строкиfor (char *cp = buf; *cp; cp++) count += *cp == "\n"
. Вы должны будете настроитьсяBUFSIZE
на свою систему, однако, что STDIO уже сделал для вас. Но этотfor
цикл должен компилироваться в потрясающе быстрые инструкции на языке ассемблера для аппаратного обеспечения вашего устройства.Между прочим, причина, по которой количество строк для версии C ++ больше, чем количество для версии Python, заключается в том, что флаг eof устанавливается только при попытке чтения за пределами eof. Таким образом, правильный цикл будет:
источник
while (getline(cin, input_line)) line_count++;
++line_count;
и нетline_count++;
.long
, и компилятор вполне может сказать, что результат приращения не используется. Если он не генерирует идентичный код для постинкремента и преинкремента, он не работает.++line_count;
вместо этогоline_count++;
не мешало бы :)while
, верно? Будет ли иметь значение, если произошла какая-то ошибка, и вы хотели убедиться, что онаline_count
была правильной? Я просто догадываюсь, но я не понимаю, почему это важно.Во втором примере (с scanf ()) причина, по которой это все еще медленнее, может заключаться в том, что scanf ("% s") анализирует строку и ищет любой символ пробела (пробел, табуляция, символ новой строки).
Кроме того, да, CPython выполняет некоторое кэширование, чтобы избежать чтения с жесткого диска.
источник
Первый элемент ответа:
<iostream>
медленно. Чертовски медленно. Я получаю огромный прирост производительности, как показаноscanf
ниже, но он все еще в два раза медленнее, чем Python.источник
Ну, я вижу , что в вашем втором растворе вы перешли от
cin
кscanf
, который был первым предложение , которое я собирался сделать вас (КИН sloooooooooooow). Теперь, если вы переключитесь сscanf
наfgets
, вы увидите еще одно повышение производительности:fgets
это самая быстрая функция C ++ для строкового ввода.Кстати, не знал об этой вещи синхронизации, хорошо. Но вы все равно должны попробовать
fgets
.источник
fgets
будет неправильным (с точки зрения количества строк и с точки зрения разделения строк по циклам, если вам действительно нужно их использовать) для достаточно больших строк, без дополнительных проверок на неполные строки (и попытка компенсировать это включает выделение излишне больших буферов гдеstd::getline
обрабатывает перераспределение, чтобы соответствовать фактическому вводу плавно). Быстро и неправильно легко, но почти всегда стоит использовать «немного медленнее, но правильно», что отключаетsync_with_stdio
вас.