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.
std::ostringstream
es nicht klug genug ist, die Puffergröße exponentiell zu erhöhen std::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 verwendet std::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 wechseln stringbuf
. Es ostream
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
zu fprintf
gewechselt sind. MSVC 2008 unter WinXPsp3. iostreams ist nur hundeschwach.