Dlaczego czytanie wierszy ze standardowego wejścia jest znacznie wolniejsze w C++ niż w Pythonie?

Chciałem porównać odczyt linii znaków ze standardowego wejścia przy użyciu Pythona i C++ i byłem zszokowany widząc, że mój kod C++ działa o rząd wielkości wolniej niż odpowiedni kod Pythona. Ponieważ mój C++ jest zardzewiały i nie jestem jeszcze ekspertem Pythonistą, proszę mi powiedzieć, czy robię coś źle lub czy coś nie rozumiem.


(TLDR answer: include the statement: cin.sync_with_stdio(false) or just use fgets zamiast tego.

Wyniki TLDR: przewiń do końca mojego pytania spójrz na stół.)


Kod 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

Odpowiednik Pythona:

#!/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))

Oto moje wyniki:

$ 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

powinienem zauważyć, że próbowałem tego zarówno pod Mac OS X v10.6.8 (Snow Leopard) i Linux 2.6.32 (Red Hat Linux 6.2). Pierwszy to MacBook Pro, a drugi to bardzo wytrzymały serwer, nie żeby to było zbyt istotne.

$ 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

Mały benchmark dodatek i podsumowanie

Dla kompletności, I pomyślałem, że zaktualizuję prędkość odczytu tego samego pliku na tym samym pudełku za pomocą oryginalnego (zsynchronizowanego) kodu C++. Ponownie, jest to dla pliku linii 100m na szybkim dysku. Oto porównanie, z kilkoma rozwiązaniami/podejściami:

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
Author: JJC, 2012-02-21

10 answers

Domyślnie, {[2] } jest zsynchronizowane ze stdio, co powoduje, że nie powoduje buforowania danych wejściowych. Jeśli dodasz to do góry głównej, powinieneś zobaczyć znacznie lepszą wydajność:

std::ios_base::sync_with_stdio(false);

Normalnie, gdy strumień wejściowy jest buforowany, zamiast odczytywać jeden znak na raz, strumień będzie odczytywany w większych fragmentach. Zmniejsza to liczbę wywołań systemowych, które są zazwyczaj stosunkowo drogie. Ponieważ jednak FILE* oparte stdio i iostreams często mają oddzielne implementacje, a więc oddzielne bufory, może to prowadzić do problemu, jeśli oba zostaną użyte razem. Na przykład:

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

Jeśli przez cin odczytano więcej danych wejściowych niż faktycznie było to potrzebne, wtedy druga wartość całkowita nie byłaby dostępna dla funkcji scanf, która ma własny niezależny bufor. Doprowadziłoby to do nieoczekiwanych rezultatów.

Aby tego uniknąć, domyślnie strumienie są synchronizowane z stdio. Jednym z powszechnych sposobów, aby to osiągnąć, jest to, że cin czyta każdy znak pojedynczo w razie potrzeby za pomocą funkcji stdio. Niestety, wprowadza to wiele kosztów. W przypadku niewielkich ilości danych wejściowych nie jest to duży problem, ale gdy czytasz miliony linii, kara wydajności jest znacząca.

Na szczęście projektanci bibliotek zdecydowali, że powinieneś również być w stanie wyłączyć tę funkcję, aby uzyskać lepszą wydajność, jeśli wiesz, co robisz, więc dostarczyli sync_with_stdio metoda.

 1341
Author: Vaughn Cato,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-30 02:38:39

Z ciekawości przyjrzałem się temu, co dzieje się pod maską i użyłem dtruss/strace w każdym teście.

C++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

Syscalls 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

Syscalls sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29
 127
Author: 2mia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-02 18:27:57

Jestem kilka lat w tyle, ale:

W 'Edit 4/5/6' oryginalnego postu używasz konstrukcji:

$ /usr/bin/time cat big_file | program_to_benchmark

To jest złe na kilka różnych sposobów:

  1. Właściwie mierzysz czas na wykonanie "kota", a nie twój punkt odniesienia. Użycie procesora' user 'i' sys `wyświetlane przez` time 'to użycie' cat`, a nie Twojego programu porównawczego. Co gorsza, czas "rzeczywisty" również niekoniecznie jest dokładny. W zależności od implementacji " cat " i potoki w Twoim lokalnym systemie operacyjnym jest możliwe, że` cat ' zapisze ostateczny gigantyczny bufor i zakończy pracę na długo przed zakończeniem pracy programu reader.

  2. Użycie " cat` jest niepotrzebne i w rzeczywistości przynosi efekty odwrotne do zamierzonych-dodajesz ruchome części. Jeśli korzystasz z wystarczająco starego systemu (tj. z pojedynczym procesorem i-w niektórych generacjach komputerów-I/O szybszym niż procesor) - sam fakt, że` cat ' był uruchomiony, może znacznie pokolorować wyniki. Jesteś również przedmiotem tego, co może to być buforowanie wejść i wyjść oraz inne przetwarzanie "cat". (To prawdopodobnie zarobić "bezużyteczne użycie kota" nagroda gdybym był Randal Schwartz.

Lepszą konstrukcją byłoby:

$ /usr/bin/time program_to_benchmark < big_file

W tej instrukcji jest to powłoka , która otwiera plik big_file, przekazując go do twojego programu (właściwie do `time`, który następnie wykonuje Twój program jako podproces) jako już otwarty deskryptor pliku. 100% odczytu pliku jest ściśle obowiązkiem program, który próbujesz porównać. To daje prawdziwy odczyt jego wydajności bez fałszywych komplikacji.

Wspomnę o dwóch możliwych, ale właściwie błędnych "poprawkach", które również można rozważyć (ale "numeruję" je inaczej, ponieważ nie są to rzeczy, które były złe w oryginalnym poście): {]}

A. Możesz to 'naprawić' tylko przez czas programu:

$ cat big_file | /usr/bin/time program_to_benchmark

B. lub przez czas całego rurociągu:

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

Są złe z tych samych powodów co # 2: nadal niepotrzebnie używają "kota". Wspominam je z kilku powodów:

  • Są one bardziej "naturalne" dla osób, które nie są w pełni komfortowe z obiektami przekierowywania We/Wy powłoki POSIX

  • Mogą się zdarzyć przypadki, w których potrzebny jest `cat` (np.: plik do odczytu wymaga pewnego rodzaju uprawnień dostępu, a nie chcesz przyznać tego uprawnienia programowi, który ma być oznaczony jako benchmarked: 'sudo cat / dev / sda / / usr / bin / time my_compression_test -- no-output`)

  • w praktyce, na nowoczesnych maszynach, dodany `cat` w rurociągu prawdopodobnie nie ma rzeczywistego znaczenia

[7]}Ale mówię to ostatnie z pewnym wahaniem. Jeśli sprawdzimy ostatni wynik w 'Edit 5' --
$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

-- to twierdzi, że " cat " zużywał 74% procesora podczas testu; i rzeczywiście 1.34/1.83 wynosi około 74%. Być może run of:

$ /usr/bin/time wc -l < temp_big_file
/ Align = "left" / 49 sekund! Prawdopodobnie nie.: 'cat' musiał tu zapłacić za wywołania systemowe read () (lub równoważne), które przenosiły Plik z 'disk' (właściwie bufor cache), jak również pipe zapisuje, aby dostarczyć je do `wc`. Poprawny test nadal musiałby wykonać te wywołania read (); tylko wywołania write-to-pipe I read-from-pipe zostałyby zapisane, a te powinny być dość tanie.

Mimo to przewiduję, że będziesz w stanie zmierzyć różnicę między `plik cat | wc-l` i `plik wc-l

W rzeczywistości zrobiłem kilka szybkich testów z 1,5 gigabajtowym plikiem śmieci, na systemie Linux 3.13 (Ubuntu 14.04), uzyskując te wyniki (są to faktycznie wyniki 'best of 3'; po zagruntowaniu pamięci podręcznej, oczywiście): {]}

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

Zauważ, że dwa wyniki potoków twierdzą, że zajmowały więcej czasu procesora (user + sys) niż w czasie rzeczywistym. Dzieje się tak dlatego, że używam wbudowanego polecenia "time" powłoki (Bash), które jest znane z potoku; i jestem na maszynie wielordzeniowej, gdzie oddzielne procesy w potoku mogą używać oddzielnych rdzeni, gromadząc czas procesora szybciej niż w czasie rzeczywistym. Używając /usr / bin / time widzę mniejszy czas procesora niż realtime -- pokazując, że może on tylko mierzyć czas przekazania do niego pojedynczego elementu potoku w wierszu poleceń. Ponadto, wyjście powłoki daje milisekundy, podczas gdy /usr/bin/time tylko daje setne sekundy.

Więc przy poziomie wydajności "wc-l", " cat " robi ogromną różnicę: 409 / 283 = 1.453 lub 45.3% więcej czasu rzeczywistego, a 775 / 280 = 2.768, czyli aż o 177% więcej procesora! Na moim losowym pudełku testowym.

Powinienem dodać, że istnieje co najmniej jedna znacząca różnica między tymi stylami testowania i nie mogę powiedzieć, czy jest to korzyść, czy wina; musisz o tym zdecydować sam: {]}

When you run ' cat big_file | /usr / bin / time my_program', Twój program odbiera dane wejściowe z rury, dokładnie w tempie wysłanym przez 'cat' i w kawałkach nie większych niż napisane przez ' cat`.

Po uruchomieniu '/usr/bin / time my_program lub w wielu przypadkach biblioteki I / o języka, w którym został napisany -- mogą podejmować różne działania, gdy są przedstawione z deskryptorem pliku odwołującym się do zwykłego pliku. Może używać mmap(2) mapuje plik wejściowy do jego przestrzeni adresowej, zamiast jawnych wywołań systemowych read(2). Różnice te mogą mieć znacznie większy wpływ na wyniki testów porównawczych niż niewielki koszt uruchomienia binarnego "cat".

Oczywiście jest to interesujący wynik benchmarka, jeśli ten sam program działa znacznie inaczej między dwoma przypadkami. Pokazuje ono, że program lub jego biblioteki We /Wy robią coś interesującego, na przykład używając mmap (). Więc w praktyka może być dobrze uruchomić benchmarki w obie strony; być może dyskontując wynik "kota" przez jakiś mały czynnik, aby "wybaczyć" koszt prowadzenia samego "kota".

 96
Author: Bela Lubkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-05 21:47:30

Odtworzyłem oryginalny wynik na moim komputerze używając g++ na Macu.

Dodanie następujących instrukcji do wersji C++ tuż przed pętlą while wprowadza ją do wersji Python :

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

Sync_with_stdio poprawiło prędkość do 2 sekund, a ustawienie większego bufora zmniejszyło ją do 1 sekundy.

 78
Author: karunski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-02 18:26:00

getline, operatory strumieniowe, scanf, mogą być wygodne, jeśli nie zależy ci na czasie ładowania plików lub jeśli ładujesz małe pliki tekstowe. Ale jeśli wydajność jest czymś, na czym Ci zależy, powinieneś po prostu buforować cały plik do pamięci (zakładając, że będzie pasował).

Oto przykład:

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

Jeśli chcesz, możesz owinąć strumień wokół tego bufora, aby uzyskać wygodniejszy dostęp w następujący sposób:

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

Ponadto, jeśli masz kontrolę nad plikiem, rozważ użycie płaskiego binarny format danych zamiast tekstu. Jest bardziej niezawodny w czytaniu i pisaniu, ponieważ nie musisz radzić sobie ze wszystkimi niejednoznacznymi białymi znakami. Jest również mniejszy i znacznie szybszy do analizy.

 27
Author: Stu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-28 17:55:09

Nawiasem mówiąc, powodem, dla którego liczba wierszy dla wersji C++ jest o jeden większa od liczby dla wersji Pythona jest to, że flaga eof jest ustawiana tylko wtedy, gdy podejmowana jest próba odczytania poza eof. Więc prawidłową pętlą będzie:

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

    if (!cin.eof())
        line_count++;
};
 11
Author: Gregg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-12 04:04:01

W drugim przykładzie (ze scanf()) powodem, dla którego jest to nadal wolniejsze, może być to, że scanf("%s") parsuje łańcuch znaków i szuka dowolnego znaku spacji (spacja, tabulator, znak nowej linii).

Również, tak, CPython wykonuje buforowanie, aby uniknąć odczytu dysku twardego.

 10
Author: davinchi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-21 03:32:17

Poniższy kod był dla mnie szybszy niż inny kod zamieszczony tutaj do tej pory: (Visual Studio 2013, 64-bitowy, 500 MB pliku o długości linii równomiernie w [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'; });
}
To bije wszystkie moje próby Pythona o więcej niż czynnik 2.
 10
Author: Petter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-23 14:56:24

Pierwszy element odpowiedzi: <iostream> jest powolny. Cholernie wolno. Dostaję ogromny wzrost wydajności z scanf jak w poniższym, ale nadal jest dwa razy wolniejszy niż 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;
}
 8
Author: J.N.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-02 18:25:14

Widzę, że w twoim drugim rozwiązaniu przełączyłeś się z cin Na scanf, co było pierwszą sugestią, którą ci zaproponowałem(cin to slooooooooooooooo). Teraz, jeśli przełączysz się z scanf na fgets, zobaczysz kolejny wzrost wydajności: {[3] } jest najszybszą funkcją C++ do wprowadzania ciągów.

BTW, nie wiedziałem o tej synchronizacji, fajnie. Ale powinieneś spróbować fgets.
 7
Author: José Ernesto Lara Rodríguez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-30 00:16:25