Chciałem porównać odczytane wiersze wejściowych ciągów ze standardowego wejścia przy użyciu Pythona i C ++ i byłem zszokowany, gdy mój kod C ++ działał o rząd wielkości wolniej niż równoważny kod Pythona. Ponieważ mój C ++ jest zardzewiały i nie jestem jeszcze ekspertem Pythonista, proszę powiedz mi, czy robię coś źle lub coś źle rozumiem.
(Odpowiedź TLDR: dołącz instrukcję: cin.sync_with_stdio(false)
lub po prostu użyj fgets
zamiast tego.
Wyniki TLDR: przewiń do samego końca pytania i spójrz na tabelę).
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 w systemie Mac OS X 10.6.8 (Snow Leopard), jak i Linux 2.6.32 (Red Hat Linux 6.2). Ten pierwszy to MacBook Pro, a ten drugi to bardzo mocny serwer, ale nie jest to 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łe uzupełnienie i podsumowanie testu porównawczego
Dla kompletności pomyślałem, że zaktualizuję prędkość odczytu tego samego pliku w tym samym pudełku z oryginalnym (zsynchronizowanym) kodem C ++. Ponownie, dotyczy to pliku liniowego 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
<iostream>
wydajność jest do kitu. Nie pierwszy raz to się dzieje. 2) Python jest wystarczająco sprytny, aby nie kopiować danych w pętli for, ponieważ go nie używasz. Możesz ponownie przetestować, próbując użyćscanf
ichar[]
. Alternatywnie możesz spróbować przepisać pętlę, aby coś zostało zrobione z łańcuchem (np. Zachowaj piątą literę i połącz ją w wyniku).cin.eof()
!! Umieśćgetline
połączenie w instrukcji „if”.wc -l
jest szybki, ponieważ odczytuje strumień więcej niż jedną linię na raz (może to byćfread(stdin)/memchr('\n')
kombinacja). Wyniki w Pythonie są tego samego rzędu wielkości, np.wc-l.py
Odpowiedzi:
Domyślnie
cin
jest synchronizowany ze stdio, co powoduje, że unika buforowania danych wejściowych. Jeśli dodasz to do górnej części głównej, powinieneś zobaczyć znacznie lepszą wydajność:Zwykle, gdy strumień wejściowy jest buforowany, zamiast odczytywania jednego znaku na raz, strumień będzie odczytywany w większych fragmentach. Zmniejsza to liczbę wywołań systemowych, które zwykle są stosunkowo drogie. Ponieważ jednak
FILE*
bazowestdio
iiostreams
często mają osobne implementacje, a zatem osobne bufory, może to prowadzić do problemu, jeśli oba zostaną użyte razem. Na przykład:Jeśli odczytano więcej danych wejściowych,
cin
niż było to potrzebne, wówczas druga wartość całkowita nie byłaby dostępna dlascanf
funkcji, która ma swój własny niezależny bufor. Doprowadziłoby to do nieoczekiwanych rezultatów.Aby tego uniknąć, domyślnie strumienie są synchronizowane
stdio
. Jednym z powszechnych sposobów osiągnięcia tego celu jestcin
odczytywanie każdego znaku po kolei za pomocąstdio
funkcji. Niestety wprowadza to wiele kosztów ogólnych. W przypadku niewielkiej ilości danych wejściowych nie jest to duży problem, ale gdy czytasz miliony wierszy, znaczna jest utrata wydajności.Na szczęście projektanci bibliotek zdecydowali, że powinieneś być w stanie wyłączyć tę funkcję, aby uzyskać lepszą wydajność, jeśli wiesz, co robisz, więc podali tę
sync_with_stdio
metodę.źródło
fscanf
wywołaniem, ponieważ to po prostu nie działa tak dobrze, jak Python. Python musi przydzielić pamięć dla ciągu, być może wiele razy, ponieważ istniejący przydział jest uważany za nieodpowiedni - dokładnie tak jak w przypadku C ++std::string
. To zadanie jest prawie na pewno związane z operacjami we / wy i jest zbyt wiele FUD dotyczących kosztów tworzeniastd::string
obiektów w C ++ lub używania<iostream>
samego w sobie.sync_with_stdio()
jest to statyczna funkcja członka, a wywołanie tej funkcji na dowolnym obiekcie strumienia (np.cin
) Włącza lub wyłącza synchronizację dla wszystkich standardowych obiektów iostream.Właśnie z ciekawości przyjrzałem się, co dzieje się pod maską, i użyłem dtruss / strace na każdym teście.
C ++
wywołania systemowe
sudo dtruss -c ./a.out < in
Pyton
wywołania systemowe
sudo dtruss -c ./a.py < in
źródło
Jestem tu kilka lat wstecz, ale:
W „Edycji 4/5/6” oryginalnego postu używasz konstrukcji:
Jest to złe na kilka różnych sposobów:
Rzeczywiście mierzysz czas wykonania
cat
, a nie testu. Wyświetlane przez użytkownika użycie procesora przez „użytkownika” i „sys” oznacza użycietime
programucat
, a nie programu testowego. Co gorsza, „czas rzeczywisty” niekoniecznie jest dokładny. W zależności od implementacjicat
i potoków w lokalnym systemie operacyjnym możliwe jest, żecat
zapisze ostateczny gigantyczny bufor i zostanie zamknięty na długo przed zakończeniem pracy czytnika.Używanie
cat
jest niepotrzebne i faktycznie przynosi efekt przeciwny do zamierzonego; dodajesz ruchome części. Jeśli korzystasz z wystarczająco starego systemu (tj. Z jednym procesorem i - w niektórych generacjach komputerów - I / O szybszym niż procesor) - sam fakt, którycat
działał, może znacznie pokolorować wyniki. Podlegasz również wszelkiemu buforowaniu danych wejściowych i wyjściowych oraz innego przetwarzaniacat
. (Prawdopodobnie dałbym ci nagrodę „Bezużyteczne użytkowanie kota”, gdybym był Randalem Schwartzem.Lepszą konstrukcją byłoby:
W tej instrukcji jest to powłoka, która otwiera duży_plik, przekazując go do twojego programu (cóż, właściwie do
time
którego następnie wykonuje twój program jako podproces) jako już otwarty deskryptor pliku. 100% odczytu pliku jest ściśle obowiązkiem programu, który próbujesz przetestować. Dzięki temu możesz naprawdę odczytać jego wydajność bez fałszywych komplikacji.Wspomnę o dwóch możliwych, ale w rzeczywistości błędnych „poprawkach”, które można również wziąć pod uwagę (ale „numeruję” je inaczej, ponieważ nie są to rzeczy, które były złe w oryginalnym poście):
A. Możesz to naprawić, mierząc czas tylko swojego programu:
B. lub mierząc czas całego rurociągu:
Są one błędne z tych samych powodów, co # 2: nadal używają
cat
niepotrzebnie. Wspominam o nich z kilku powodów:są bardziej „naturalne” dla osób, które nie do końca czują się dobrze z funkcjami przekierowywania we / wy powłoki POSIX
mogą być przypadki, gdzie
cat
jest potrzebne (np: plik do odczytu wymaga pewnego rodzaju przywilej dostępu, i nie chcą przyznać, że uprawnienie do programu należy odwzorować:sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output
)w praktyce na nowoczesnych maszynach dodanie
cat
do rurociągu prawdopodobnie nie ma rzeczywistych konsekwencji.Ale mówię to z wahaniem. Jeśli sprawdzimy ostatni wynik w „Edycji 5” -
- twierdzi, że
cat
zużywa 74% procesora podczas testu; i faktycznie 1,34 / 1,83 wynosi około 74%. Być może seria:zajęłoby tylko pozostałe 0,49 sekundy! Prawdopodobnie nie:
cat
tutaj trzeba było zapłacić zaread()
wywołania systemowe (lub równoważne), które przeniosły plik z „dysku” (faktycznie bufor pamięci podręcznej), a także zapisy potoku, aby je dostarczyćwc
. Właściwy test nadal musiałby wykonywać teread()
połączenia; zapisywane byłyby tylko wywołania zapisu do potoku i odczytu z potoku, a te powinny być dość tanie.Mimo to przewidywania będzie można zmierzyć różnicę między
cat file | wc -l
awc -l < file
i znaleźć zauważalny (2-cyfrowy) procentową różnicę. Każdy wolniejszy test zapłaci podobną karę w czasie bezwzględnym; co jednak stanowiłoby mniejszy ułamek jego większego całkowitego czasu.W rzeczywistości zrobiłem kilka szybkich testów z plikiem śmieci 1,5 gigabajta, na systemie Linux 3.13 (Ubuntu 14.04), uzyskując te wyniki (w rzeczywistości są to wyniki „najlepsze z 3”; oczywiście po zainicjowaniu pamięci podręcznej):
Zauważ, że wyniki dwóch potoków twierdzą, że zajęły więcej czasu procesora (użytkownik + sys) niż rzeczywisty czas zegara ściennego. Jest tak, ponieważ używam wbudowanego polecenia powłoki (bash) „time”, które rozpoznaje potok; i jestem na wielordzeniowym komputerze, na którym osobne procesy w potoku mogą korzystać z oddzielnych rdzeni, akumulując czas procesora szybciej niż w czasie rzeczywistym. Używając
/usr/bin/time
widzę mniejszy czas procesora niż w czasie rzeczywistym - pokazując, że może on tylko zmierzyć czas pojedynczego elementu potoku przekazanego do niego w wierszu poleceń. Ponadto, wynik powłoki daje milisekundy, a/usr/bin/time
jedynie setne sekundy.Tak na poziomie sprawności
wc -l
Thecat
robi ogromną różnicę: 409/283 = 1,453 lub 45,3% więcej w czasie rzeczywistym, a 775/280 = 2.768, czyli aż o 177% więcej CPU używane! Na moim losowym pudełku testowym „był tam, kiedy był”.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 sam to zdecydować:
Kiedy uruchamiasz
cat big_file | /usr/bin/time my_program
, twój program odbiera dane wejściowe z potoku, dokładnie w tempie wysyłanym przezcat
i w kawałkach nie większych niż napisane przezcat
.Po uruchomieniu
/usr/bin/time my_program < big_file
program otrzymuje otwarty deskryptor pliku do rzeczywistego pliku. Twój program - lub w wielu przypadkach biblioteki I / O języka, w którym został napisany - może podejmować różne działania, gdy zostanie przedstawiony deskryptor pliku odnoszący się do zwykłego pliku. Może użyćmmap(2)
do mapowania pliku wejściowego do jego przestrzeni adresowej, zamiast używania jawnychread(2)
wywołań systemowych. Różnice te mogą mieć znacznie większy wpływ na wyniki testu porównawczego niż niewielki koszt uruchomienia plikucat
binarnego.Oczywiście jest to interesujący wynik testu porównawczego, jeśli ten sam program działa znacząco różnie w obu przypadkach. To pokazuje, że rzeczywiście, program lub jego biblioteki I / O są robi coś ciekawego, jak przy użyciu
mmap()
. Tak więc w praktyce może być dobrze przeprowadzić testy porównawcze w obie strony; być może pomniejszająccat
wynik o jakiś drobny czynnik, aby „wybaczyć”cat
samemu sobie koszty .źródło
$ < big_file time my_program
$ time < big_file my_program
Powinno to działać w dowolnej powłoce POSIX (tj. Nie w `csh `i nie jestem pewien co do egzotyki takiej jak` rc`:)time
mierzy cały potok zamiast pierwszego programu.time seq 2 | while read; do sleep 1; done
drukuje 2 sek.,/usr/bin/time seq 2 | while read; do sleep 1; done
drukuje 0 sek.Odtworzyłem oryginalny wynik na moim komputerze, używając g ++ na komputerze Mac.
Dodanie następujących instrukcji do wersji C ++ tuż przed tym, jak
while
pętla dostosuje ją do wersji Python :sync_with_stdio poprawił prędkość do 2 sekund, a ustawienie większego bufora sprowadziło ją do 1 sekundy.
źródło
getline
, operatorzy strumieniowiscanf
, może być wygodny, jeśli nie obchodzi Cię czas ładowania pliku lub jeśli ładujesz małe pliki tekstowe. Ale jeśli zależy Ci na wydajności, powinieneś po prostu buforować cały plik do pamięci (zakładając, że będzie pasował).Oto przykład:
Jeśli chcesz, możesz owinąć strumień wokół tego bufora, aby uzyskać bardziej wygodny dostęp:
Ponadto, jeśli masz kontrolę nad plikiem, rozważ użycie płaskiego binarnego formatu danych zamiast tekstu. Czytanie i pisanie jest bardziej niezawodne, ponieważ nie musisz radzić sobie z wszystkimi dwuznacznościami białych znaków. Jest także mniejszy i znacznie szybszy do analizy.
źródło
Poniższy kod był dla mnie szybszy niż inny kod opublikowany tutaj tutaj: (Visual Studio 2013, 64-bitowy, 500 MB plik o długości linii równomiernie w [0, 1000)).
Pokonuje wszystkie moje próby w Pythonie ponad dwa razy.
źródło
read
przekształca niebuforowane syscalli w statyczny bufor długościBUFSIZE
lub poprzez równoważne odpowiadające immmap
syscalli, a następnie przesuwa się przez ten bufor licząc nowe linie à lafor (char *cp = buf; *cp; cp++) count += *cp == "\n"
. Musisz jednak dostroićBUFSIZE
system, co stdio już dla ciebie zrobił. Ale tafor
pętla powinna się skompilować do niesamowicie szybkich instrukcji asemblera w języku sprzętowym twojego urządzenia.Nawiasem mówiąc, powodem, dla którego liczba wierszy dla wersji C ++ jest większa niż liczba dla wersji Pythona, jest to, że flaga eof jest ustawiana tylko wtedy, gdy próba odczytu odbywa się poza eof. Tak więc poprawna pętla to:
źródło
while (getline(cin, input_line)) line_count++;
++line_count;
i nieline_count++;
.long
, a kompilator jest w stanie stwierdzić, że wynik przyrostu nie jest używany. Jeśli nie generuje identycznego kodu dla postinkrementu i preinkrementacji, jest zepsuty.++line_count;
zamiastline_count++;
nie zaszkodziłoby :)while
, prawda? Czy miałoby to znaczenie, gdyby wystąpił jakiś błąd i chciałbyś się upewnić, żeline_count
jest poprawny? Po prostu zgaduję, ale nie rozumiem, dlaczego to ma znaczenie.W twoim drugim przykładzie (z scanf ()) powodem, dla którego wciąż jest wolniejszy, może być to, że scanf ("% s") analizuje ciąg znaków i szuka dowolnego znaku spacji (spacja, tabulator, nowa linia).
Ponadto tak, CPython wykonuje pewne buforowanie, aby uniknąć odczytów z dysku twardego.
źródło
Pierwszy element odpowiedzi:
<iostream>
jest powolny. Cholera powolna. Mam ogromny wzrost wydajności, jak pokazanoscanf
poniżej, ale wciąż jest dwa razy wolniejszy niż Python.źródło
Cóż, widzę, że w twoim drugim rozwiązaniu przełączyłeś się
cin
nascanf
, co było pierwszą sugestią, którą chciałem cię zmusić (cin to sloooooooooooow). Teraz, jeśli przełączysz się zscanf
nafgets
, zobaczysz kolejny wzrost wydajności:fgets
jest to najszybsza funkcja C ++ do wprowadzania ciągów znaków.BTW, nie wiedziałem o tej synchronizacji, miło. Ale nadal powinieneś spróbować
fgets
.źródło
fgets
że będą błędne (pod względem liczby linii i podziału linii między pętlami, jeśli faktycznie trzeba ich użyć) dla wystarczająco dużych linii, bez dodatkowych kontroli niekompletnych linii (i próba zrekompensowania tego wymaga przydzielenia niepotrzebnie dużych buforów , gdziestd::getline
obsługuje realokację w celu płynnego dopasowania rzeczywistych danych wejściowych). Szybko i źle jest łatwe, ale prawie zawsze warto używać „nieco wolniej, ale poprawnie”, cosync_with_stdio
powoduje wyłączenie .