std::cout ist langsam! Stimmt es wirklich? (Ein kleiner Test)
-
Hallo,
Oft hört man ja std::cout und Konsorten (std::clog, std::cerr) hätten im direkten Verglich mit std::printf und seinen Abwandlungen keine Chance (zumindest im Bezug auf Schnelligkeit. Dass sie typsicher und erweiterbar sind, ist natürlich ihr großer Vorteil). Genauso oft taucht allerdings das Argument auf, dass std::cout durch ein höheres Maß an Typinformationen eigentlich schneller als std::printf, das zuerst den Formatstring parsen muss, sein müsste.
Zumindest bei meinem Kompiler (VC++ 2008 EE) ist std::cout im Vergleich zu std::printf einfach nur langsam. Ich fragte mich also, woran das liegt. Nachdem ich mir die std::basic_ostream Klasse angesehen hatte, erkannt ich, dass die Typinformationen sehr wohl genutzt wurden. Zur Umwandlung von bestimmten Variablen in Strings wird ein Facet mit aktuellem Locale benutzt.
Was bleibt da noch übrig? Ein eigenes Locale schreiben um herauszufinden, ob das der Flaschenhals ist, war mir zu aufwendig. Außerdem zeigten kleine Profilingtests das die meiste Zeit in den Methoden des internen Streambuffers verwendet wurde. Folglich arbeitete ich mich ein wenig in Streambuffer ein und habe eine eigene ConsoleBuffer Klasse geschrieben, die auf OS-spezifischen Funktionen beruht (Ich wollte ein schnelles std::cout erschaffen). Da ich nur Windows habe, benutzt mein Buffer intern WriteConsole.
Letzten Endes war das einfügen des Buffers in std::cout dann eine Kleinigkeit:
std::cout.rdbuf(&console_buffer)Daraufhin folgte ein kleines Benchmark. Ich habe std::cout mit normalem Puffer, std::printf, std::puts und std::cout mir meinem Puffer verglichen. Erwartet hatte ich ungefähr Folgendes:
- 4. std::cout (normal)
- 3. std::cout (mein Puffer)
- 2. std::printf
- 1. std::puts
Ich erhoffte mir einen kleinen Vorsprung vor dem normalen Puffer. std::puts hielt ich für schneller als std::printf, da es den String nicht nach Formatzeichen parsen musste.
Die tatsächlichen Ergebnisse sind aber ein wenig anders. Hier mal ein Ergebnis bei dem ein String mit einer Länge von 247 Zeichen (hat sich so ergeben) 20.000 mal ausgegeben wurde (auf die Konsole):
- std::cout (normal Buffer) : 41.968s
- std::printf : 1.672s
- std::puts : 2.094s
- std::cout (dons::ConsoleBuffer): 1.687s
Andere Tests lieferten im Verhältnis gleiche Ergebnisse. Erstaunlich ist, das puts langsamer als das parsende printf ist und das std::cout fast so schnell wie printf ist. Interessant wird es wohl erst, wenn Variablen anderen Typs ausgegeben werden und printf parsen muss. Dafür hatte ich bisjetzt aber leider noch keine Zeit.
Den Geschwindigkeitsunterschied zwischen den verschiedenen Puffern führe ich momentan auf die Thread-Sicherheit des normalen Puffers zurück. Einen so großen Unterschied (Faktor ca. 25) hatte ich allerdings nicht erwartet. Ist zwar jetzt Implementationsabhängig, aber, wenn jemand den Grund hierfür kennt, bin ich immer daran interessiert.
Wenn jemand Interesse an dem Code hat, stelle ich ihn hier gerne rein (zu finden hier). Auch Kritik ist dann natürlich erwünscht, da ich davon ausgehe, dass ich irgendwo grobe Fehler gemacht habe, die irgendwann zu undefiniertem Verhalten führen oder nicht einem Standard Streambuffer gerecht werden, usw.
Gruß
Don06
-
Denke dein Testprogrammcode währe noch interessant.
Dann könnte man auch optimieren
-
Don06 schrieb:
Wenn jemand Interesse an dem Code hat, stelle ich ihn hier gerne rein. Auch Kritik ist dann natürlich erwünscht, da ich davon ausgehe, dass ich irgendwo grobe Fehler gemacht habe, die irgendwann zu undefiniertem Verhalten führen oder nicht einem Standard Streambuffer gerecht werden, usw.
Selbstverständlich musst du dein Testprogramm mitangeben, wenn du hier Benchmarkergebnisse veröffentlichst.

Wie soll man es denn sonst kontrollieren bzw. auf anderen Implementierungen testen?
-
Don06 schrieb:
Erstaunlich ist, das puts langsamer als das parsende printf ist...
Anscheinend schreibt puts den String direkt zum Ausgabegerät und wartet auf eine Rückmeldung, bevor der nächste String gesendet werden kann. Printf formatiert die Ausgabe erstmal im Speicher (Pufferung, Daten sammeln) und braucht daher viel weniger Schreibzugriffe zur Hardware. Das wäre vielleicht eine Erklärung.
-
Hier gibt es die Sources. Es fehlt noch jegliche Dokumentation und ein bisschen müsste man wohl noch überarbeiten, aber das seht ihr dann selbst.
@funky cat: Danke, das wäre dann natürlich logisch.
Gruß
Don06
-
hm - also ich hab dein program ausprobiert, allerdings den teil mit deinem streambuf weggelassen (unter linux) und NUMBER_OF_PRINTS auf 50000 verzehnfacht, weil die ausgabe sonst <0.02 sekunden war, ergebnis:
g++ (4.2.2) -Wall std::cout (normal Buffer) : 0.17s std::printf : 0.38s std::puts : 0.34sdazu muss ich fairerweise sagen, dass der flaschenhals allein darin liegt, wie schnell meine konsole den text anzeigen kann:
time -p ./main std::cout (normal Buffer) : 0.17s std::printf : 0.38s std::puts : 0.33s real 17.24 user 0.16 sys 0.72dabei komme ich mit gnome-terminal also auf 17.24 sekunden, das fast dreimal schneller ist als xterm (58.10s).
jetzt der direkte vergleich mit folgender änderung am programm:
#ifndef TEST #error "define TEST!" #endif int main() { std::cout.sync_with_stdio(false); double times[4]; times[TEST] = make_test(TEST); const char* const TEST_NAMES[] = { "std::cout (normal Buffer) ", "std::printf ", "std::puts " }; std::cout << '\n' << std::endl; std::cout << TEST_NAMES[TEST] << ": " << times[TEST] << 's' << std::endl; return 0; }die ergebnisse (gnome-terminal):
g++ -Wall -DTEST=0 -o testcout g++ -Wall -DTEST=1 -o testprintf g++ -Wall -DTEST=2 -o testputs time -p ./testcout std::cout (normal Buffer) : 0.17s real 5.43 user 0.01 sys 0.16 time -p ./testprintf std::printf : 0.36s real 6.17 user 0.09 sys 0.27 time -p ./testputs std::puts : 0.36s real 5.91 user 0.04 sys 0.32der flaschenhals liegt also eindeutig bei der jeweiligen konsole, die ich verwendet habe. die zeit, die cout, printf oder puts selbst brauchen, ist vernachlässigbar.
warum deine implementation des cout-streambufs so langsam ist, kann dir wohl am besten die dokumentation deiner entwicklungsumgebung sagen (threadsafe? anderer overhead? funktionalität? vergessen, auf release zu schalten?)abgesehen davon finde ich nicht, dass man bei einer text-ausgabe auf die geschwindigkeit achten sollte; ich habe zumindest noch niemanden gesehen, der 50000 wörter in fünf millisekunden lesen kann.
-
std::endl ist überflüssig.