Jedes Mal, wenn ich die langsame Leistung von Iostreams der C ++ - Standardbibliothek erwähne, stoße ich auf eine Welle des Unglaubens. Ich habe jedoch Profiler-Ergebnisse, die zeigen, wie viel Zeit im iostream-Bibliothekscode verbracht wurde (vollständige Compiler-Optimierungen), und der Wechsel von iostreams zu betriebssystemspezifischen E / A-APIs und die benutzerdefinierte Pufferverwaltung führen zu einer Verbesserung um eine Größenordnung.
Welche zusätzliche Arbeit leistet die C ++ - Standardbibliothek, wird sie vom Standard verlangt und ist sie in der Praxis nützlich? Oder bieten einige Compiler Implementierungen von iostreams an, die mit der manuellen Pufferverwaltung konkurrenzfähig sind?
Benchmarks
Um die Dinge in Bewegung zu bringen, habe ich ein paar kurze Programme geschrieben, um die interne Pufferung von iostreams zu üben:
- Einfügen von Binärdaten in ein
ostringstream
http://ideone.com/2PPYw - Einfügen von Binärdaten in einen
char[]
Puffer http://ideone.com/Ni5ct - Einfügen von Binärdaten in eine
vector<char>
Verwendung vonback_inserter
http://ideone.com/Mj2Fi - NEU :
vector<char>
einfacher Iterator http://ideone.com/9iitv - NEU : Binärdaten direkt in
stringbuf
http://ideone.com/qc9QA einfügen - NEU :
vector<char>
Einfacher Iterator plus Grenzüberprüfung http://ideone.com/YyrKy
Beachten Sie, dass die ostringstream
und stringbuf
Versionen weniger Iterationen laufen , weil sie so viel langsamer sind.
Auf ideone ist das ostringstream
ungefähr 3 mal langsamer als std:copy
+ back_inserter
+ std::vector
und ungefähr 15 mal langsamer als memcpy
in einem Rohpuffer. Dies steht im Einklang mit der Vorher-Nachher-Profilerstellung, als ich meine reale Anwendung auf benutzerdefinierte Pufferung umstellte.
Dies sind alles In-Memory-Puffer, sodass die Langsamkeit von Iostreams nicht auf langsame Festplatten-E / A, zu viel Leeren, Synchronisation mit stdio oder andere Dinge zurückzuführen ist, mit denen die beobachtete Langsamkeit der C ++ - Standardbibliothek entschuldigt wird iostream.
Es wäre schön, Benchmarks auf anderen Systemen zu sehen und Kommentare zu den üblichen Implementierungen (wie gccs libc ++, Visual C ++, Intel C ++) und zu sehen, wie viel Overhead vom Standard vorgeschrieben wird.
Begründung für diesen Test
Einige Leute haben richtig darauf hingewiesen, dass iostreams häufiger für formatierte Ausgaben verwendet werden. Sie sind jedoch auch die einzige moderne API, die vom C ++ - Standard für den Zugriff auf Binärdateien bereitgestellt wird. Der eigentliche Grund für die Durchführung von Leistungstests für die interne Pufferung gilt jedoch für die typischen formatierten E / A: Wenn iostreams den Festplattencontroller nicht mit Rohdaten versorgen können, wie können sie möglicherweise mithalten, wenn sie auch für die Formatierung verantwortlich sind?
Benchmark-Timing
All dies erfolgt pro Iteration der äußeren ( k
) Schleife.
Auf ideone (gcc-4.3.4, unbekanntes Betriebssystem und unbekannte Hardware):
ostringstream
: 53 Millisekundenstringbuf
: 27 msvector<char>
undback_inserter
: 17,6 msvector<char>
mit gewöhnlichem Iterator: 10,6 msvector<char>
Iterator- und Grenzüberprüfung: 11,4 mschar[]
: 3,7 ms
Auf meinem Laptop (Visual C ++ 2010 x86, cl /Ox /EHsc
Windows 7 Ultimate 64-Bit, Intel Core i7, 8 GB RAM):
ostringstream
: 73,4 Millisekunden, 71,6 msstringbuf
: 21,7 ms, 21,3 msvector<char>
undback_inserter
: 34,6 ms, 34,4 msvector<char>
mit gewöhnlichem Iterator: 1,10 ms, 1,04 msvector<char>
Überprüfung des Iterators und der Grenzen: 1,11 ms, 0,87 ms, 1,12 ms, 0,89 ms, 1,02 ms, 1,14 mschar[]
: 1,48 ms, 1,57 ms
Visual C ++ 2010 x86, mit Profil-geführte Optimierung cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, laufen, link /ltcg:pgo
, Maße:
ostringstream
: 61,2 ms, 60,5 msvector<char>
mit gewöhnlichem Iterator: 1,04 ms, 1,03 ms
Gleicher Laptop, gleiches Betriebssystem mit cygwin gcc 4.3.4 g++ -O3
:
ostringstream
: 62,7 ms, 60,5 msstringbuf
: 44,4 ms, 44,5 msvector<char>
undback_inserter
: 13,5 ms, 13,6 msvector<char>
mit gewöhnlichem Iterator: 4,1 ms, 3,9 msvector<char>
Iterator- und Grenzüberprüfung: 4,0 ms, 4,0 mschar[]
: 3,57 ms, 3,75 ms
Gleicher Laptop, Visual C ++ 2008 SP1 , cl /Ox /EHsc
:
ostringstream
: 88,7 ms, 87,6 msstringbuf
: 23,3 ms, 23,4 msvector<char>
undback_inserter
: 26,1 ms, 24,5 msvector<char>
mit gewöhnlichem Iterator: 3,13 ms, 2,48 msvector<char>
Iterator- und Grenzüberprüfung: 2,97 ms, 2,53 mschar[]
: 1,52 ms, 1,25 ms
Gleicher Laptop, Visual C ++ 2010 64-Bit-Compiler:
ostringstream
: 48,6 ms, 45,0 msstringbuf
: 16,2 ms, 16,0 msvector<char>
undback_inserter
: 26,3 ms, 26,5 msvector<char>
mit gewöhnlichem Iterator: 0,87 ms, 0,89 msvector<char>
Iterator- und Grenzüberprüfung: 0,99 ms, 0,99 mschar[]
: 1,25 ms, 1,24 ms
EDIT: Lief alle zweimal, um zu sehen, wie konsistent die Ergebnisse waren. Ziemlich konsistente IMO.
HINWEIS: Da ich auf meinem Laptop mehr CPU-Zeit sparen kann, als ideone zulässt, setze ich die Anzahl der Iterationen für alle Methoden auf 1000. Dies bedeutet , dass ostringstream
und vector
Umverteilung, die erst beim ersten Durchgang nimmt, sollte nur geringe Auswirkungen auf die endgültigen Ergebnisse haben.
EDIT: Ups, habe einen Fehler im vector
normalen Iterator gefunden, der Iterator wurde nicht weiterentwickelt und daher gab es zu viele Cache-Treffer. Ich habe mich gefragt, wie gut es vector<char>
war char[]
. Es machte jedoch keinen großen Unterschied, vector<char>
ist immer noch schneller als char[]
unter VC ++ 2010.
Schlussfolgerungen
Das Puffern von Ausgabestreams erfordert jedes Mal, wenn Daten angehängt werden, drei Schritte:
- Überprüfen Sie, ob der eingehende Block zum verfügbaren Pufferplatz passt.
- Kopieren Sie den eingehenden Block.
- Aktualisieren Sie den Datenende-Zeiger.
Das neueste Code-Snippet, das ich veröffentlicht habe, " vector<char>
einfacher Iterator plus Grenzüberprüfung", tut dies nicht nur, es weist auch zusätzlichen Speicherplatz zu und verschiebt die vorhandenen Daten, wenn der eingehende Block nicht passt. Wie Clifford betonte, müsste das Puffern in einer Datei-E / A-Klasse dies nicht tun, sondern nur den aktuellen Puffer leeren und wiederverwenden. Dies sollte also eine Obergrenze für die Kosten für die Pufferung der Ausgabe sein. Und genau das ist erforderlich, um einen funktionierenden In-Memory-Puffer zu erstellen.
Warum ist stringbuf
ideone 2,5-mal langsamer und mindestens 10-mal langsamer, wenn ich es teste? Es wird in diesem einfachen Mikro-Benchmark nicht polymorph verwendet, das erklärt es also nicht.
quelle
std::ostringstream
es nicht klug genug ist, die Puffergröße exponentiell zu erhöhenstd::vector
, ist dies (A) dumm und (B) etwas, über das Leute nachdenken sollten, die über die E / A-Leistung nachdenken. Auf jeden Fall wird der Puffer wiederverwendet und nicht jedes Mal neu zugewiesen. Und verwendetstd::vector
auch einen dynamisch wachsenden Puffer. Ich versuche hier fair zu sein.ostringstream
und eine möglichst schnelle Leistung wünschen, sollten Sie in Betracht ziehen, direkt zu wechselnstringbuf
. Esostream
wird angenommen, dass die Klassen länderspezifische Formatierungsfunktionen mit flexibler Pufferauswahl (Datei, Zeichenfolge usw.)rdbuf()
und ihrer virtuellen Funktionsschnittstelle verbinden. Wenn Sie keine Formatierung vornehmen, wird diese zusätzliche Indirektionsebene im Vergleich zu anderen Ansätzen sicherlich proportional teuer aussehen.ofstream
zufprintf
gewechselt sind. MSVC 2008 unter WinXPsp3. iostreams ist nur hundeschwach.Antworten:
Beantworten Sie nicht die Einzelheiten Ihrer Frage, sondern den Titel: das Jahr 2006 Einzelheiten technische Bericht zur C ++ - Leistung enthält einen interessanten Abschnitt zu IOStreams (S.68). Am relevantesten für Ihre Frage ist in Abschnitt 6.1.2 ("Ausführungsgeschwindigkeit"):
Da der Bericht im Jahr 2006 verfasst wurde, würde man hoffen, dass viele der Empfehlungen in aktuelle Compiler aufgenommen wurden, aber vielleicht ist dies nicht der Fall.
Wie Sie bereits erwähnt haben, sind Facetten möglicherweise nicht enthalten
write()
(aber ich würde das nicht blind annehmen). Was ist also? Das Ausführen von GProf auf Ihremostringstream
mit GCC kompilierten Code führt zu folgender Aufschlüsselung:std::basic_streambuf<char>::xsputn(char const*, int)
std::ostream::write(char const*, int)
main
std::ostream::sentry::sentry(std::ostream&)
std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
std::fpos<int>::fpos(long long)
Der Großteil der Zeit wird also in verbracht
xsputn
, was schließlichstd::copy()
nach vielen Überprüfungen und Aktualisierungen der Cursorpositionen und Puffer aufruft (siehec++\bits\streambuf.tcc
Details).Ich gehe davon aus, dass Sie sich auf den schlimmsten Fall konzentriert haben. Alle Überprüfungen, die durchgeführt werden, machen nur einen kleinen Teil der gesamten geleisteten Arbeit aus, wenn Sie mit relativ großen Datenblöcken arbeiten. Ihr Code verschiebt jedoch Daten in jeweils vier Bytes und verursacht jedes Mal alle zusätzlichen Kosten. Es ist klar, dass man dies in einer realen Situation vermeiden würde - überlegen Sie, wie vernachlässigbar die Strafe gewesen wäre, wenn
write
sie auf einem Array von 1 m Ints statt auf 1 m Mal auf einem Int aufgerufen worden wäre . Und in einer realen Situation würde man die wichtigen Funktionen von IOStreams wirklich schätzen, nämlich das speichersichere und typsichere Design. Solche Vorteile haben ihren Preis, und Sie haben einen Test geschrieben, bei dem diese Kosten die Ausführungszeit dominieren.quelle
ostream::write()
.sizeof i
haben alle Compiler, mit denen ich teste, 4 Byteint
). Und das scheint mir gar nicht so unrealistisch zu sein, an welche Größe Chunks Ihrer Meinung nach bei jedem Aufrufxsputn
in einem typischen Code wie übergeben werdenstream << "VAR: " << var.x << ", " << var.y << endl;
.xsputn
fünfmal aufgerufen wird, könnte sich sehr gut in einer Schleife befinden, die eine Datei mit 10 Millionen Zeilen schreibt. Das Übergeben von Daten an Iostreams in großen Blöcken ist weitaus weniger ein reales Szenario als mein Benchmark-Code. Warum sollte ich in einen gepufferten Stream mit der Mindestanzahl von Anrufen schreiben müssen ? Wenn ich meine eigene Pufferung durchführen muss, wozu dienen iostreams überhaupt? Und mit Binärdaten habe ich die Möglichkeit, sie selbst zu puffern. Wenn ich Millionen von Zahlen in eine Textdatei schreibe, existiert die Massenoption einfach nicht. Ich MUSSoperator <<
für jede einzelne aufrufen .Ich bin ziemlich enttäuscht von den Visual Studio-Benutzern da draußen, die eher einen Gimme zu diesem Thema hatten:
ostream
tritt dassentry
Objekt (das vom Standard gefordert wird) in einen kritischen Abschnitt ein , der das Objekt schütztstreambuf
(was nicht erforderlich ist). Dies scheint nicht optional zu sein, daher zahlen Sie die Kosten für die Thread-Synchronisierung selbst für einen lokalen Stream, der von einem einzelnen Thread verwendet wird und für den keine Synchronisierung erforderlich ist.Dies schadet dem Code, mit
ostringstream
dem Nachrichten ziemlich stark formatiert werden. Durch diestringbuf
direkte Verwendung vonsentry
wird die Verwendung von vermieden , aber die formatierten Einfügeoperatoren können nicht direkt mitstreambuf
s arbeiten. Bei Visual C ++ 2010 verlangsamt sich der kritische Abschnittostringstream::write
gegenüber dem Basiswert um den Faktor dreistringbuf::sputn
Aufruf .Wenn man sich die Profilerdaten von beldaz auf newlib ansieht , scheint es klar zu sein, dass gcc's
sentry
so etwas Verrücktes nicht macht.ostringstream::write
unter gcc dauert nur etwa 50% länger alsstringbuf::sputn
, aberstringbuf
selbst ist viel langsamer als unter VC ++. Und beide sind immer noch sehr ungünstig mit der Verwendung einervector<char>
E / A-Pufferung, wenn auch nicht mit dem gleichen Abstand wie unter VC ++.quelle
sentry
... "Der Klassenwächter definiert eine Klasse, die für die Ausführung ausnahmesicherer Präfix- und Suffixoperationen verantwortlich ist." und ein Hinweis "Der Sentry-Konstruktor und der Destruktor können auch zusätzliche implementierungsabhängige Operationen ausführen." Man kann auch aus dem C ++ - Prinzip "Sie zahlen nicht für das, was Sie nicht verwenden" vermuten, dass das C ++ - Komitee eine solch verschwenderische Anforderung niemals genehmigen würde. Sie können jedoch gerne eine Frage zur Sicherheit von iostream-Threads stellen.Das Problem, das Sie sehen, liegt im Overhead um jeden Aufruf von write (). Jede Abstraktionsebene, die Sie hinzufügen (char [] -> vector -> string -> ostringstream), fügt ein paar weitere Funktionsaufrufe / Rückgaben und andere Housekeeping-Probleme hinzu, die sich - wenn Sie sie millionenfach aufrufen - summieren.
Ich habe zwei der Beispiele auf ideone so modifiziert, dass sie jeweils zehn Ints schreiben. Die Ostringstream-Zeit stieg von 53 auf 6 ms (fast 10-fache Verbesserung), während sich die Char-Schleife verbesserte (3,7 auf 1,5) - nützlich, aber nur um den Faktor zwei.
Wenn Sie so besorgt über die Leistung sind, müssen Sie das richtige Werkzeug für den Job auswählen. ostringstream ist nützlich und flexibel, aber es gibt eine Strafe dafür, es so zu verwenden, wie Sie es versuchen. char [] ist härtere Arbeit, aber die Leistungssteigerungen können großartig sein (denken Sie daran, dass der gcc wahrscheinlich auch die Memcpys für Sie inline wird).
Kurz gesagt, ostringstream ist nicht kaputt, aber je näher Sie dem Metall kommen, desto schneller wird Ihr Code ausgeführt. Assembler hat für einige Leute immer noch Vorteile.
quelle
ostringstream::write()
tunvector::push_back()
? Wenn überhaupt, sollte es schneller sein, da anstelle von vier einzelnen Elementen ein Block übergeben wird. Wennostringstream
es langsamer ist alsstd::vector
ohne zusätzliche Funktionen, dann würde ich das als kaputt bezeichnen.stringbuf
direkte Verwendung werden nicht alle Funktionsaufrufe entfernt, dastringbuf
die öffentliche Schnittstelle aus öffentlichen nicht virtuellen Funktionen in der Basisklasse besteht, die dann an geschützte virtuelle Funktionen in der abgeleiteten Klasse gesendet werden .sputn
Funktion, die den virtuellen Schutzxsputn
aufruft, inline ist. Auch wenn diesxsputn
nicht inline ist, kann der Compiler beim Inliningsputn
die genauexsputn
erforderliche Überschreibung ermitteln und einen direkten Aufruf generieren, ohne die vtable zu durchlaufen.Um eine bessere Leistung zu erzielen, müssen Sie verstehen, wie die von Ihnen verwendeten Container funktionieren. In Ihrem Array-Beispiel char [] wird das Array mit der erforderlichen Größe im Voraus zugewiesen. In Ihrem Vektor- und Ostringstream-Beispiel zwingen Sie die Objekte, Daten wiederholt zuzuweisen, neu zuzuweisen und möglicherweise viele Male zu kopieren, wenn das Objekt wächst.
Mit std :: vector lässt sich dies leicht beheben, indem die Größe des Vektors wie beim char-Array auf die endgültige Größe initialisiert wird. Stattdessen lähmen Sie die Leistung zu Unrecht, indem Sie die Größe auf Null ändern! Das ist kaum ein fairer Vergleich.
In Bezug auf ostringstream ist eine Vorbelegung des Raums nicht möglich. Ich würde vorschlagen, dass dies eine unangemessene Verwendung ist. Die Klasse hat einen weitaus größeren Nutzen als ein einfaches char-Array. Wenn Sie dieses Dienstprogramm jedoch nicht benötigen, verwenden Sie es nicht, da Sie den Overhead auf jeden Fall bezahlen müssen. Stattdessen sollte es für das verwendet werden, wofür es gut ist - das Formatieren von Daten in eine Zeichenfolge. C ++ bietet eine große Auswahl an Containern, und ein Ostringstram ist für diesen Zweck am wenigsten geeignet.
Im Fall des Vektors und des ostringstream erhalten Sie Schutz vor Pufferüberlauf, den Sie mit einem char-Array nicht erhalten, und dieser Schutz ist nicht kostenlos.
quelle
ostringstream.str.reserve(4000000)
und es machte keinen Unterschied.ostringstream
, Sie könnten "reservieren", indem Sie eine Dummy-Zeichenfolge übergeben, dh:ostringstream str(string(1000000 * sizeof(int), '\0'));
Mitvector
, dasresize
gibt keinen Platz frei, es wird nur erweitert, wenn es benötigt wird.vector[]
Operator wird normalerweise NICHT standardmäßig auf Grenzfehler überprüft.vector.at()
ist jedoch.vector<T>::resize(0)
operator[]
, aberpush_back()
(überback_inserter
), was definitiv auf Überlauf testet. Es wurde eine andere Version hinzugefügt, die nicht verwendet wirdpush_back
.