Warum ist das Lesen von Zeilen aus stdin in C++ viel langsamer als in Python?

Lesezeit: 12 Minuten

Ich wollte das Lesen von Zeilen der Zeichenfolgeneingabe von stdin mit Python und C++ vergleichen und war schockiert, als ich sah, dass mein C++-Code eine Größenordnung langsamer als der entsprechende Python-Code ausgeführt wurde. Da mein C++ eingerostet ist und ich noch kein Python-Experte bin, sagen Sie mir bitte, ob ich etwas falsch mache oder etwas falsch verstehe.


(TLDR-Antwort: füge die Aussage hinzu: cin.sync_with_stdio(false) oder einfach verwenden fgets stattdessen.

TLDR-Ergebnisse: scrollen Sie ganz nach unten zum Ende meiner Frage und sehen Sie sich die Tabelle an.)


C++-Code:

#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-Äquivalent:

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

Hier sind meine Ergebnisse:

$ 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

Ich sollte beachten, dass ich dies sowohl unter Mac OS X v10.6.8 (Snow Leopard) als auch unter Linux 2.6.32 (Red Hat Linux 6.2) ausprobiert habe. Ersteres ist ein MacBook Pro und letzteres ein sehr kräftiger Server, nicht dass dies zu relevant wäre.

$ 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

Winziger Benchmark-Nachtrag und Zusammenfassung

Der Vollständigkeit halber dachte ich, ich würde die Lesegeschwindigkeit für dieselbe Datei auf derselben Box mit dem ursprünglichen (synchronisierten) C++-Code aktualisieren. Auch dies gilt für eine 100-M-Zeilendatei auf einer schnellen Festplatte. Hier ist der Vergleich mit mehreren Lösungen/Ansätzen:

Implementierung Zeilen pro Sekunde
Python (Standard) 3.571.428
cin (Standard/naiv) 819.672
cin (keine Synchronisierung) 12.500.000
fgets 14.285.714
WC (nicht fairer Vergleich) 54.644.808

  • Hast du deine Tests mehrfach durchgeführt? Vielleicht gibt es ein Problem mit dem Festplatten-Cache.

    – Vaughn Cato

    21. Februar 2012 um 2:20 Uhr

  • @VaughnCato Ja, und auch auf zwei verschiedenen Maschinen.

    – JJC

    21. Februar 2012 um 2:22 Uhr

  • Das Problem ist die Synchronisierung mit stdio – siehe meine Antwort.

    – Vaughn Cato

    21. Februar 2012 um 3:30 Uhr

  • Da anscheinend niemand erwähnt hat, warum Sie mit C++ eine zusätzliche Zeile erhalten: Nicht gegen testen cin.eof()!! Lege das getline Aufruf in die ‘if’-Anweisung.

    – Xeo

    21. Februar 2012 um 18:29 Uhr

  • wc -l ist schnell, weil es den Stream mehr als eine Zeile gleichzeitig liest (es könnte sein fread(stdin)/memchr('\n') Kombination). Python-Ergebnisse liegen in der gleichen Größenordnung, z. wc-l.py

    – jfs

    27. Februar 2012 um 0:21 Uhr

Warum ist das Lesen von Zeilen aus stdin in C
Vaughn Cato

tl;dr: Aufgrund unterschiedlicher Standardeinstellungen in C++ sind mehr Systemaufrufe erforderlich.

Standardmäßig, cin wird mit stdio synchronisiert, wodurch jegliche Eingabepufferung vermieden wird. Wenn Sie dies oben auf Ihrem Main hinzufügen, sollten Sie eine viel bessere Leistung sehen:

std::ios_base::sync_with_stdio(false);

Wenn ein Eingabestream gepuffert wird, wird der Stream normalerweise in größeren Blöcken gelesen, anstatt jeweils ein Zeichen zu lesen. Dies reduziert die Anzahl der Systemaufrufe, die typischerweise relativ teuer sind. Doch seit dem FILE* basierend stdio und iostreams oft separate Implementierungen und daher separate Puffer haben, könnte dies zu einem Problem führen, wenn beide zusammen verwendet würden. Zum Beispiel:

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

Wenn mehr Eingaben gelesen wurden cin als es tatsächlich benötigt wird, dann wäre der zweite ganzzahlige Wert für die nicht verfügbar scanf Funktion, die über einen eigenen unabhängigen Puffer verfügt. Dies würde zu unerwarteten Ergebnissen führen.

Um dies zu vermeiden, werden Streams standardmäßig mit synchronisiert stdio. Ein gängiger Weg, dies zu erreichen, ist zu haben cin Lesen Sie jedes Zeichen einzeln nach Bedarf mit stdio Funktionen. Leider führt dies zu einer Menge Overhead. Bei kleinen Eingabemengen ist dies kein großes Problem, aber wenn Sie Millionen von Zeilen lesen, ist die Leistungseinbuße erheblich.

Glücklicherweise haben die Designer der Bibliothek entschieden, dass Sie diese Funktion auch deaktivieren können sollten, um eine verbesserte Leistung zu erzielen, wenn Sie wissen, was Sie tun, also haben sie dies bereitgestellt sync_with_stdio Methode. Von diesem Link (Hervorhebung hinzugefügt):

Wenn die Synchronisation ausgeschaltet ist, dürfen die C++-Standardstreams ihre E/A unabhängig voneinander puffern. was in einigen Fällen erheblich schneller sein kann.

  • Dies sollte ganz oben sein. Es ist mit ziemlicher Sicherheit richtig. Die Antwort kann nicht darin liegen, das read durch ein zu ersetzen fscanf call, weil das einfach nicht so viel Arbeit macht wie Python. Python muss Speicher für die Zeichenfolge zuweisen, möglicherweise mehrmals, da die vorhandene Zuweisung als unzureichend angesehen wird – genau wie der C++-Ansatz mit std::string. Diese Aufgabe ist mit ziemlicher Sicherheit E/A-gebunden und es gibt viel zu viel FUD über die Erstellungskosten std::string Objekte in C++ oder mit <iostream> an und für sich.

    – Karl Knechtel

    21. Februar 2012 um 3:34 Uhr

  • Ja, das Hinzufügen dieser Zeile direkt über meiner ursprünglichen While-Schleife hat den Code beschleunigt, um sogar Python zu übertreffen. Ich bin dabei, die Ergebnisse als endgültige Bearbeitung zu veröffentlichen. Danke noch einmal!

    – JJC

    21. Februar 2012 um 3:45 Uhr

  • Beachten Sie, dass sync_with_stdio() ist eine statische Member-Funktion, und ein Aufruf dieser Funktion für ein beliebiges Stream-Objekt (z cin) schaltet die Synchronisierung für ein oder aus alle Standard-iostream-Objekte.

    – Johannes Zwinck

    21. Januar 2015 um 1:16 Uhr

Warum ist das Lesen von Zeilen aus stdin in C
2mia

Nur aus Neugier habe ich mir mal angeschaut, was unter der Haube passiert, und das habe ich genutzt dtruss/streife bei jeder Prüfung.

C++

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

Systemaufrufe 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

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

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29

Ich hinke hier ein paar Jahre hinterher, aber:

In ‘Edit 4/5/6’ des ursprünglichen Beitrags verwenden Sie die Konstruktion:

$ /usr/bin/time cat big_file | program_to_benchmark

Das ist in mehrfacher Hinsicht falsch:

  1. Sie planen tatsächlich die Ausführung von cat, nicht Ihr Maßstab. Die ‘user’- und ‘sys’-CPU-Auslastung, angezeigt von time sind die von cat, nicht Ihr Benchmark-Programm. Schlimmer noch, die „echte“ Zeit ist auch nicht unbedingt genau. Abhängig von der Implementierung von cat und von Pipelines in Ihrem lokalen Betriebssystem ist es möglich, dass cat schreibt einen letzten riesigen Puffer und wird beendet, lange bevor der Leseprozess seine Arbeit beendet hat.

  2. Gebrauch von cat ist unnötig und sogar kontraproduktiv; Sie fügen bewegliche Teile hinzu. Wenn Sie auf einem ausreichend alten System waren (dh mit einer einzelnen CPU und — in bestimmten Generationen von Computern — I/O schneller als die CPU) — die bloße Tatsache, dass cat lief, konnte die Ergebnisse erheblich verfälschen. Sie unterliegen auch jeglicher Eingabe- und Ausgabepufferung und anderer Verarbeitung cat machen dürfen. (Dies würde Ihnen wahrscheinlich eine einbringen „Nutzloser Einsatz von Cat“ Award, wenn ich Randal Schwartz wäre.

Eine bessere Konstruktion wäre:

$ /usr/bin/time program_to_benchmark < big_file

Bei dieser Aussage handelt es sich um die Hülse Dadurch wird big_file geöffnet und an Ihr Programm übergeben (naja, eigentlich an time der dann Ihr Programm als Unterprozess ausführt) als bereits geöffneter Dateideskriptor. Das Lesen der Datei liegt zu 100 % ausschließlich in der Verantwortung des Programms, das Sie testen möchten. Dadurch erhalten Sie ein echtes Ablesen seiner Leistung ohne falsche Komplikationen.

Ich werde zwei mögliche, aber tatsächlich falsche „Korrekturen“ erwähnen, die ebenfalls in Betracht gezogen werden könnten (aber ich „nummeriere“ sie anders, da dies keine Dinge sind, die im ursprünglichen Beitrag falsch waren):

A. Sie könnten dies „reparieren“, indem Sie nur Ihr Programm timen:

$ cat big_file | /usr/bin/time program_to_benchmark

B. oder durch Timing der gesamten Pipeline:

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

Diese sind aus den gleichen Gründen falsch wie Nr. 2: Sie werden immer noch verwendet cat unnötigerweise. Ich erwähne sie aus mehreren Gründen:

  • sie sind „natürlicher“ für Leute, die mit den E/A-Umleitungsfunktionen der POSIX-Shell nicht ganz vertraut sind

  • es mag Fälle geben, wo cat ist erforderlich (z. B.: Die zu lesende Datei erfordert eine Art Zugriffsrecht, und Sie möchten dieses Recht nicht dem zu testenden Programm erteilen: sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output)

  • in der Praxisauf modernen Maschinen, die hinzugefügt cat in der Pipeline ist wahrscheinlich nicht wirklich von Bedeutung.

Aber das letzte sage ich mit einigem Zögern. Wenn wir das letzte Ergebnis in ‘Edit 5’ untersuchen —

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

— das behauptet das cat verbrauchte während des Tests 74 % der CPU; und tatsächlich sind 1,34/1,83 etwa 74 %. Vielleicht eine Folge von:

$ /usr/bin/time wc -l < temp_big_file

hätte nur die restlichen 0,49 Sekunden gedauert! Wahrscheinlich nicht: cat hier musste für die bezahlen read() Systemaufrufe (oder Äquivalent), die die Datei von der ‘Festplatte’ (eigentlich Puffercache) übertragen haben, sowie die Pipe-Schreibvorgänge, an die sie gesendet werden sollen wc. Den richtigen Test hätten die noch machen müssen read() Anrufe; Nur die Write-to-Pipe- und Read-from-Pipe-Aufrufe wären eingespart worden, und diese sollten ziemlich billig sein.

Dennoch gehe ich davon aus, dass Sie in der Lage sein würden, den Unterschied zwischen zu messen cat file | wc -l und wc -l < file und finden Sie einen merklichen Unterschied (2-stelliger Prozentsatz). Jeder der langsameren Tests wird in absoluter Zeit eine ähnliche Strafe bezahlt haben; was jedoch einen kleineren Bruchteil seiner größeren Gesamtzeit ausmachen würde.

Tatsächlich habe ich einige schnelle Tests mit einer 1,5-Gigabyte-Mülldatei auf einem Linux 3.13-System (Ubuntu 14.04) durchgeführt und diese Ergebnisse erhalten (dies sind eigentlich die „Best of 3“-Ergebnisse; natürlich nach dem Vorbereiten des Caches):

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

Beachten Sie, dass die beiden Pipeline-Ergebnisse behaupten, mehr CPU-Zeit (user+sys) als echte Wanduhrzeit in Anspruch genommen zu haben. Das liegt daran, dass ich den eingebauten „time“-Befehl der Shell (bash) verwende, der die Pipeline kennt; und ich befinde mich auf einem Multi-Core-Computer, auf dem separate Prozesse in einer Pipeline separate Kerne verwenden können, wodurch die CPU-Zeit schneller als in Echtzeit akkumuliert wird. Verwenden /usr/bin/time Ich sehe eine geringere CPU-Zeit als Echtzeit – was zeigt, dass es nur das einzelne Pipeline-Element zeitlich steuern kann, das ihm auf seiner Befehlszeile übergeben wird. Außerdem gibt die Ausgabe der Shell Millisekunden während an /usr/bin/time gibt nur Hundertstelsekunden aus.

Also auf dem Effizienzniveau von wc -lder cat macht einen riesigen Unterschied: 409 / 283 = 1,453 oder 45,3 % mehr Echtzeit und 775 / 280 = 2,768, oder satte 177 % mehr CPU-Auslastung! Auf meiner Zufallstestbox war es damals da.

Ich sollte hinzufügen, dass es mindestens einen weiteren signifikanten Unterschied zwischen diesen Teststilen gibt, und ich kann nicht sagen, ob es ein Vorteil oder ein Fehler ist; das musst du selbst entscheiden:

Wenn du rennst cat big_file | /usr/bin/time my_programerhält Ihr Programm Eingaben von einer Pipe, genau in dem Tempo, das von gesendet wird catund in Stücken, die nicht größer sind als geschrieben von cat.

Wenn du rennst /usr/bin/time my_program < big_file, erhält Ihr Programm einen offenen Dateideskriptor für die eigentliche Datei. Ihr Programm — oder in vielen Fällen können die E/A-Bibliotheken der Sprache, in der es geschrieben wurde, unterschiedliche Aktionen ausführen, wenn ihnen ein Dateideskriptor präsentiert wird, der auf eine reguläre Datei verweist. Es kann verwenden mmap(2) um die Eingabedatei ihrem Adressraum zuzuordnen, anstatt explizit zu verwenden read(2) Systemaufrufe. Diese Unterschiede könnten einen viel größeren Einfluss auf Ihre Benchmark-Ergebnisse haben als die geringen Kosten für den Betrieb des cat binär.

Natürlich ist es ein interessantes Benchmark-Ergebnis, wenn das gleiche Programm zwischen den beiden Fällen deutlich unterschiedlich abschneidet. Es zeigt das tatsächlich das Programm oder seine I/O-Bibliotheken sind etwas Interessantes zu tun, wie zu benutzen mmap(). In der Praxis könnte es also gut sein, die Benchmarks in beide Richtungen auszuführen; vielleicht unter Berücksichtigung der cat Ergebnis durch einen kleinen Faktor, um die Betriebskosten zu “verzeihen”. cat selbst.

  • Wow, das war ziemlich aufschlussreich! Obwohl ich mir bewusst war, dass cat unnötig ist, um Eingaben an stdin von Programmen zu füttern, und dass die <-Shell-Weiterleitung bevorzugt wird, habe ich mich im Allgemeinen an cat gehalten, da die erstere Methode den Datenfluss von links nach rechts visuell bewahrt wenn ich über Pipelines nachdenke. Leistungsunterschiede in solchen Fällen habe ich als vernachlässigbar empfunden. Aber ich weiß es zu schätzen, dass du uns erziehst, Bela.

    – JJC

    9. Mai 2017 um 1:16 Uhr

  • Die Umleitung wird in einem frühen Stadium aus der Shell-Befehlszeile herausgeparst, was es Ihnen ermöglicht, eine der folgenden Aktionen durchzuführen, wenn dies ein ansprechenderes Erscheinungsbild des Links-nach-Rechts-Flusses ergibt: $ < big_file time my_program $ time < big_file my_program Dies sollte in jeder POSIX-Shell funktionieren (dh nicht `csh` und ich bin mir nicht sicher, ob es sich um Exotica wie `rc` handelt : )

    – Bela Lübkin

    10. Mai 2017 um 21:55 Uhr


1647099616 862 Warum ist das Lesen von Zeilen aus stdin in C
Karunski

Ich habe das Originalergebnis auf meinem Computer mit g++ auf einem Mac reproduziert.

Das Hinzufügen der folgenden Anweisungen zur C++-Version direkt vor dem while Schleife bringt es inline mit der Python Ausführung:

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

sync_with_stdio Die Geschwindigkeit wurde auf 2 Sekunden verbessert, und das Festlegen eines größeren Puffers reduzierte sie auf 1 Sekunde.

1647099617 154 Warum ist das Lesen von Zeilen aus stdin in C
Stu

getlineStream-Betreiber, scanf, kann praktisch sein, wenn Sie sich nicht um die Ladezeit von Dateien kümmern oder wenn Sie kleine Textdateien laden. Aber wenn Ihnen die Leistung wichtig ist, sollten Sie wirklich nur die gesamte Datei in den Speicher puffern (vorausgesetzt, sie passt).

Hier ist ein Beispiel:

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

Wenn Sie möchten, können Sie einen Stream um diesen Puffer wickeln, um einen bequemeren Zugriff wie folgt zu erhalten:

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

Wenn Sie die Kontrolle über die Datei haben, sollten Sie außerdem ein flaches binäres Datenformat anstelle von Text verwenden. Es ist zuverlässiger zu lesen und zu schreiben, da Sie sich nicht mit all den Mehrdeutigkeiten von Leerzeichen auseinandersetzen müssen. Es ist auch kleiner und viel schneller zu analysieren.

Der folgende Code war bei mir schneller als der andere bisher hier gepostete Code: (Visual Studio 2013, 64 Bit, 500 MB Datei mit Zeilenlänge einheitlich in[01000))[01000))

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'; });
}

Es schlägt alle meine Python-Versuche um mehr als den Faktor 2.

Der Grund, warum die Zeilenanzahl für die C++-Version um eins höher ist als die Anzahl für die Python-Version, liegt übrigens darin, dass das eof-Flag nur gesetzt wird, wenn versucht wird, über eof hinaus zu lesen. Die richtige Schleife wäre also:

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

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

  • Die wirklich richtige Schleife wäre: while (getline(cin, input_line)) line_count++;

    – Jonathan Wakely

    5. Mai 2012 um 14:42 Uhr

994190cookie-checkWarum ist das Lesen von Zeilen aus stdin in C++ viel langsamer als in Python?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy