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


1,267

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

Edit:я должен отметить, что я попробовал это и под Mac   OS   X   v10. 6.8 (Snow   Leopard) и Linux 2.6.32 (Red Hat Linux 6.2). Первый - это MacBook Pro, а последний - очень мясистый сервер, а не то, что это слишком уместно.

Edit 2:(Удалены это изменение, так как больше не применимо)

$ 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 

Edit 3:

Хорошо, я попробовал предложение JN в попытки имеющего Python сохраните строку, прочитанную: но это не имело никакого значения для скорости питона.

Я также попытался предложение J.N. в использовании scanf в char массив вместо getline в std::string. Бинго! Это привело к эквивалентной производительности как для Python, так и для C++. (3,333,333 LPS с моими входными данными, которые, кстати, являются лишь короткими линиями из трех полей каждый, обычно шириной около 20 символов, хотя иногда и больше).

Код:

char input_a[512]; 
char input_b[32]; 
char input_c[512]; 
while(scanf("%s %s %s\n", input_a, input_b, input_c) != EOF) { 
    line_count++; 
}; 

Скорость:

$ cat test_lines | ./readline_test_cpp2 
Read 10000000 lines in 3 seconds. LPS: 3333333 
$ cat test_lines | ./readline_test2.py 
Read 10000000 lines in 3 seconds. LPS: 3333333 

(Да, я побежал в несколько раз.) Так что, я думаю, теперь я буду использовать scanf вместо getline. Но мне все еще интересно, если люди думают, что этот ударный удар с std::string/getline является типичным и разумным.

Edit 4 (было: Final Edit/Solution):

Добавление:

cin.sync_with_stdio(false); 

Непосредственно над моим оригинальным время цикла выше результатов в коде, который работает быстрее, чем Python.

сравнение Новый спектакль (это на моем MacBook Pro 2011 года), используя исходный код, оригинал синхронизации отключены, и исходный код Python, соответственно, на файл с 20М строк текста. Да, я несколько раз запускал его, чтобы устранить дисковое кэширование.

$ /usr/bin/time cat test_lines_double | ./readline_test_cpp 
     33.30 real   0.04 user   0.74 sys 
Read 20000001 lines in 33 seconds. LPS: 606060 
$ /usr/bin/time cat test_lines_double | ./readline_test_cpp1b 
     3.79 real   0.01 user   0.50 sys 
Read 20000000 lines in 4 seconds. LPS: 5000000 
$ /usr/bin/time cat test_lines_double | ./readline_test.py 
     6.88 real   0.01 user   0.38 sys 
Read 20000000 lines in 6 seconds. LPS: 3333333 

Благодаря @Vaughn Cato за его ответ! Любые специалисты по разработке, которые могут сделать или могут быть полезными, могут указывать на то, почему происходит эта синхронизация, что это значит, когда это полезно, и когда это нормально отключать, было бы очень полезно потомству. :-)

Edit 5/лучшее решение:

Как было предложено Gandalf ниже The Gray, gets даже быстрее, чем scanf или несинхронизированного cin подхода. Я также узнал, что scanf и gets оба UNSAFE и НЕ ДОЛЖНЫ ИСПОЛЬЗОВАТЬСЯ из-за потенциала переполнения буфера. Итак, я написал эту итерацию, используя fgets, более безопасную альтернативу для получения. Вот соответствующие строки для моих коллег недоносков:

char input_line[MAX_LINE]; 
char *result; 

//<snip> 

while((result = fgets(input_line, MAX_LINE, stdin)) != NULL) 
    line_count++; 
if (ferror(stdin)) 
    perror("Error reading stdin."); 

Теперь, вот результаты, используя еще больший размер файла (100M линии; ~ 3,4   ГБ) на мощном сервере с очень быстрым диском, сравнивая код Python , несинхронизированный cin и fgets, а также сравнение с утилитой wc. [Сегментация scanf версии нарушенная, и я не чувствую себя диагностику проблем.]:

$ /usr/bin/time cat temp_big_file | readline_test.py 
0.03user 2.04system 0:28.06elapsed 7%CPU (0avgtext+0avgdata 2464maxresident)k 
0inputs+0outputs (0major+182minor)pagefaults 0swaps 
Read 100000000 lines in 28 seconds. LPS: 3571428 

$ /usr/bin/time cat temp_big_file | readline_test_unsync_cin 
0.03user 1.64system 0:08.10elapsed 20%CPU (0avgtext+0avgdata 2464maxresident)k 
0inputs+0outputs (0major+182minor)pagefaults 0swaps 
Read 100000000 lines in 8 seconds. LPS: 12500000 

$ /usr/bin/time cat temp_big_file | readline_test_fgets 
0.00user 0.93system 0:07.01elapsed 13%CPU (0avgtext+0avgdata 2448maxresident)k 
0inputs+0outputs (0major+181minor)pagefaults 0swaps 
Read 100000000 lines in 7 seconds. LPS: 14285714 

$ /usr/bin/time cat temp_big_file | wc -l 
0.01user 1.34system 0:01.83elapsed 74%CPU (0avgtext+0avgdata 2464maxresident)k 
0inputs+0outputs (0major+182minor)pagefaults 0swaps 
100000000 


Recap (lines per second): 
python:   3,571,428 
cin (no sync): 12,500,000 
fgets:   14,285,714 
wc:   54,644,808 

Как вы можете видеть, fgets лучше, но все еще довольно далека от унитазов исполнения; Я уверен, что это связано с тем, что wc проверяет каждый символ без копирования памяти. Я подозреваю, что в этот момент другие части кода станут узким местом, поэтому я не думаю, что оптимизация на этом уровне будет даже стоить даже, если это возможно (поскольку, в конце концов, мне действительно нужно хранить строки чтения в памяти).

Также обратите внимание, что небольшой компромисс с использованием char * буфера и fgets против unsynchronised cin нанизывать является то, что последний может читать строки любой длины, в то время как бывший требует ограничения ввода некоторого конечного числа. На практике это, вероятно, не проблема для чтения большинства входных файлов на основе строк, так как буфер может быть установлен на очень большое значение, которое не будет превышено действительным вводом.

Это была образовательная. Спасибо всем за ваши комментарии и предложения.

Edit 6:

Как было предложено J.F.Себастьян в комментариях ниже, утилита GNU wc использует обычную C read() (в оболочке safe-read.c) для чтения фрагментов (из 16k байт) за раз и подсчета новых строк. Вот Python эквивалент, основанный на коде компании JF (только с указанием соответствующего фрагмента кода, который заменяет цикл Python for:

BUFFER_SIZE = 16384 
count = sum(chunk.count('\n') for chunk in iter(partial(sys.stdin.read, BUFFER_SIZE), '')) 

Производительность этой версии довольно быстро (хотя и немного медленнее, чем полезность туалет сырье C , конечно):

$ /usr/bin/time cat temp_big_file | readline_test3.py 
0.01user 1.16system 0:04.74elapsed 24%CPU (0avgtext+0avgdata 2448maxresident)k 
0inputs+0outputs (0major+181minor)pagefaults 0swaps 
Read 100000000 lines in 4.7275 seconds. LPS: 21152829 

Опять же, это немного глупо для меня, чтобы сравнить C++ fgets/cin и первый код питона с одной стороны, к wc -l и этот последний Python фрагмент кода на другой, так как последние два на самом деле не хранят прочитанные строки, а просто пересчитывают символы новой строки. чтобы исследовать все различные реализации и подумать о последствиях производительности. Еще раз спасибо!

Edit 7: Крошечный тест добавление и резюмировать

Для полноты, я думал, обновить скорость чтения для одного файла на том же поле с оригиналом (синхронизированные) код 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 
+7

Проводили ли вы свои тесты несколько раз? Возможно, проблема с дисковым кэшем. 21 фев. 122012-02-21 02:20:14

  0

@ VaughnCato Да, и на двух разных машинах. 21 фев. 122012-02-21 02:22:39

  0

Попробуйте скопировать свой тестовый файл во второй отдельный файл, чтобы они были кэшированы отдельно. 21 фев. 122012-02-21 02:33:07

+5

@JJC: Я вижу две возможности (предполагая, что вы устранили проблему кэширования, предложенную Дэвидом): 1) '<iostream>' производительность отстой. Не в первый раз. 2) Python достаточно умен, чтобы не копировать данные в цикле for, потому что вы его не используете. Вы можете повторить попытку использования 'scanf' и' char [] '. В качестве альтернативы вы можете попробовать переписать цикл так, чтобы что-то было сделано со строкой (например, сохраните 5-ю букву и соедините ее в результате). 21 фев. 122012-02-21 02:35:47

+2

Еще одна вещь, это простой тест, который фактически не отражает производительность языка N и языка M. Python может иметь очень умную оптимизацию для этого точного случая, которого вы не получите при запуске полнофункционального приложения. 21 фев. 122012-02-21 02:39:59

+1

@JJC, вы должны, вероятно, использовать 'time.time()' вместо 'datetime.datetime.now(). Seconds'; вы получаете результаты с плавающей запятой и отсутствие возможности деления на ноль в вычислении LPS. Я получаю аналогичные результаты (python около 10 раз быстрее) на моей машине. 21 фев. 122012-02-21 02:43:58

  0

@ J.N. версия Python все еще намного быстрее, если я собираю символ из каждой строки. 21 фев. 122012-02-21 02:48:52

+1

@ J.N. Python определенно недостаточно умен, чтобы не копировать данные. Кроме того, файл нужно читать, чтобы увидеть, где находятся символы новой строки. 21 фев. 122012-02-21 03:04:03

  0

Насколько велики файлы? Если линии не очень длинны, я бы сомневался, что это может быть проблема кеширования. т.е. кеш не будет очищен/разбит между прогонами 21 фев. 122012-02-21 03:06:32

  0

@gnibbler: есть две копии, и вы можете сохранить один 21 фев. 122012-02-21 03:07:00

  0

. Я получаю разницу до 3-х с помощью fscanf. Это странно, потому что в какой-то момент python должен использовать базовый C API. 21 фев. 122012-02-21 03:08:21

  0

На самом деле, 2 раза при использовании большего файла (точность была слишком низкой с 1 секундой). 21 фев. 122012-02-21 03:14:10

  0

Возможно ли, что виртуальная машина python делает какое-то умное буферизированное чтение stdin при передаче ему файла, поэтому ему не нужно выходить в кэш диска для каждого чтения одной строки? 21 фев. 122012-02-21 03:14:20

+7

Проблема заключается в синхронизации с stdio - см. Мой ответ. 21 фев. 122012-02-21 03:30:51

  0

@brendanw, да, но так делают C и C++. Реализация C Python почти наверняка полагается на stdio для этого. В современной системе AFAIK существует множество уровней кэширования. 21 фев. 122012-02-21 03:39:29

+9

Поскольку никто, кажется, не упомянул, почему вы получаете дополнительную строку с C++: ** Не проверяйте на 'cin.eof()' !! ** Поместите вызов 'getline' в оператор 'if'. 21 фев. 122012-02-21 18:29:28

+14

'wc -l' является быстрым, потому что он считывает поток более чем по одной строке за раз (это может быть комбинация« fread (stdin)/memchr ('\ n') '). Результаты Python находятся в том же порядке, например, ['wc-l.py'] (http://ideone.com/Ri0ia) 27 фев. 122012-02-27 00:21:23

  0

@ J.F.Sebastian Спасибо за этот фрагмент кода на питон! Похоже, wc использует safe_read, который является просто оберткой вокруг обычного чтения и читает 16k за раз. Изменив ваш код на Python, чтобы использовать буфер 16k вместо 32k и запускать его на одном компьютере и тестовом файле, хруст занял 4 секунды (т. Е. 25 000 000 LPS). Спасибо! 28 фев. 122012-02-28 00:26:03

  0

Связанный вопрос: http://stackoverflow.com/questions/8310039/why-do-stdstring-operations-perform-poorly 11 мар. 122012-03-11 10:18:18

+1

Ответ на вопрос «почему мой ввод-вывод медленный?» почти всегда «буферизуется». 12 мар. 122012-03-12 14:46:09

+2

wee, знаменитый вопрос! Если вы хотите получить производительность на скорости wc, вы можете сделать это: не вызывать линейные функции, а готовые двоичные блоки и анализировать их по одному int за раз, логически маскируя их с помощью битовых масок XOR для символ новой строки. (это обычно приводит к меньшим однобайтным выборкам). кроме того, используйте встроенные функции stdio, чтобы указать свой буфер на один, который вы выделяете. вы можете проверить его непосредственно. есть больше voodoo, если вы хотите на самом деле злоупотреблять stdio, так что вы даже не приблизились к пределу, но вы, вероятно, на пределе диска, поэтому не стоит злоупотреблять им. 26 апр. 122012-04-26 04:24:45

  0

@ std''OrgnlDave Спасибо, эти советы звучат многообещающе! Если у вас есть/написать/найти какой-либо простой примерный код, реализующий некоторые из них, и отправить его как ответ (или хотя бы ссылку), я и будущие читатели этого вопроса будут очень благодарны за ваше обучение. Ура! 26 апр. 122012-04-26 09:00:42

  0

Для полноты, почему бы вам не добавить быструю версию python и аналогичный код на C++ в диаграмму внизу? Вы также можете рассмотреть возможность перемещения диаграммы вверх, так как люди могут не найти ее в довольно длинном посте. Интересно, действительно! 14 янв. 142014-01-14 08:46:52

  0

В соответствии с http://stackoverflow.com/questions/21107131/why-mesh-python-code-slower-than-decomposed-one вы можете дважды ускорить выполнение кода python, просто извлекая его в функцию. 14 янв. 142014-01-14 18:23:01

  0

@ThomasAhle, на какую «быструю версию python» вы имеете в виду? Спасибо за предложения, я скопирую диаграмму вверх. 15 янв. 142014-01-15 16:38:27

  0

@JCC Я имел в виду тот, который был изменен. 6 16 янв. 142014-01-16 08:41:53

  0

Также см. Мой вопрос о разделении строк на C++ vs Python ... аналогичная история скорости, где наивный подход медленнее в C++! Здесь: http://stackoverflow.com/q/9378500/379037 27 янв. 152015-01-27 17:53:00

+1

Это действительно очень образовательный. И еще раз подчеркнем тот факт, что C++ является мощным, но только тогда, когда в его использование ставится большая осторожность. 05 авг. 152015-08-05 03:36:33

  0

Если бы я был вами, я бы посмотрел на функцию mmap и memchr. Поскольку память не является проблемой, сопоставьте весь файл в вашей программе с помощью mmap, а затем обработайте с помощью memchr, чтобы выяснить «лимитные лимиты». А также fadvise, чтобы сказать ядро, что вы читаете последовательно 14 сен. 152015-09-14 15:33:29

  0

Nice сообщение. Но я хотел бы упомянуть, что проблему переполнения буфера с помощью scanf можно обработать, указав количество символов для чтения (для любого типа данных). См. Параметр ширины, упомянутый в [link] (http://www.cplusplus.com/reference/clibrary/cstdio/scanf/). В качестве примера: char s [10]; Scanf ("% 9s", с); // Это будет читать не более 9 символов из ввода.int x; scanf ("% 2d", &x); // Это будет читать 2-значный номер из ввода. (просто упоминание) Это может заботиться о переполнении буфера. Также нельзя указать динамическую ширину, но для преодоления этого можно просто сгенерировать the 11 мар. 122012-03-11 18:05:28

  0

Вы также можете исправить цикл C++. Этот дополнительный тест в цикле может быть дорогостоящим. 'while (getline (cin, input_line)) {line_count ++;}' 09 апр. 182018-04-09 16:14:26

1,134

По умолчанию 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.

+90

Это должно быть наверху. Это почти наверняка правильно. Ответ не может заключаться в замене чтения на вызов 'fscanf', потому что это довольно просто не работает так, как это делает Python. Python должен выделять память для строки, возможно, несколько раз, поскольку существующее распределение считается неадекватным - точно так же, как подход C++ с 'std :: string'. Эта задача почти наверняка связана с вводом-выводом, и слишком много FUD обойдется вокруг стоимости создания объектов 'std :: string' в C++ или с помощью' <iostream>' сам по себе. 21 фев. 122012-02-21 03:34:32

+32

Да, добавив эту строку непосредственно выше моего исходного цикла, цикл ускорил код, чтобы превзойти даже питон. Я собираюсь опубликовать результаты в качестве окончательного редактирования. Еще раз спасибо! 21 фев. 122012-02-21 03:45:25

+5

Я не мог найти хорошую ссылку, но я добавил некоторые объяснения, основанные на моем собственном понимании. 21 фев. 122012-02-21 04:34:19

  0

@VaughnCato Оказывается, fgets еще быстрее, см. Мое Редактирование 5. Тем не менее, ваше решение очень полезно, особенно. когда нужно использовать cin и писать в строковый объект, например. в контексте, где одна строка может быть намного длиннее, чем ожидалось, и где маршрут fgets -> char buffer [MAXLINE] приведет к усечению. Благодарю. 22 фев. 122012-02-22 11:47:50

+1

100-й верх и золотой значок для вас хороший сэр. 11 мар. 122012-03-11 07:38:51

  0

Что относительно вывода/печати? потому что я чувствую, что счет слишком медленный, это то же самое? 11 мар. 122012-03-11 12:31:20

+4

Да, это действительно относится к cout, cerr и clog. 11 мар. 122012-03-11 13:56:11

+2

Чтобы сделать cout, cin, cerr и clog быстрее, сделайте это так: std :: ios_base :: sync_with_stdio (false); 11 мар. 122012-03-11 14:17:35

  0

Рад, что я столкнулся с этим вопросом. Просто помните, что если вы используете решение cin.sync_with_stdio (false), никакая другая часть вашей программы не должна читать из stdin! Отличную статью о синхронизации потока можно найти по адресу http://drdobbs.com/184401305 13 мар. 122012-03-13 17:09:56

  0

С радостью буду записывать одно из моих ежедневных голосов за это, и я даже не приехал сюда на поиски, просто увидел это в возможно связанном списке из совершенно произвольного вопроса.Это * невероятно * полезно для людей, которые делают чувствительный к скорости вход 'cin' и хотят использовать преимущества буферизации. Большое вам спасибо, Вон. Потрясающий ответ. 24 июн. 132013-06-24 20:06:48

+25

Обратите внимание, что 'sync_with_stdio()' является функцией статического члена, и вызов этой функции для любого объекта потока (например, 'cin') включает или выключает синхронизацию для * всех * стандартных объектов iostream. 21 янв. 152015-01-21 01:16:26

  0

@gjpc Wrong; если мое понимание синхронизации правильное, вы по-прежнему свободны делать то, что хотите, если оно полагается на 'std :: cin.rdbuf()'. Вы просто не можете использовать 'stdin' cstdio. 08 апр. 172017-04-08 15:34:15

  0

(_p.s._ или 'std :: wcin'.) 08 апр. 172017-04-08 15:44:31


4

Первый элемент ответа: <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; 
} 
  0

Не видел этого сообщения, пока не сделал третью правку, но еще раз спасибо за ваше предложение. Как ни странно, для меня нет бита 2x для python теперь с помощью строки scanf в edit3 выше. Кстати, я использую 2,7. 21 фев. 122012-02-21 03:32:48

+7

После исправления версии C++ эта версия stdio существенно медленнее, чем версия C++ iostreams на моем компьютере.(3 секунды против 1 секунды) 21 фев. 122012-02-21 03:39:13

+3

То же самое здесь. Синкрой для stdio был трюк. 21 фев. 122012-02-21 04:08:57

  0

fgets еще быстрее; см. править 5 выше. Благодарю. 22 фев. 122012-02-22 11:49:20


5

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

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


64

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

+3

Возможно, вы захотите попробовать различные размеры буфера, чтобы получить более полезную информацию. Я подозреваю, что вы увидите быстро убывающие результаты. 21 фев. 122012-02-21 03:37:59

+6

Я был слишком поспешным в своем ответе; установка размера буфера на что-то иное, чем значение по умолчанию, не вызывало заметной разницы. 21 фев. 122012-02-21 03:51:45

+80

Я бы также избежал установки буфера 1 МБ в стеке. Это может привести к stackoverflow (хотя я думаю, что это хорошее место для обсуждения!) 21 фев. 122012-02-21 07:30:30

+8

Matthieu, Mac по умолчанию использует 8MB стек процесса. Linux использует 4MB на поток по умолчанию, IIRC. 1MB - не такая уж большая проблема для программы, которая преобразует ввод с относительно неглубокой глубиной стека. Что еще более важно, std :: cin удалит стек, если буфер выходит за рамки. 14 янв. 142014-01-14 09:28:54

+17

@SEK Размер стека Windows по умолчанию равен 1 МБ. 15 мар. 142014-03-15 02:11:35

+1

Следует отметить, что эффекты pubsetbuf на буфере cin не стандартизированы. Реализация может действительно использовать предоставленный буфер, игнорировать его (что было бы унаследованным действием по умолчанию из std :: basic_streambuf) или, возможно, даже сделать что-то еще. См. Также http://stackoverflow.com/questions/12481463/stringstream-rdbuf-pubsetbuf-is-not-setting-the-buffer 09 июн. 152015-06-09 18:10:23

+1

Чтобы выделить «более важно» @SEK: комментарий: если буфер находится в стеке, вы не можете позволить функции возвращаться, пока файл не будет закрыт, или использование буфера в противном случае прекращено. 22 сен. 172017-09-22 22:10:38


2

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

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

+4

Отлично! Хотя get() плохой (мы все должны использовать fgets вместо этого, чтобы помешать хаксорам), я реализовал использование fgets и вижу гораздо лучшую производительность. Посмотрите мое последнее изменение (5) выше. Спасибо за указание на это! 22 фев. 122012-02-22 11:22:36

+51

Вы должны *********** НИКОГДА не используйте ********* 'gets'. Неограниченный вход буфера является серьезной проблемой, и 'get' является/является основным источником. 11 мар. 122012-03-11 07:44:46

+7

Никогда не используйте get, если вы не хотите использовать уязвимость переполнения буфера в своей программе. GCC предупреждает о том, что вы попадете во время компиляции, даже если у вас нет установленных предупреждающих флагов. 11 мар. 122012-03-11 07:55:37

+1

Вы, парни, абсолютно правы. Я предложил 'get', поскольку это был простой бенчмаркинг, а не производственный код, но мой ответ мог заставить неопытных программистов использовать' gets' в производственном коде.Я отредактировал его, чтобы предложить безопасную версию (' fgets'). Спасибо большое! 30 авг. 172017-08-30 00:17:39


7

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

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

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

Действительно правильный цикл будет: 'while (getline (cin, input_line)) line_count ++;' 05 май. 122012-05-05 14:42:01


87

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

15

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(&buffer[0], length); 

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


4

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


44

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

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

$ /usr/bin/time cat big_file | program_to_benchmark 

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

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

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

Лучше строительство будет:

$ /usr/bin/time program_to_benchmark < big_file 

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

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

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

$ cat big_file | /usr/bin/time program_to_benchmark 

Б. или по времени весь трубопровод:

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

Они неправы по тем же причинам, что и # 2: they'r e по-прежнему без использования `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 

займет всего лишь 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% больше CPU используемый! На моем случайном ящике-есть-в-время-тест.

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

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

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

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

+6

Ничего себе, это было довольно проницательно! В то время как я знал, что кошка не нужна для подачи ввода в stdin программ и что предпочтительным является перенаправление <shell redirect, я обычно придерживался cat из-за потока данных слева направо, которое прежний метод сохраняет визуально когда я рассуждаю о трубопроводах. Различия в производительности в таких случаях я обнаружил, что они незначительны. Но я ценю ваше образование, Бела. 09 май. 172017-05-09 01:16:09

+1

Я лично воздержусь от передовой, так как это не касается первоначального вопроса (обратите внимание, что использование кошки постоянно в конкурирующих примерах). Но, опять же, спасибо за интеллектуальную дискуссию о входе и выходе * nix. 09 май. 172017-05-09 01:18:24

+4

Перенаправление выведено из командной строки на ранней стадии, что позволяет вам сделать один из них, если он дает более приятный вид потока слева направо: '$ <большое_имя_файла my_program' ' $ time <big_file my_program' Это должно работать в любой оболочке POSIX (т. е. не \ 'csh \', и я не уверен в том, что exotica нравится '' rc \ ':) 10 май. 172017-05-10 21:55:01

+3

Опять же, помимо, возможно, неинтересной инкрементной разницы в производительности из-за бинарного запуска '' cat \ 'в то же время, вы отказываетесь от возможности тестируемой программы иметь возможность mmap() входного файла. Это может существенно повлиять на результаты. Это верно, даже если вы сами написали тесты, на разных языках, используя только свои «входные строки из файла». Это зависит от детальной работы их различных библиотек ввода-вывода. 10 май. 172017-05-10 22:01:02