Ist die VC-STL-Implementierung inzwischen gut?



  • sfdfdsfdf schrieb:

    Könnte man nicht eine Bibliothek schreiben, die sowas wie mycout << "Hallo " << string << int << '!'; in ein passendes (f)printf("Hallo"); (f)printf(string.c_str()); (f)printf(inttostring(int)); umwandelt? Dann wäre man mindestens genauso schnell wie C (eher schneller, weil die format-Strings entfallen), und hätte trotzdem die Typsicherheit von C++.

    Oder gibt es das schon?

    Oder noch besser fputs(string, stdout) http://www.cplusplus.com/reference/cstdio/fputs/ weil das keine Formatzeichen sucht.



  • sfdfdsfdf schrieb:

    Könnte man nicht eine Bibliothek schreiben, die sowas wie mycout << "Hallo " << string << int << '!'; in ein passendes (f)printf("Hallo"); (f)printf(string.c_str()); (f)printf(inttostring(int)); umwandelt? Dann wäre man mindestens genauso schnell wie C

    Leider nein, das wäre nicht sicher mindestens genauso schnell. Nicht so wie du es zeigst.
    Aber ja, grundsätzlich könnte man da schon 'was machen.
    Müsste halt mal wer machen.



  • hustbaer schrieb:

    sfdfdsfdf schrieb:

    Könnte man nicht eine Bibliothek schreiben, die sowas wie mycout << "Hallo " << string << int << '!'; in ein passendes (f)printf("Hallo"); (f)printf(string.c_str()); (f)printf(inttostring(int)); umwandelt? Dann wäre man mindestens genauso schnell wie C

    Leider nein, das wäre nicht sicher mindestens genauso schnell. Nicht so wie du es zeigst.

    Warum?



  • Weil da dann ein Haufen printf() Aufrufe wären, wohingegen ein printf() Aufruf nur ein printf() Aufruf ist.
    Und dass der Compiler den ganzen Overhead der vielen printf() Aufrufe durch Inlining und schlaue statische Analyse ja wegmachen könnte, das kann man vielleicht argumentieren wenn man mutig ist, aber real haut es halt einfach nicht hin.

    Was ja auch genau einer der grössten Vorteile von printf() gegenüber iostreams ist, nämlich dass man viel mit einem einzigen printf() Aufruf machen kann.

    Wenn dann würde sich eher etwas anbieten was Code ala

    Writer __tmp__(stdout);
        __tmp__.reserve(compiler generierte schätzung);
        __tmp__.write("Blah i = ");
        __tmp__.write(i);
        __tmp__.write("\n");
        __tmp__.and_now_write_it_all_at_once();
    

    generiert. (Als Beispiel für Code der als Ersatz für ein einziges printf() generiert werden müsste.)

    Dadurch könnte man gewisse teure Dinge wie das Ermitteln der Locale der diverser Formatierungs-Settings 1x im ctor von Writer machen. Und schön lokal im Writer puffern ohne dabei threadsafe sein zu müssen o.ä. Die vielen (ganz real inlinebaren) Aufrufe von Writer::write etc. tun dann vermutlich auch nicht mehr sehr weh.
    Andrerseits erzeugt das einigermassen aufgeblasenen Code, eben weil viele Funktionen geinlined werden bzw. ansonsten halt die ganzen push-call-pop Overhead eines echten Funktionsaufrufs. Bei Microbenchmarks ist das dann super-schnell, aber in real müllt es dir den I-Cache zu, und kann damit u.U. mehr schaden als nutzen.

    Format-Strings sind diesbezüglich schon ziemlich gut, das signifikant schneller zu machen (oder auch nur in allen Fällen mindestens gleich schnell) ist nicht einfach.



  • Oha, ich sehe gerade, ich hab' mich da vertan.

    Stimmt es wirklich dass printf() auf gar keine locale-spezifischen Dinge zugreifen muss? Mach aber auch voll Sinn 🙂 Dann würde ein Punkt auf der "Overhead pro printf call" Liste wegfallen. Bzw. wenn man eben puts statt printf einsetzt natürlich sowieso.
    Trotzdem bleibt IMO noch einiges an Overhead, speziell wenn Zugriff auf stdout thread-safe sein soll. Was mMn. immer so sein sollte.

    Und der Overhead fällt auch nicht ganz weg, er verlagert sich bloss in Funktionen wie datetostring() (analog zu dem inttostring() in deinem Beispiel nur halt z.B. für Datumswerte, oder sonstige "tostring" Funktionen die irgendwas locale-spezifisch formatieren sollen).

    Was nebenbei bemerkt auch gleich nochmal ein Punkt in deinem Beispiel ist der Overhead erzeugt...

    Und zwar kann man so ein inttostring() eigentlich kaum wirklich performant umsetzen. Entweder man muss nen (dynamisch allozierten) String ( std::string etc.) zurückgeben - da kostet die Allokation. (Für kleine Integers kommt man vielleicht noch mit der SSO optimierten Strings ohne Allokation davon, aber für grössere Werte oder eben Dinge wie Daten... eher nicht.)
    Oder aber man gibt nen char-Zeiger auf nen thread-lokalen Buffer zurück. Das ist einerseits schonmal grauslich, und andrerseits ist Zugriff auf thread-local Zeugs auch nicht gratis.

    Das von mir skizzierte Muster bietet da mehr Optimierungspotential, bzw. generall das Potential auch ohne besonders gefinkelte Optimierungen schneller zu sein. Und nochmal schneller wird es natürlich wenn man die "writer" für mehrere Zeilen wiederverwendet. Und dann kann man gleich sowas wie

    stream_writer w(std::cout);
    w << "Hallo, mein Integer hat den Wert " << i << " und es ist gerade " << dateAndTime << ".\n";
    w << ...
    w << ...
    w.flush();
    

    machen.
    Was jetzt so aussieht als könnte man es gleich mit std::ostream machen. Es gibt aber IMO wichtige Unterschiede:
    * So ein Writer muss nie nie nie selbst thread-safe sein, egal auf was für einen Stream er rausschreibt
    * Man kann definieren dass der Writer nur bei seiner Erzeugung die Thread-Locale abfragt, und sich danach nicht um Änderungen kümmert die seit dem passiert sein könnten
    * Man könnte sogar definieren dass der Writer grundsätzlich immer die default C-Locale verwendet, bis man ihn explizit auffordert 'was anderes zu tun
    * Man kann Writer Klassen bauen die einen "eingebauten" Buffer von ein paar KB haben, die man dann einfach auf dem Stack instanzieren kann und damit "gratis" an einen brauchbaren Buffer kommt

    Weiters müsste man sich nicht mehr darum kümmern irgendwelche Änderungen an den Stream-Einstellungen rückgängig zu machen - Änderungen an den Einstellungen würde man ja nur am Writer machen, und den teilt man sich idealerweise nicht mit wildfremdem Code.

    Das löst natürlich immer noch nicht das Problem mit dem Code-Bloat, aber es wäre schonmal ein Schritt in Richtung bessere Performance. Und sowas wie boost::format (=type safe) könnte man leicht dazubauen, wenn man den Code-Bloat verringern möchte.

    Wobei das natürlich auch mit Streams ginge, also so ala

    std::cout << my_cool_formatter("Hi, i = %1%, zeit = %2%\n.") << i << dateAndTime;
    

    ps: Sorry wegen OT-Gespamme 🙂


  • Mod

    printf hat schon einen recht heftigen Overhead, da der Formatstring zur Laufzeit ausgewertet wird. Daher ist eine gängige Optimierung, dies möglichst gut zur Compilezeit zu machen, z.B. printf("%s\n", ...) -> puts(...).

    Wäre die iostream Langsamkeit nicht gelöst, wenn man das ganze Locale-Zeug weglassen oder optional machen würde? Dann hat man doch das beste beider Welten (zumindest bezüglich Laufzeit; mir ist schon klar, dass so ein Formatstring auch recht komfortabel ist). Dann wird alles zur Compilezeit optimal auf spezialisierte Lightweight-Funktionen aufgelöst. Das derzeitige Hauptproblem ist doch, dass im C++-Standard derzeit jede Beschreibung einer formatierten Ausgabefunktion mit einem langen Absatz über Proxyobjekte und Locales beginnt anstatt direkt mit dem Teil zu beginnen, der tatsächlich etwas ausgibt. Die Grundidee mit der Verkettung von Funktionsaufrufen ist hingegen durchaus gut.



  • Das Hauptproblem wäre dann sicher weg, ja.
    Ich bin aber echt nicht so 100% überzeugt dass Format-Strings so böse sind. Bzw. umgekehrt: ich schätze dass Format-Strings für viele Anwendungsfälle besser sind als Code-Bloat durch zig Funktionsaufrufe.

    So ein %d braucht zwei Byte, dazu kommt dann noch ein push für das "..." Argument, was je nach Architektur vermutlich irgendwo zwischen 2 und 12 Byte haben wird. Also so 4~14 Byte Cache-Belastung. Dafür kann man sich in vielen Fällen vermutlich schon ein bisschen was an Parsing-Overhead leisten.* Also z.B. wo man Code hat wo alle 2-3 Zeilen ein printf/cout vorkommt, immer leicht unterschiedlich - also keine Code-duplication, und wo man halt nicht einen engen Loop hat der wenige Meldungen immer wieder und wieder rausschreibt. Wenn dann die meisten printf-Codepfade noch kalt bis "lauwarm" sind... dann haut glaube ich das I-Cache Clobbering das man damit verursacht wenn man cout-Style-call-Chaining verwendet ordentlich rein. printf-Style hat da glaube ich schon nen Vorteil. Aber wie gesagt, dazu könnte man sich ja vermutlich dann auch 'was überlegen, wenn man erstmal den ganzen Locking- und Locale-Overhead weg hat.

    *: Ich könnte mir sogar vorstellen dass es einige Fälle gibt, wo es vorteilhaft wäre wenn man ne Formatierungs-Syntax hätte mit der man mehrere Werte aus ner struct rauslesen kann, also so Pointer+Offset-mässig. Ala %[1+16]d für "nimm das erste ... Argument, das ist ein Zeiger, und 16 Byte weiter steht ein Integer (d) und den möchte ich ausgedruckt haben". Weil man dadurch statt mehreren push bloss noch eins bräuchte falls alle Infos aus der selben struct kommen.



  • ps: Einen Vorteil der stream_writer (oder wie man es nennen möchte) Variante sehe ich auch darin dass man relativ einfach Code schreiben kann der mehrere Outputs macht, auch mit ein paar anderen Aufrufen dazwischen die man nicht in die Operator-Chain reinbekommt, ohne dass der Stream bei jedem Output angefasst werden muss. Eben weil man dadurch den für std::cout vorgeschriebenen Thread-Synchronisierungs Overhead senken kann. Denn das wäre etwas was immer noch bliebe, selbt wenn man den iostreams die Locales abgewöhnen würde.

    Wenn man das ganze dann nicht auf Streams einschränkt, sondern die stream_writer Klasse gleich als "Formatter" Klasse auslegt, die in eine beliebige char-Sink schreiben kann, dann hätte man auch gleich noch den Anwendungsfall StringBuilder erschlagen. Ohne dass man dazu den Umweg über einen stringstream gehen muss.

    ----

    Keine Ahnung ob es dazu schon brauchbare OSS Implementierungen gibt, aber grundsätzlich fände ich das mal ein recht cooles Projekt. Hmmm...



  • Ich hab mal angefangen:

    #pragma once
    
    #include <cstdio>
    #include <string>
    #include <exception>
    #include <array>
    #include <algorithm>
    
    class new_line {};
    class flush_stream {};
    
    class stream_writer
    {
    	std::FILE* ostream;
    
    	std::string buffer;
    
    public:
    	stream_writer(std::FILE* ostream_) : ostream(ostream_) {}
    
    	void flush() { if (std::fputs(buffer.c_str(), ostream) == EOF) throw std::exception("fputs failed"); buffer.clear(); }
    
    	friend stream_writer& operator<<(stream_writer& stream, char c) { stream.buffer += c; return stream; }
    	friend stream_writer& operator<<(stream_writer& stream, const char* s) { stream.buffer += s; return stream; }
    	friend stream_writer& operator<<(stream_writer& stream, const std::string& s) { stream.buffer += s; return stream; }
    	friend stream_writer& operator<<(stream_writer& stream, unsigned int i)
    	{
    		if (i == 0)
    			stream.buffer += '0';
    		while (i != 0)
    		{
    			stream.buffer += static_cast<char>(i % 10) + '0';
    			i /= 10;
    		}
    		return stream;
    	}
    	friend stream_writer& operator<<(stream_writer& stream, int i)
    	{
    		if (i < 0)
    		{
    			stream.buffer += '-';
    			i = -i;
    		}
    		return stream << static_cast<unsigned int>(i);
    	}
    	friend stream_writer& operator<<(stream_writer& stream, new_line)
    	{
    		stream.buffer += '\n';
    		return stream << flush_stream();
    	}
    	friend stream_writer& operator<<(stream_writer& stream, flush_stream)
    	{
    		stream.flush();
    		return stream;
    	}
    };
    
    template <size_t buffer_size>
    class stream_reader
    {
    	std::FILE* istream;
    
    	std::array<char, buffer_size> buffer;
    
    	void read()
    	{
    		if (std::fgets(buffer.data(), buffer_size, istream) == nullptr)
    			throw std::exception("fgets failed");
    	}
    public:
    	stream_reader(std::FILE* istream_) : istream(istream_) {}
    
    	friend stream_reader& operator>>(stream_reader& stream, char& c)
    	{
    		stream.read();
    		c = stream.buffer[0];
    		return stream;
    	}
    	friend stream_reader& operator>>(stream_reader& stream, std::string& s)
    	{
    		stream.read();
    		s = std::string(stream.buffer.begin(), std::find(stream.buffer.begin(), stream.buffer.end(), '\n'));
    		return stream;
    	}
    	friend stream_reader& operator>>(stream_reader& stream, unsigned int& i)
    	{
    		stream.read();
    		i = 0;
    		char c = stream.buffer[0];
    		auto index = stream.buffer.first();
    		while (c >= '0' && c <= '9')
    		{
    			i = (i * 10) + static_cast<unsigned int>(c - '0');
    			++index;
    			c = *index;
    		}
    		return stream;
    	}
    };
    

    Verbesserungsvorschläge? Ergänzungen? Kann man zwischen signed- und unsigned-Datentypen irgendwie per Templates unterscheiden? Dass man nicht für jeden Integerdatentyp eine eigene Funktion schreiben muss.



  • Wenn ich hustbaer verstanden habe, sollte der Puffer
    nicht dynamisch angelegt werden, sondern als direkter Member mit fester Größe (std::array oder so. Die Größe kann als Templateparameter angegeben werden).



  • Japp. Sowas schnell mal für ein spezifisches Projekt zusammenfummeln geht auch relativ schnell. Ne generische, saubere, standardkonforme und ausreichend flexible Implementierung ist aber wesentlich mehr Aufwand.

    Mit "fände ich ein recht cooles Projekt" meinte ich nicht dass man das hier so zwischen Tür und Angel im Forum mal eben macht. Wenn dann müsste man das "ordentlich" angehen und vermutlich auch einiges an Zeit da einplanen (und dann investieren).


Anmelden zum Antworten