Wie schlimm ist es, println () oft aufzurufen, als Zeichenfolgen zusammenzufügen und einmal aufzurufen?

23

Ich weiß, dass die Ausgabe an die Konsole ein kostspieliger Vorgang ist. Im Interesse der Lesbarkeit von Code ist es manchmal hilfreich, eine Funktion aufzurufen, um Text zweimal auszugeben, anstatt eine lange Zeichenfolge als Argument zu verwenden.

Zum Beispiel, wie viel weniger effizient ist es zu haben

System.out.println("Good morning.");
System.out.println("Please enter your name");

gegen

System.out.println("Good morning.\nPlease enter your name");

Im Beispiel ist der Unterschied nur ein Anruf, println()aber was ist, wenn es mehr ist?

In einem verwandten Hinweis können Aussagen, die das Drucken von Text betreffen, beim Anzeigen des Quellcodes merkwürdig aussehen, wenn der zu druckende Text lang ist. Angenommen, der Text selbst kann nicht gekürzt werden. Was kann getan werden? Sollte dies ein Fall sein, in dem mehrere println()Anrufe getätigt werden? Jemand sagte mir einmal, eine Codezeile sollte nicht mehr als 80 Zeichen (IIRC) enthalten. Was würden Sie also tun?

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Gilt das auch für Sprachen wie C / C ++, da jedes Mal, wenn Daten in einen Ausgabestream geschrieben werden, ein Systemaufruf erfolgen muss und der Prozess in den Kernelmodus wechseln muss (was sehr kostspielig ist)?

Celeritas
quelle
Auch wenn dies nur ein sehr kleiner Code ist, muss ich sagen, dass ich mich das gleiche gefragt habe. Wäre nett, die Antwort auf diese ein für alle Mal zu bestimmen
Simon Forsberg
@ SimonAndréForsberg Ich bin nicht sicher, ob es auf Java anwendbar ist, da es auf einer virtuellen Maschine ausgeführt wird, aber in niedrigeren Sprachen wie C / C ++ würde ich mir vorstellen, dass es kostspielig wäre, wenn jedes Mal etwas in einen Ausgabestream geschrieben wird, ein Systemaufruf muss gemacht werden.
1
Ich muss sagen, dass ich den Punkt hier nicht sehe. Wenn ich über ein Terminal mit einem Benutzer interagiere, kann ich mir keine Leistungsprobleme vorstellen, da normalerweise nicht so viel zu drucken ist. Und Anwendungen mit einer grafischen Benutzeroberfläche oder einer Webanwendung sollten in eine Protokolldatei schreiben (normalerweise unter Verwendung eines Frameworks).
Andy
1
Wenn du guten Morgen sagst, tust du es ein- oder zweimal am Tag. Optimierung ist kein Problem. Wenn es etwas anderes ist, müssen Sie ein Profil erstellen, um zu wissen, ob es ein Problem ist. Der Code, den ich für die Protokollierung verwende, verlangsamt den Code auf unbrauchbar, es sei denn, Sie erstellen einen mehrzeiligen Puffer und geben den Text in einem Aufruf aus.
Mattnz

Antworten:

29

In diesem Spannungsfeld gibt es zwei „Kräfte“: Leistung vs. Lesbarkeit.

Lassen Sie uns zuerst das dritte Problem angehen, lange Schlangen:

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Die beste Möglichkeit, dies zu implementieren und die Lesbarkeit zu erhalten, ist die Verwendung der Zeichenfolgenverkettung:

System.out.println("Good morning everyone. I am here today to present you "
                 + "with a very, very lengthy sentence in order to prove a "
                 + "point about how it looks strange amongst other code.");

Die Verkettung der Zeichenfolgenkonstanten erfolgt zur Kompilierungszeit und hat keinerlei Auswirkungen auf die Leistung. Die Zeilen sind lesbar und Sie können einfach weitermachen.

Nun zu den:

System.out.println("Good morning.");
System.out.println("Please enter your name");

gegen

System.out.println("Good morning.\nPlease enter your name");

Die zweite Option ist deutlich schneller. Ich werde etwa 2X so schnell vorschlagen .... warum?

Denn 90% (mit einer großen Fehlerquote) der Arbeit beziehen sich nicht auf das Ausgeben der Zeichen in die Ausgabe, sondern auf den Aufwand, der erforderlich ist, um die Ausgabe für das Schreiben in die Ausgabe zu sichern.

Synchronisation

System.outist ein PrintStream. Alle mir bekannten Java-Implementierungen synchronisieren den PrintStream intern: Siehe den Code auf GrepCode! .

Was bedeutet das für Ihren Code?

Dies bedeutet, dass Sie jedes Mal, wenn System.out.println(...)Sie Ihr Speichermodell synchronisieren, überprüfen und auf eine Sperre warten. Alle anderen Threads, die System.out aufrufen, werden ebenfalls gesperrt.

In Single-Thread-Anwendungen wird die Auswirkung System.out.println()häufig durch die E / A-Leistung Ihres Systems begrenzt. Wie schnell können Sie in eine Datei schreiben? In Multithread-Anwendungen kann das Sperren ein größeres Problem darstellen als die E / A.

Spülen

Jeder Druck wird geleert . Dies bewirkt, dass die Puffer gelöscht werden, und löst ein Schreiben auf Konsolenebene in die Puffer aus. Der Aufwand, der hier unternommen wird, hängt von der Implementierung ab, es versteht sich jedoch im Allgemeinen, dass die Leistung des Spülvorgangs nur zu einem kleinen Teil mit der Größe des zu spülenden Puffers zusammenhängt. Mit dem Leeren ist ein erheblicher Aufwand verbunden, da Speicherpuffer als fehlerhaft markiert sind, die virtuelle Maschine E / A-Vorgänge ausführt usw. Es ist eine offensichtliche Optimierung, diesen Aufwand einmal statt zweimal zu verursachen.

Einige Zahlen

Ich habe folgenden kleinen Test zusammengestellt:

public class ConsolePerf {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            benchmark("Warm " + i);
        }
        benchmark("real");
    }

    private static void benchmark(String string) {
        benchString(string + "short", "This is a short String");
        benchString(string + "long", "This is a long String with a number of newlines\n"
                  + "in it, that should simulate\n"
                  + "printing some long sentences and log\n"
                  + "messages.");

    }

    private static final int REPS = 1000;

    private static void benchString(String name, String value) {
        long time = System.nanoTime();
        for (int i = 0; i < REPS; i++) {
            System.out.println(value);
        }
        double ms = (System.nanoTime() - time) / 1000000.0;
        System.err.printf("%s run in%n    %12.3fms%n    %12.3f lines per ms%n    %12.3f chars per ms%n",
                name, ms, REPS/ms, REPS * (value.length() + 1) / ms);

    }


}

Der Code ist relativ einfach und gibt wiederholt entweder eine kurze oder eine lange Zeichenfolge aus, die ausgegeben werden soll. Die lange Zeichenfolge enthält mehrere Zeilenumbrüche. Es misst, wie lange es dauert, jeweils 1000 Iterationen zu drucken.

Wenn ich es auf dem Unix (Linux) Befehlszeile ausführen, und leiten das STDOUTzu /dev/null, und drucken Sie die tatsächlichen Ergebnisse STDERR, kann ich folgendes tun:

java -cp . ConsolePerf > /dev/null 2> ../errlog

Die Ausgabe (im Fehlerprotokoll) sieht folgendermaßen aus:

Warm 0short run in
           7.264ms
         137.667 lines per ms
        3166.345 chars per ms
Warm 0long run in
           1.661ms
         602.051 lines per ms
       74654.317 chars per ms
Warm 1short run in
           1.615ms
         619.327 lines per ms
       14244.511 chars per ms
Warm 1long run in
           2.524ms
         396.238 lines per ms
       49133.487 chars per ms
.......
Warm 99short run in
           1.159ms
         862.569 lines per ms
       19839.079 chars per ms
Warm 99long run in
           1.213ms
         824.393 lines per ms
      102224.706 chars per ms
realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Was bedeutet das? Lassen Sie mich die letzte Strophe wiederholen:

realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Dies bedeutet, dass die Ausgabe in jeder Hinsicht genauso lange dauert wie die Ausgabe der kurzen Zeile, obwohl die lange Zeile etwa fünfmal länger ist und mehrere neue Zeilen enthält.

Die Anzahl der Zeichen pro Sekunde ist auf lange Sicht fünfmal so hoch und die verstrichene Zeit ungefähr gleich.

Mit anderen Worten, skaliert Ihre Leistung in Bezug auf die Anzahl von printlns Sie haben, nicht , was sie drucken.

Update: Was passiert, wenn Sie auf eine Datei anstatt auf / dev / null umleiten?

realshort run in
           2.592ms
         385.815 lines per ms
        8873.755 chars per ms
reallong run in
           2.686ms
         372.306 lines per ms
       46165.955 chars per ms

Es ist viel langsamer, aber die Proportionen sind ungefähr gleich ....

rolfl
quelle
Einige Performance-Nummern hinzugefügt.
Rolfl
Sie müssen auch das Problem berücksichtigen, das "\n"möglicherweise nicht der richtige Zeilenabschluss ist. printlnschließt die Zeile automatisch mit den richtigen Zeichen ab, das \ndirekte Einfügen von a in die Zeichenfolge kann jedoch zu Problemen führen. Wenn Sie es richtig machen möchten, müssen Sie möglicherweise die Zeichenfolgenformatierung oder die line.separatorSystemeigenschaft verwenden . printlnist viel sauberer.
user2357112 unterstützt Monica
3
Das ist alles eine großartige Analyse, also mit Sicherheit +1, aber ich würde argumentieren, dass, sobald Sie sich für die Konsolenausgabe entschieden haben, diese kleinen Leistungsunterschiede aus dem Fenster fliegen. Wenn der Algorithmus Ihres Programms schneller ausgeführt wird als die Ausgabe der Ergebnisse (bei dieser kleinen Ausgabeebene), können Sie jedes Zeichen einzeln drucken, ohne den Unterschied zu bemerken.
David Harkness
Ich glaube, das ist ein Unterschied zwischen Java und C / C ++, dass die Ausgabe synchronisiert ist. Ich sage das, weil ich mich erinnere, ein Multithread-Programm geschrieben zu haben und Probleme mit verstümmelter Ausgabe zu haben, wenn verschiedene Threads versuchen zu schreiben, um auf die Konsole zu schreiben. Kann jemand dies überprüfen?
6
Es ist wichtig, sich auch daran zu erinnern, dass keine dieser Geschwindigkeiten von Bedeutung ist, wenn direkt neben die Funktion gestellt wird, die auf Benutzereingaben wartet.
VMROB
2

Ich denke nicht, dass ein Haufen printlns überhaupt ein Designproblem ist. Ich sehe es so, dass dies mit dem Static Code Analyzer eindeutig möglich ist, wenn es wirklich ein Problem ist.

Dies ist jedoch kein Problem, da die meisten Benutzer solche E / A-Vorgänge nicht ausführen. Wenn sie wirklich viele E / A-Vorgänge ausführen müssen, verwenden sie gepufferte Vorgänge (BufferedReader, BufferedWriter usw.). Wenn die Eingabe gepuffert ist, werden Sie feststellen, dass die Leistung ähnlich genug ist und Sie sich keine Sorgen um eine machen müssen Haufen printlnoder wenige println.

Also, um die ursprüngliche Frage zu beantworten. Ich würde sagen, nicht schlecht, wenn Sie printlnein paar Dinge ausdrucken, für die die meisten Leute verwenden würden println.

InformedA
quelle
1

In höheren Sprachen wie C und C ++ ist dies weniger problematisch als in Java.

Zunächst definieren C und C ++ die Verkettung von Zeichenfolgen zur Kompilierungszeit, sodass Sie so etwas wie Folgendes tun können:

std::cout << "Good morning everyone. I am here today to present you with a very, "
    "very lengthy sentence in order to prove a point about how it looks strange "
    "amongst other code.";

In einem solchen Fall ist das Verketten der Zeichenfolge nicht nur eine Optimierung, die Sie in der Regel (usw.) vom Compiler abhängig machen können. Vielmehr wird es direkt von den C- und C ++ - Standards gefordert (Phase 6 der Übersetzung: "Angrenzende Zeichenfolgen-Literal-Token werden verkettet.").

Obwohl C und C ++ den Compiler und die Implementierung etwas komplizierter machen, tragen sie etwas mehr dazu bei, die Komplexität der effizienten Ausgabe vor dem Programmierer zu verbergen. Java ist viel mehr eine Assemblersprache - jeder Aufruf wird System.out.printlndirekter in einen Aufruf des zugrunde liegenden Betriebs übersetzt, um die Daten in die Konsole zu schreiben. Wenn die Effizienz durch Pufferung verbessert werden soll, muss dies separat angegeben werden.

Dies bedeutet zum Beispiel, dass in C ++ das vorherige Beispiel folgendermaßen umgeschrieben wird:

std::cout << "Good morning everyone. I am here today to present you with a very, ";
std::cout << "very lengthy sentence in order to prove a point about how it looks ";       
std::cout << "strange amongst other code.";

... normalerweise 1 haben fast keinen Einfluss auf die Effizienz. Jede Verwendung von coutwürde einfach Daten in einem Puffer ablegen. Dieser Puffer würde in den zugrunde liegenden Stream gespült, wenn der Puffer voll ist oder der Code versucht, Eingaben von der Verwendung (wie z. B. mit std::cin) zu lesen .

iostreams haben auch eine sync_with_stdioEigenschaft, die bestimmt, ob die Ausgabe von iostreams mit der Eingabe im C-Stil synchronisiert wird (z getchar. B. ). Standardmäßig sync_with_stdioist true festgelegt. Wenn Sie also beispielsweise schreiben std::coutund dann über lesen getchar, werden die Daten, in die Sie geschrieben haben, coutbeim Aufrufen gelöscht getchar. Sie können den Wert sync_with_stdioauf false setzen, um dies zu deaktivieren (normalerweise, um die Leistung zu verbessern).

sync_with_stdioSteuert auch den Synchronisationsgrad zwischen Threads. Wenn die Synchronisierung aktiviert ist (Standardeinstellung), kann das Schreiben von mehreren Threads in einen Iostream dazu führen, dass die Daten aus den Threads verschachtelt werden, verhindert jedoch alle Race-Bedingungen. IOW, Ihr Programm wird ausgeführt und erzeugt eine Ausgabe, aber wenn mehr als ein Thread gleichzeitig in einen Stream schreibt, macht das willkürliche Vermischen der Daten aus den verschiedenen Threads die Ausgabe normalerweise ziemlich nutzlos.

Wenn Sie die Option aus der Synchronisation, dann synchronisieren Zugriff von mehreren Threads wird vollständig in Ihrer Verantwortung als gut. Gleichzeitiges Schreiben von mehreren Threads kann / wird zu einem Datenwettlauf führen, was bedeutet, dass der Code ein undefiniertes Verhalten aufweist.

Zusammenfassung

In C ++ wird standardmäßig versucht, Geschwindigkeit und Sicherheit in Einklang zu bringen. Das Ergebnis ist für Code mit einem Thread ziemlich erfolgreich, für Code mit mehreren Threads jedoch weniger. Multithread-Code muss normalerweise sicherstellen, dass jeweils nur ein Thread in einen Stream schreibt, um eine nützliche Ausgabe zu erzielen.


1. Es ist möglich, die Pufferung für einen Stream zu deaktivieren, dies ist jedoch ziemlich ungewöhnlich, und wenn / wenn jemand dies tut, liegt dies wahrscheinlich an einem bestimmten Grund, z . In jedem Fall geschieht dies nur, wenn der Code dies explizit tut.

Jerry Sarg
quelle
13
" In höheren Sprachen wie C und C ++ ist dies weniger problematisch als in Java. " - was? C und C ++ sind untergeordnete Sprachen als Java. Außerdem haben Sie Ihre Leitungsabschlüsse vergessen.
user2357112 unterstützt Monica
1
Im ganzen weise ich auf die objektive Grundlage hin, dass Java die niedrigere Sprache ist. Ich bin mir nicht sicher, über welche Leitungsabschlusszeichen Sie sprechen.
Jerry Coffin
2
Java führt auch die Verkettung zur Kompilierungszeit durch. Wird beispielsweise "2^31 - 1 = " + Integer.MAX_VALUEals einzelne internierte Zeichenfolge gespeichert (JLS Sec 3.10.5 und 15.28 ).
200_success
2
@ 200_success: Java, das die Zeichenfolgenverkettung zur Kompilierungszeit ausführt, scheint auf §15.18.1 zurückzukommen: "Das Zeichenfolgenobjekt wird neu erstellt (§12.5), es sei denn, der Ausdruck ist ein Konstantenausdruck zur Kompilierungszeit (§15.28)." Dies scheint zuzulassen, erfordert jedoch nicht, dass die Verkettung zur Kompilierungszeit erfolgt. Das heißt, das Ergebnis muss neu erstellt werden, wenn die Eingaben keine Konstanten für die Kompilierungszeit sind, es werden jedoch keine Anforderungen in beide Richtungen gestellt, wenn es sich um Konstanten für die Kompilierungszeit handelt. Um eine Verkettung zur Kompilierungszeit zu erfordern, müssten Sie das (implizite) "if" lesen, was "genau dann" bedeutet, wenn ".
Jerry Coffin
2
@Phoshi: Try with resources ist RAII nicht einmal vage ähnlich. Mit RAII kann die Klasse die Ressourcen verwalten, für den Versuch mit Ressourcen ist jedoch der Clientcode erforderlich, um die Ressourcen zu verwalten. Die Merkmale (Abstraktionen, genauer gesagt), die der eine hat, und die anderen Mängel, sind absolut relevant - genau das macht eine Sprache höher als eine andere.
Jerry Coffin
1

Während die Leistung hier nicht wirklich von Bedeutung ist println, deutet die schlechte Lesbarkeit einiger Aussagen auf einen fehlenden Designaspekt hin.

Warum schreiben wir eine Folge vieler printlnAussagen? Wenn es nur ein fester Textblock wäre, wie ein --helpText in einem Konsolenbefehl, wäre es viel besser, ihn als separate Ressource zu haben und ihn auf Anfrage einzulesen und auf den Bildschirm zu schreiben.

In der Regel handelt es sich jedoch um eine Mischung aus dynamischen und statischen Teilen. Nehmen wir an, wir haben einerseits einige reine Bestelldaten und andererseits einige feste statische Textteile, und diese Dinge müssen zusammengemischt werden, um ein Bestellbestätigungsblatt zu bilden. Auch in diesem Fall ist es besser, eine separate Ressourcentextdatei zu haben: Die Ressource wäre eine Vorlage, die eine Art von Symbolen (Platzhaltern) enthält, die zur Laufzeit durch die eigentlichen Auftragsdaten ersetzt werden.

Die Trennung der Programmiersprache von der natürlichen Sprache hat viele Vorteile - darunter die Internationalisierung: Möglicherweise müssen Sie den Text übersetzen, wenn Sie mit Ihrer Software mehrsprachig werden möchten. Warum sollte ein Kompilierungsschritt auch notwendig sein, wenn Sie nur eine Textkorrektur wünschen, beispielsweise um Rechtschreibfehler zu beheben?

rplantiko
quelle