Ich versuche, Code von Python nach C ++ zu konvertieren, um ein wenig Geschwindigkeit zu gewinnen und meine verrosteten C ++ - Fähigkeiten zu verbessern. Gestern war ich schockiert, als eine naive Implementierung des Lesens von Zeilen aus stdin in Python viel schneller war als in C ++ (siehe hier ). Heute habe ich endlich herausgefunden, wie man einen String in C ++ mit zusammengeführten Trennzeichen teilt (ähnliche Semantik wie pythons split ()), und jetzt erlebe ich deja vu! Mein C ++ - Code benötigt viel länger, um die Arbeit zu erledigen (allerdings nicht um eine Größenordnung mehr, wie es in der gestrigen Lektion der Fall war).
Python-Code:
#!/usr/bin/env python
from __future__ import print_function
import time
import sys
count = 0
start_time = time.time()
dummy = None
for line in sys.stdin:
dummy = line.split()
count += 1
delta_sec = int(time.time() - start_time)
print("Python: Saw {0} lines in {1} seconds. ".format(count, delta_sec), end='')
if delta_sec > 0:
lps = int(count/delta_sec)
print(" Crunch Speed: {0}".format(lps))
else:
print('')
C ++ - Code:
#include <iostream>
#include <string>
#include <sstream>
#include <time.h>
#include <vector>
using namespace std;
void split1(vector<string> &tokens, const string &str,
const string &delimiters = " ") {
// Skip delimiters at beginning
string::size_type lastPos = str.find_first_not_of(delimiters, 0);
// Find first non-delimiter
string::size_type pos = str.find_first_of(delimiters, lastPos);
while (string::npos != pos || string::npos != lastPos) {
// Found a token, add it to the vector
tokens.push_back(str.substr(lastPos, pos - lastPos));
// Skip delimiters
lastPos = str.find_first_not_of(delimiters, pos);
// Find next non-delimiter
pos = str.find_first_of(delimiters, lastPos);
}
}
void split2(vector<string> &tokens, const string &str, char delim=' ') {
stringstream ss(str); //convert string to stream
string item;
while(getline(ss, item, delim)) {
tokens.push_back(item); //add token to vector
}
}
int main() {
string input_line;
vector<string> spline;
long count = 0;
int sec, lps;
time_t start = time(NULL);
cin.sync_with_stdio(false); //disable synchronous IO
while(cin) {
getline(cin, input_line);
spline.clear(); //empty the vector for the next line to parse
//I'm trying one of the two implementations, per compilation, obviously:
// split1(spline, input_line);
split2(spline, input_line);
count++;
};
count--; //subtract for final over-read
sec = (int) time(NULL) - start;
cerr << "C++ : Saw " << count << " lines in " << sec << " seconds." ;
if (sec > 0) {
lps = count / sec;
cerr << " Crunch speed: " << lps << endl;
} else
cerr << endl;
return 0;
//compiled with: g++ -Wall -O3 -o split1 split_1.cpp
Beachten Sie, dass ich zwei verschiedene Split-Implementierungen ausprobiert habe. One (split1) verwendet String-Methoden, um nach Token zu suchen, und kann mehrere Token zusammenführen sowie zahlreiche Token verarbeiten (es kommt von hier ). Der zweite (split2) verwendet getline, um die Zeichenfolge als Stream zu lesen, führt keine Trennzeichen zusammen und unterstützt nur ein einzelnes Begrenzungszeichen (das von mehreren StackOverflow-Benutzern in Antworten auf Fragen zur Zeichenfolgenaufteilung veröffentlicht wurde).
Ich habe dies mehrmals in verschiedenen Reihenfolgen ausgeführt. Mein Testgerät ist ein Macbook Pro (2011, 8 GB, Quad Core), nicht dass es viel ausmacht. Ich teste mit einer 20-Millionen-Zeilentextdatei mit drei durch Leerzeichen getrennten Spalten, die jeweils ähnlich aussehen: "foo.bar 127.0.0.1 home.foo.bar"
Ergebnisse:
$ /usr/bin/time cat test_lines_double | ./split.py
15.61 real 0.01 user 0.38 sys
Python: Saw 20000000 lines in 15 seconds. Crunch Speed: 1333333
$ /usr/bin/time cat test_lines_double | ./split1
23.50 real 0.01 user 0.46 sys
C++ : Saw 20000000 lines in 23 seconds. Crunch speed: 869565
$ /usr/bin/time cat test_lines_double | ./split2
44.69 real 0.02 user 0.62 sys
C++ : Saw 20000000 lines in 45 seconds. Crunch speed: 444444
Was mache ich falsch? Gibt es eine bessere Möglichkeit zum Aufteilen von Zeichenfolgen in C ++, die nicht auf externen Bibliotheken basiert (dh keinen Boost), das Zusammenführen von Trennzeichenfolgen unterstützt (wie die Aufteilung von Python), threadsicher ist (also kein strtok) und deren Leistung mindestens ist? auf Augenhöhe mit Python?
Bearbeiten 1 / Teillösung?:
Ich habe versucht, den Vergleich fairer zu gestalten, indem Python die Dummy-Liste zurückgesetzt und jedes Mal angehängt hat, wie dies in C ++ der Fall ist. Dies ist immer noch nicht genau das, was der C ++ - Code tut, aber es ist ein bisschen näher. Grundsätzlich ist die Schleife jetzt:
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
Die Leistung von Python entspricht jetzt in etwa der von split1 C ++.
/usr/bin/time cat test_lines_double | ./split5.py
22.61 real 0.01 user 0.40 sys
Python: Saw 20000000 lines in 22 seconds. Crunch Speed: 909090
Ich bin immer noch überrascht, dass diese C ++ - Implementierungen nicht schneller wären, selbst wenn Python für die Zeichenfolgenverarbeitung so optimiert ist (wie Matt Joiner vorgeschlagen hat). Wenn jemand Ideen hat, wie dies mit C ++ optimaler gemacht werden kann, teilen Sie bitte Ihren Code mit. (Ich denke, mein nächster Schritt wird darin bestehen, dies in reinem C zu implementieren, obwohl ich die Produktivität der Programmierer nicht abwägen werde, um mein Gesamtprojekt in C erneut zu implementieren. Dies wird also nur ein Experiment für die Geschwindigkeit der Zeichenfolgenaufteilung sein.)
Vielen Dank an alle für Ihre Hilfe.
Endgültige Bearbeitung / Lösung:
Bitte lesen Sie die akzeptierte Antwort von Alf. Da Python Strings ausschließlich als Referenz behandelt und STL-Strings häufig kopiert werden, ist die Leistung bei Vanilla-Python-Implementierungen besser. Zum Vergleich habe ich meine Daten über Alfs Code kompiliert und ausgeführt. Hier ist die Leistung auf demselben Computer wie bei allen anderen Läufen, die im Wesentlichen mit der naiven Python-Implementierung identisch ist (obwohl sie schneller ist als die Python-Implementierung, mit der die Liste zurückgesetzt / angehängt wird) in der obigen Bearbeitung gezeigt):
$ /usr/bin/time cat test_lines_double | ./split6
15.09 real 0.01 user 0.45 sys
C++ : Saw 20000000 lines in 15 seconds. Crunch speed: 1333333
Mein einziger kleiner Kritikpunkt betrifft die Menge an Code, die erforderlich ist, damit C ++ in diesem Fall funktioniert.
Eine der Lehren aus dieser Ausgabe und der gestrigen Ausgabe zum Lesen von Standardzeilen (siehe oben) ist, dass man immer einen Benchmark durchführen sollte, anstatt naive Annahmen über die relative "Standard" -Leistung von Sprachen zu treffen. Ich schätze die Ausbildung.
Nochmals vielen Dank an alle für Ihre Vorschläge!
g++ -Wall -O3 -o split1 split_1.cpp
@JJC: Wie sieht Ihr Benchmark - Tarif , wenn Sie tatsächlich nutzendummy
undspline
jeweils vielleicht Python entfernt den Aufruf ,line.split()
weil es keine Nebenwirkungen hat?Antworten:
Vermutlich sind Python-Zeichenfolgen unveränderliche Zeichenfolgen mit Referenzzählung, sodass im Python-Code keine Zeichenfolgen kopiert werden, während C ++
std::string
ein veränderlicher Werttyp ist und bei der kleinsten Gelegenheit kopiert wird.Wenn das Ziel eine schnelle Aufteilung ist, würde man Teilzeichenfolgenoperationen mit konstanter Zeit verwenden, was bedeutet, dass nur auf Teile der ursprünglichen Zeichenfolge Bezug genommen wird , wie in Python (und Java und C #…).
Die C ++ -
std::string
Klasse verfügt jedoch über eine Einlösungsfunktion: Sie ist Standard , sodass sie verwendet werden kann, um Zeichenfolgen sicher und portabel zu übergeben, wenn die Effizienz keine Hauptrolle spielt. Aber genug plaudern. Code - und auf meinem Computer ist dies natürlich schneller als Python, da Pythons String-Handling in C implementiert ist, einer Teilmenge von C ++ (he he):Haftungsausschluss: Ich hoffe, es gibt keine Fehler. Ich habe die Funktionalität nicht getestet, sondern nur die Geschwindigkeit überprüft. Aber ich denke, selbst wenn es ein oder zwei Fehler gibt, wirkt sich eine Korrektur nicht wesentlich auf die Geschwindigkeit aus.
quelle
StringRef
können Sie den Teilstringstd::string
sehr einfach auf einen kopierenstring( sr.begin(), sr.end() )
.PyString_FromStringAndSize()
das diese Aufrufe verwendet , neue Zeichenfolgen zuPyObject_MALLOC()
. Es gibt also keine Optimierung mit einer gemeinsam genutzten Darstellung, die ausnutzt, dass die Zeichenfolgen in Python unveränderlich sind.Ich biete keine besseren Lösungen (zumindest in Bezug auf die Leistung), aber einige zusätzliche Daten, die interessant sein könnten.
Verwenden
strtok_r
(Wiedereintrittsvariante vonstrtok
):Verwenden Sie zusätzlich Zeichenfolgen für Parameter und
fgets
zur Eingabe:Und in einigen Fällen, in denen das Zerstören der Eingabezeichenfolge akzeptabel ist:
Die Zeitpunkte für diese sind wie folgt (einschließlich meiner Ergebnisse für die anderen Varianten aus der Frage und der akzeptierten Antwort):
Wie wir sehen können, ist die Lösung aus der akzeptierten Antwort immer noch am schnellsten.
Für alle, die weitere Tests durchführen möchten, habe ich auch ein Github-Repo mit allen Programmen aus der Frage, der akzeptierten Antwort, dieser Antwort und zusätzlich einem Makefile und einem Skript zum Generieren von Testdaten erstellt: https: // github. com / tobbez / string-split .
quelle
memcpy
nicht verwendenstrcpy
, falls der Compiler diese Optimierung nicht bemerkt.strcpy
Verwendet normalerweise eine langsamere Startstrategie, die ein Gleichgewicht zwischen schnell für kurze Saiten und Hochfahren auf volle SIMD für lange Saiten schafft.memcpy
kennt die Größe sofort und muss keine SIMD-Tricks verwenden, um nach dem Ende einer Zeichenfolge mit impliziter Länge zu suchen. (Keine große Sache bei modernen x86). Das Erstellen vonstd::string
Objekten mit dem(char*, len)
Konstruktor ist möglicherweise auch schneller, wenn Sie dies herausholen könnensaveptr-token
. Offensichtlich wäre es am schnellsten, nurchar*
Token zu speichern : PIch vermute, dass dies daran liegt, dass die
std::vector
Größe während des Vorgangs eines Funktionsaufrufs von push_back () geändert wird. Wenn Sie versuchen, genügend Platz für die Sätze zu verwendenstd::list
oderstd::vector::reserve()
zu reservieren, sollten Sie eine viel bessere Leistung erzielen. Oder Sie können eine Kombination von beiden wie unten für split1 () verwenden:EDIT : Das andere offensichtliche , was ich sehe , ist , dass Python - Variable
dummy
wird zugewiesen jedes Mal , aber nicht verändert werden . Es ist also kein fairer Vergleich mit C ++. Sie sollten versuchen, Ihren Python-Code sodummy = []
zu ändern , dass er initialisiert wird, und dies dann tundummy += line.split()
. Können Sie die Laufzeit danach melden?EDIT2 : Um es noch fairer zu machen, können Sie die while-Schleife im C ++ - Code folgendermaßen ändern:
quelle
Ich denke, der folgende Code ist besser, wenn einige C ++ 17- und C ++ 14-Funktionen verwendet werden:
Die Wahl des Behälters:
std::vector
.Angenommen, die anfängliche Größe des zugewiesenen internen Arrays ist 1 und die endgültige Größe ist N, Sie werden log2 (N) -Zeiten zuweisen und freigeben, und Sie werden (2 ^ (log2 (N) + 1) - 1) = kopieren (2N - 1) mal. Wie in Ist die schlechte Leistung von std :: vector darauf zurückzuführen, dass realloc nicht logarithmisch aufgerufen wird? Dies kann eine schlechte Leistung haben, wenn die Größe des Vektors nicht vorhersehbar ist und sehr groß sein kann. Wenn Sie jedoch die Größe abschätzen können, ist dies weniger problematisch.
std::list
.Für jeden push_back ist die verbrauchte Zeit eine Konstante, aber es wird wahrscheinlich mehr Zeit als std :: vector für einzelne push_back dauern. Die Verwendung eines Thread-Speicherpools und eines benutzerdefinierten Allokators kann dieses Problem beheben.
std::forward_list
.Entspricht std :: list, belegt jedoch weniger Speicher pro Element. Eine Wrapper-Klasse muss funktionieren, da die API push_back fehlt.
std::array
.Wenn Sie die Wachstumsgrenze kennen, können Sie std :: array verwenden. Natürlich können Sie es nicht direkt verwenden, da es nicht über die API push_back verfügt. Aber Sie können einen Wrapper definieren, und ich denke, dies ist der schnellste Weg, und Sie können Speicherplatz sparen, wenn Ihre Schätzung ziemlich genau ist.
std::deque
.Mit dieser Option können Sie Speicher gegen Leistung eintauschen. Es gibt keine (2 ^ (N + 1) - 1) -malige Kopie des Elements, nur eine N-fache Zuordnung und keine Freigabe. Außerdem haben Sie eine konstante Direktzugriffszeit und die Möglichkeit, an beiden Enden neue Elemente hinzuzufügen.
Laut std :: deque-cppreference
oder Sie können eine Kombination davon verwenden:
std::vector< std::array<T, 2 ^ M> >
Dies ähnelt std :: deque. Der Unterschied besteht nur darin, dass dieser Container das Hinzufügen von Elementen an der Vorderseite nicht unterstützt. Die Leistung ist jedoch immer noch schneller, da das zugrunde liegende std :: -Array nicht (2 ^ (N + 1) - 1) Mal kopiert wird, sondern nur das Zeigerarray für (2 ^) (N - M + 1) - 1) Mal und Zuweisung eines neuen Arrays nur, wenn der Strom voll ist und keine Freigabe erforderlich ist. Übrigens können Sie eine konstante Direktzugriffszeit erhalten.
std::list< std::array<T, ...> >
Entlasten Sie den Druck der Gedächtnisfragmentierung erheblich. Es wird nur dann ein neues Array zugewiesen, wenn das aktuelle voll ist, und es muss nichts kopiert werden. Sie müssen immer noch den Preis für einen zusätzlichen Zeiger bezahlen, der mit Combo 1 verglichen wird.
std::forward_list< std::array<T, ...> >
Wie 2, kostet aber den gleichen Speicher wie Combo 1.
quelle
N
Fall. Es ist schade, dass std :: vector nicht verwenden kann,realloc
um möglicherweise mehr Seiten am Ende der aktuellen Zuordnung zuzuordnen , daher ist es ungefähr 2x langsamer.stringview::remove_prefix
so billig, nur Ihre aktuelle Position in einer normalen Saite zu verfolgen?std::basic_string::find
hat ein optionales 2. Argumentpos = 0
, mit dem Sie die Suche von einem Offset aus starten können.log2(size - 1) + 2
unter Verwendung eines Ganzzahlprotokolls). Bei der ersten Zuordnung wurden 0 Zeichenfolgen verschoben, bei der zweiten 1, dann 2, dann 4, dann 8 und schließlich 16 Zeichenfolgen, was insgesamt 31 Zügen entspricht (2^(log2(size - 1) + 1) - 1)
). Dies ist O (n), nicht O (2 ^ n). Dies wird deutlich besser abschneidenstd::list
.Sie gehen fälschlicherweise davon aus, dass Ihre gewählte C ++ - Implementierung notwendigerweise schneller ist als die von Python. Das String-Handling in Python ist stark optimiert. Weitere Informationen finden Sie in dieser Frage: Warum funktionieren std :: string-Operationen schlecht?
quelle
Wenn Sie die split1-Implementierung verwenden und die Signatur so ändern, dass sie der von split2 besser entspricht, ändern Sie Folgendes:
dazu:
Sie erhalten einen dramatischeren Unterschied zwischen split1 und split2 und einen faireren Vergleich:
quelle
quelle
Ich vermute, dass dies mit der Pufferung auf sys.stdin in Python zusammenhängt, aber keine Pufferung in der C ++ - Implementierung.
In diesem Beitrag finden Sie Details zum Ändern der Puffergröße. Versuchen Sie dann den Vergleich erneut: Festlegen einer kleineren Puffergröße für sys.stdin?
quelle