Konsolenausgabe buffern



  • Der aus dem Westen ... schrieb:

    Einen Buffer innerhalb der Klasse zu verwenden erscheint mir logisch, aber die Frage ist, wann der Inhalt des Buffers an die Konsole/an die Datei mit dem Handle geschickt werden soll. Derzeit sehe ich hier nur zwei Präzedenzfälle:

    1. Wenn vom Benutzer eine Eingabe verlangt werden könnte.
    2. Wenn der Buffer überläuft.

    Beide unbefriedigend.

    Der aus dem Westen ... schrieb:

    Dies abzufangen ist kein Problem, nur fürchte ich, dies könnte bei rechenintensiven Prozeduren zu lange dauern. Wenn eine Prozedur die ganze Zeit Statusmeldungen in den Buffer schreibt, dieser aber nicht überläuft, und die Prozedur plötzlich eine Exception wirft, welche dafür sorgt, dass ich den Buffer nicht mehr an die Konsole oder das Logging-Handle schicken an, was mache ich dann? Ich bitte um Denkanstöße für diese Problematik und an Ereignisse, welche ich eventuell nicht bedacht habe.

    Also Du mußt natürlich in einen Buffer schreiben für den hohen Durchsatz. Denn die Konsole von Windows ist arschlahm(!). Und Du möchtest schnell Ergebnisse sehen.

    Also mach einfach Folgendes: Du schreibst immer in den Buffer (z.B. 8192 Bytes) und wenn er voll ist, geht er raus. Soweit so gut. Aber Du möchtest nicht endlos warten, wenn die 8192 Bytes mal nicht voll werden, weil zum Beispiel mal 60 Sekunden lang kein Log-Traffic ist. Das löse so: Du schreibst heraus, wenn der Buffer voll ist, UND Du schreibst heraus, wenn ein 10-Millisekunden-Timer zuschlägt.

    Jetzt haben wir aber konstante Rechenlast durch den Timer. Das ändern wir. Ein zweiter Thread ist für das Ausgeben zuständig. Er wartet auf ein Event. Du schickst das Event bei jeder Log-Ausgabe. Falls 60 Sekunden lang keine Ausgabe kommt, schläft der Ausgabethread auch 60s. Soweit so gut. Aber willst natürlich den Ausgabethread nicht bei jeder Zeile wecken. Dafür machst Du in ihn noch ein Sleep(10); rein. Damit er maximal 100-mal pro Sekunde was ausgibt. Und Du willst nicht immer sowas teures wie einen Event anfassen, wenn Du was ausgibst. Das kannste dann noch mit einer atomic Variablen schützen.

    Und siehe da, es ist 1% schneller. Warum? Weil Windows doch den ganzen Scheiß nacheinander auf die Konsole rotzt. Und das ist langsam. Die Anzahl der Win-API-Aufrufe ist hier mal überhaupt kein Problem (im Gegensatz zu Dateiausgaben, die ja auch viel schneller als die Konsole-Ausgaben sind), sondern es ist wirklich der Kack, daß jeder Buchstabe auf den Bildschirm gerendert wird und jedes Scrollen ausgeführt wird und böse Zungen behaupten, daß die Konsole generft wird, um DOS-Programme endlich aus dem Markt zu schießen.

    Nächste Verbesserung, Du baust eine cout-Klasse, die NICHT einfach alles nach stdout streamt, sondern vorher abfragt, wie groß das sichtbare Fenster ist und selber chars in das lokal gebufferte Fenster schreibt und bei Überlauf selber scrollt und dieses Abbild wird nur (mit allen Tricks) 100-mal pro Sekunde auf die echte Konsole gerotzt. Statt verschieben noch einen Ringbuffer von Zeilen und es ist schon recht locker.

    Anscheinend habe ich das auch mal gemacht. Als Tech-Demo:
    http://www.normannia.de/members/~volkard/prime.exe
    (Die Primzahlen bis 10Mio ausgeben, dauerte damals VIEL länger, als sie zu berechnen, das Prog optimierte die Ausgabe und gewann, höhö :xmas2: )

    edit: Auch gut, bei mehrerehn Threads Zeilen der Art cout<<"test"<<i<<"foo"<<j; so hinmachen, daß über die ganze Zeile nur einmal gelockt wird und nicht bei jedem <<. Macht nicht nur schneller, sondern sorgt dafür, daß sich die Log-Zeilöen nicht vermischen. Geht mit ein wenig Phantasie, const& und mutable.

    edit2: Der nächste Schritt wäre, Ausgabeberechnungen wie itoa sogar zu lazy zu machen und nur auszuführen, wenn sie tatsächlich angezeigt werden. Davor hatte ich mich bisher gedrückt. Das würde dann unvorstellbar blasen…
    Für den vollen Spaß gehört da noch eine String-Klassen-Gemeinschaft (incl String-Literalen, die ihre Größe kennen und Small-Strings, die die Nutzdaten drin haben, und Foreigns-Strings, die zum Beispiel, auf memrory-mapped Datei-Inhalte zeigen) dazu, die per Expression Templates niemals strlen oder ein Äquivalent benutzen muß. Für zum Beispiel das Zusammensetzen von SQL-Statements oder HTML-Ausgaben.



  • volkard schrieb:

    Also Du mußt natürlich in einen Buffer schreiben für den hohen Durchsatz. Denn die Konsole von Windows ist arschlahm(!). Und Du möchtest schnell Ergebnisse sehen.

    Also mach einfach Folgendes: Du schreibst immer in den Buffer (z.B. 8192 Bytes) und wenn er voll ist, geht er raus. Soweit so gut. Aber Du möchtest nicht endlos warten, wenn die 8192 Bytes mal nicht voll werden, weil zum Beispiel mal 60 Sekunden lang kein Log-Traffic ist. Das löse so: Du schreibst heraus, wenn der Buffer voll ist, UND Du schreibst heraus, wenn ein 10-Millisekunden-Timer zuschlägt.

    Soweit war ich mit meinen Überlegungen auch schon gekommen. Und wie du unten bemerkst, ist das immer noch zu rechenintensiv.

    volkard schrieb:

    Jetzt haben wir aber konstante Rechenlast durch den Timer. Das ändern wir. Ein zweiter Thread ist für das Ausgeben zuständig. Er wartet auf ein Event. Du schickst das Event bei jeder Log-Ausgabe. Falls 60 Sekunden lang keine Ausgabe kommt, schläft der Ausgabethread auch 60s. Soweit so gut. Aber willst natürlich den Ausgabethread nicht bei jeder Zeile wecken. Dafür machst Du in ihn noch ein Sleep(10); rein. Damit er maximal 100-mal pro Sekunde was ausgibt. Und Du willst nicht immer sowas teures wie einen Event anfassen, wenn Du was ausgibst. Das kannste dann noch mit einer atomic Variablen schützen.

    1. Log-Ausgabe? Du meinst das Schreiben der Statusmeldung auf die Konsole und der Debuginformationen in die Log-Datei?

    2. Du meinst das Senden des Events, um den Thread aufzurufen? Das würde, wenn ich dich recht verstehe, nur passieren, wenn der Buffer rappelvoll ist ODER der 10-Millisekundentimer zuschlägt. Aber hier sehe ich wieder die Problematik - selbst wenn ich den Thread schlafen lasse - dass mir die Auslastung wieder zu hoch geht. Das Problem, überprüfen zu müssen, ob was im Buffer drin ist, wollte ich eigentlich durch eine Variable lösen, welche die gegenwärtigen Bytes im Buffer hält - bei 0 muss nichts aufgerufen werden.

    volkard schrieb:

    Und siehe da, es ist 1% schneller. Warum? Weil Windows doch den ganzen Scheiß nacheinander auf die Konsole rotzt. Und das ist langsam. Die Anzahl der Win-API-Aufrufe ist hier mal überhaupt kein Problem (im Gegensatz zu Dateiausgaben, die ja auch viel schneller als die Konsole-Ausgaben sind), sondern es ist wirklich der Kack, daß jeder Buchstabe auf den Bildschirm gerendert wird und jedes Scrollen ausgeführt wird und böse Zungen behaupten, daß die Konsole generft wird, um DOS-Programme endlich aus dem Markt zu schießen.

    Wut? Das heißt, selbst wenn ich den Rotz ERST in den Arbeitsspeicher füllen lasse, um ihn nachher auf die Konsole zu schieben (die ist primär wichtiger, wenn auch nicht höchste Priorität), zerhaut mir Windows, weil die Buchstaben - so habe ich dich verstanden - einzeln ausgegeben werden? Toll. Großartig. 'Ne Grafikschnittstelle können sie programmieren, aber für die Konsole reicht's im Budget nicht, was?

    Nächste Verbesserung, Du baust eine cout-Klasse, die NICHT einfach alles nach stdout streamt, sondern vorher abfragt, wie groß das sichtbare Fenster ist und selber chars in das lokal gebufferte Fenster schreibt und bei Überlauf selber scrollt und dieses Abbild wird nur (mit allen Tricks) 100-mal pro Sekunde auf die echte Konsole gerotzt. Statt verschieben noch einen Ringbuffer von Zeilen und es ist schon recht locker.

    Gut, hier habe ich überhaupt keinen Ansatz. Meinst du damit, ich soll einen Screenbuffer erstellen, hier den Rotz einladen und dann über ScrollConsoleScreenBuffer (oder ähnlich, hab's gerade nur in meinem Fenster auf) den Datenblock setzen?



  • Der aus dem Westen ... schrieb:

    Meinst du damit, ich soll einen Screenbuffer erstellen, hier den Rotz einladen und dann über ScrollConsoleScreenBuffer (oder ähnlich, hab's gerade nur in meinem Fenster auf) den Datenblock setzen?

    Ja.
    Und lies mein Kackposting noch ein paar mal. Ist harter Tobak.



  • volkard schrieb:

    Ja.
    Und lies mein Kackposting noch ein paar mal. Ist harter Tobak.

    Ich habe zugegebenermaßen lange gebraucht, um es zu verstehen ... na gut, so was wie eine Grundstruktur habe ich, jetzt muss ich schauen, wie es es implementiere. Dank dir.



  • [quote="volkard"]

    Der aus dem Westen ... schrieb:

    Meinst du damit, ich soll einen Screenbuffer erstellen, hier den Rotz einladen und dann über ScrollConsoleScreenBuffer (oder ähnlich, hab's gerade nur in meinem Fenster auf) den Datenblock setzen?

    Und keinen http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx
    Die sind, glaube ich, auch generft.
    Einfach selber im RAM machen. '\n' und '\r' und '\t' sonderbehandeln ist ja nicht so schwer. Auf die anderen Sonderzeichen einfach verzichten, die hat man nicht zu loggen.



  • volkard schrieb:

    Und keinen http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx
    Die sind, glaube ich, auch generft.
    Einfach selber im RAM machen. '\n' und '\r' und '\t' sonderbehandeln ist ja nicht so schwer. Auf die anderen Sonderzeichen einfach verzichten, die hat man nicht zu loggen.

    Das Dokumentationsfenster habe ich auch gerade auf. Was lässt dich so denken? Gibt es Anhaltspunkte dafür?



  • Der aus dem Westen ... schrieb:

    Das Dokumentationsfenster habe ich auch gerade auf. Was lässt dich so denken? Gibt es Anhaltspunkte dafür?

    Ich meine, mich daran zu erinnern.
    Bin aber nicht ganz sicher. Ich wollte damals absolute Performance. Auch WinAPI-Aufrufge sparen. Und jeder denkt ja mal über ein eigenes BS nach, wie man eine Konsole effizient baut, ist ein Teil dessen. Daher könnte es sein, daß ich keine Anhaltspunkte dafür hatte. Kann echt sein.
    Aber ich meine, mich zu erinnern, auch die Offscreen-Buffer als wirklich langsam beim Scrollen ausgemessen zu haben. Kein Ringbuffer eben. Und noch viel schlechter.

    Da das jetzt sehr vage ist, mußt Du es wohl erst mal ausmessen. Hau 1000000 mal '\n' rein oder so und schau mal, ob es VIEL langsamer ist als so viele '\n' in eine Datei. Und sag mir die Ergebnisse, damit ich mich wieder kalibrieren kann.



  • Ich bin gerade bei der Implementierung des Timers, um die Ausgabe damit zu steuern, aber ich glaube, ich habe da einen Designfehler oder dich nicht richtig verstanden.

    Deinem Ratschlag zufolge sollte ich einen einfachen Callback-Timer für eine Funktion einrichten, wäre soweit kein Problem. Allerdings handelt es sich um eine Klasse, welche direkt mehrere Ausgabemodi kennen soll. Wenn ein Objekt der Klasse erstellt werden soll, muss bereits ein HANDLE existieren, welches an den ctor übergeben wird, hier wird der Rotz dann geschrieben (derzeit weiß die Klasse aber nicht, ob es sich um ein File- oder um ein Console- HANDLE handelt, was später bei der Formatierung, wie du sie mit dem Ringbuffer vorgeschlagen hast, wichtig sein könnte. Beim File-Handle muss ich nicht rendern lassen ...).

    Da die WinAPI keine __thiscall s kennt und TIMERPROC einen astreinen __stdcall verlangt, entschied ich mich, den Thread ausserhalb der Klasse zu verlagern. Das löst natürlich nicht die Problematik, dass die Parameterübergabe von SetTimer übernommen wird und ich - zumindest derzeit - nur die Timer-ID als Referenz von der Funktion erhalte. Auf eine ID-Handle-Lookuptable, um anhand der ID das verwendete Handle bestimmen zu können, bin ich auch schon gekommen, aber ich habe keinen Bock, die Funktion erst diese Table durchgesehen zu lassen, um dann bestimmen zu können, dass der Buffer leer ist und die Funktion wieder zurückspringen kann. Hast du auch hierfür einen Ratschlag?



  • Der aus dem Westen ... schrieb:

    Deinem Ratschlag zufolge sollte ich einen einfachen Callback-Timer für eine Funktion einrichten, wäre soweit kein Problem.

    Eigentlich wäre mir ein Rausschreib-Thread lieber. Aber das mit dem Timer geht auch ok, denke ich.

    Allerdings handelt es sich um eine Klasse, welche direkt mehrere Ausgabemodi kennen soll.

    Kannst die verschiedenen Stream-Sorten von einer gemeinsamen Basisklasse erben lassen. Oder sogar einen streambuf schreiben.
    Wobei ich das gar nicht für Dateien vorgesehen hatte. Aber naja, kannst auch Dateien damit "selten aber nie sehr spät" flushen.

    (derzeit weiß die Klasse aber nicht, ob es sich um ein File- oder um ein Console- HANDLE handelt, was später bei der Formatierung, wie du sie mit dem Ringbuffer vorgeschlagen hast, wichtig sein könnte.

    Aber erst, beim Rausschreiben des streambufs selbst. Sowas wie ein op<< weiß davon noch nichts.

    entschied ich mich, den Thread ausserhalb der Klasse zu verlagern. Das löst natürlich nicht die Problematik, dass die Parameterübergabe von SetTimer übernommen wird und ich - zumindest derzeit - nur die Timer-ID als Referenz von der Funktion erhalte. Auf eine ID-Handle-Lookuptable,

    Der Thread braucht doch dann keinen Timer mehr, er kann Sleep() machen.
    Und pro Stream einen Thread und ein Event.
    Wer was in einen Stream geschrieben hat, setzt dessen Event.
    Der Thread wartet, schreibt, sleept im Kreis.



  • volkard schrieb:

    Eigentlich wäre mir ein Rausschreib-Thread lieber. Aber das mit dem Timer geht auch ok, denke ich.

    Das war nicht ganz ersichtlich nach dem Hüh und Hott. 😉 Aber ein Thread ist auch kein Problem.

    volkard schrieb:

    Kannst die verschiedenen Stream-Sorten von einer gemeinsamen Basisklasse erben lassen. Oder sogar einen streambuf schreiben.
    Wobei ich das gar nicht für Dateien vorgesehen hatte. Aber naja, kannst auch Dateien damit "selten aber nie sehr spät" flushen.

    Das ist der Plan gewesen.

    volkard schrieb:

    Aber erst, beim Rausschreiben des streambufs selbst. Sowas wie ein op<< weiß davon noch nichts.

    Klar, weiß ich. Am Ende basiert es sowieso auf den Funktionen, die Windows mir zur Verfügung stellt.

    volkard schrieb:

    Der Thread braucht doch dann keinen Timer mehr, er kann Sleep() machen.
    Und pro Stream einen Thread und ein Event.
    Wer was in einen Stream geschrieben hat, setzt dessen Event.
    Der Thread wartet, schreibt, sleept im Kreis.

    Ja, klar. Der Thread, aber nicht der Timer. Und du hast nicht gesagt, "Vergiss das mit dem Timer.", du hast nur gesagt: "Das ändern wir. Ein zweiter Thread ist für das Ausgeben zuständig." Und weil ich mir daraus keinen Reim machen konnte, habe ich "Thread" mit "asyncron aufgerufene Funktion" gleichgesetzt.

    Aber gut, bei einem Thread kann ich die Parameter direkt diktieren, so brauche ich keine Lookuptabel. Gut, ist das auch geklärt.


Anmelden zum Antworten