À quel point appelle-t-il souvent println () que de concaténer des chaînes ensemble et de l'appeler une fois?

23

Je sais que la sortie vers la console est une opération coûteuse. Dans l'intérêt de la lisibilité du code, il est parfois agréable d'appeler une fonction pour sortir du texte deux fois, plutôt que d'avoir une longue chaîne de texte comme argument.

Par exemple, combien est-il moins efficace d'avoir

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

contre.

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

Dans l'exemple, la différence est d'un seul appel, println()mais que faire si c'est plus?

Sur une note connexe, les instructions impliquant l'impression de texte peuvent sembler étranges lors de l'affichage du code source si le texte à imprimer est long. En supposant que le texte lui-même ne peut pas être raccourci, que peut-on faire? Devrait-il s'agir d'un cas où plusieurs println()appels devraient être effectués? Quelqu'un m'a dit une fois qu'une ligne de code ne devrait pas contenir plus de 80 caractères (IIRC) alors que feriez-vous avec

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.");

Est-ce la même chose pour des langages tels que C / C ++ puisque chaque fois que des données sont écrites dans un flux de sortie, un appel système doit être effectué et le processus doit passer en mode noyau (ce qui est très coûteux)?

Celeritas
la source
Même si c'est très peu de code, je dois dire que je me demandais la même chose. Ce serait bien de déterminer la réponse une fois pour toutes
Simon Forsberg
@ SimonAndréForsberg Je ne sais pas s'il est applicable à Java car il s'exécute sur une machine virtuelle, mais dans des langages de niveau inférieur tels que C / C ++, j'imagine que cela coûterait cher car chaque fois que quelque chose écrit dans un flux de sortie, un appel système doit être fait.
Il y a aussi ceci à considérer: stackoverflow.com/questions/21947452/…
hjk
1
Je dois dire que je ne vois pas le point ici. Lorsque j'interagis avec un utilisateur via un terminal, je ne peux pas imaginer de problème de performances car il n'y a généralement pas grand-chose à imprimer. Et les applications avec une interface graphique ou une webapp doivent écrire dans un fichier journal (généralement en utilisant un framework).
Andy
1
Si vous dites bonjour, faites-le une ou deux fois par jour. L'optimisation n'est pas une préoccupation. Si c'est autre chose, vous devez profiler pour savoir si c'est un problème. Le code que je travaille sur la journalisation ralentit le code à inutilisable, sauf si vous créez un tampon multi-lignes et videz le texte en un seul appel.
mattnz

Réponses:

29

Il y a ici deux «forces» en tension: performances vs lisibilité.

Abordons d'abord le troisième problème, les longues files d'attente:

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.");

La meilleure façon d'implémenter cela et de garder la lisibilité, est d'utiliser la concaténation de chaînes:

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.");

La concaténation de chaîne constante se produira au moment de la compilation et n'aura aucun effet sur les performances. Les lignes sont lisibles et vous pouvez simplement continuer.

Maintenant, à propos de:

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

contre.

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

La deuxième option est nettement plus rapide. Je proposerai environ 2 fois plus vite ... pourquoi?

Parce que 90% (avec une large marge d'erreur) du travail n'est pas lié au dumping des caractères dans la sortie, mais est une surcharge nécessaire pour sécuriser la sortie et y écrire.

Synchronisation

System.outest un PrintStream. Toutes les implémentations Java que je connais, synchronisent en interne le PrintStream: Voir le code sur GrepCode! .

Qu'est-ce que cela signifie pour votre code?

Cela signifie que chaque fois que vous appelez System.out.println(...)vous synchronisez votre modèle de mémoire, vous vérifiez et attendez un verrou. Tous les autres threads appelant System.out seront également verrouillés.

Dans les applications à un seul thread, l'impact de System.out.println()est souvent limité par les performances d'E / S de votre système, à quelle vitesse pouvez-vous écrire dans un fichier. Dans les applications multithread, le verrouillage peut être plus problématique que l'IO.

Flushing

Chaque impression est rincée . Cela provoque l'effacement des tampons et déclenche une écriture de niveau console dans les tampons. La quantité d'effort effectuée ici dépend de la mise en œuvre, mais il est généralement entendu que les performances du vidage ne sont que partiellement liées à la taille du tampon à vider. Il y a une surcharge importante liée au vidage, où les tampons de mémoire sont marqués comme sales, la machine virtuelle effectue des E / S, etc. Le fait d'engager ces frais généraux une fois, au lieu de deux, est une optimisation évidente.

Quelques chiffres

J'ai mis en place le petit test suivant:

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);

    }


}

Le code est relativement simple, il imprime à plusieurs reprises une chaîne courte ou longue à afficher. La longue chaîne contient plusieurs nouvelles lignes. Il mesure le temps nécessaire pour imprimer 1000 itérations de chacun.

Si je l'exécute à l'invite de commande unix (Linux) , que je redirige STDOUTvers /dev/nullet que j'imprime les résultats réels STDERR, je peux faire ce qui suit:

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

La sortie (dans errlog) ressemble à:

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

Qu'est-ce que ça veut dire? Permettez-moi de répéter la dernière «strophe»:

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

Cela signifie que, à toutes fins utiles, même si la ligne «longue» est environ 5 fois plus longue et contient plusieurs retours à la ligne, la sortie prend à peu près autant de temps que la ligne courte.

Le nombre de caractères par seconde pour le long terme est 5 fois plus élevé et le temps écoulé est à peu près le même .....

En d'autres termes, vos performances varient en fonction du nombre d'imprimantes dont vous disposez, et non de ce qu'elles impriment.

Mise à jour: que se passe-t-il si vous redirigez vers un fichier, au lieu de / dev / null?

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

C'est beaucoup plus lent, mais les proportions sont à peu près les mêmes ...

rolfl
la source
Ajout de quelques performances.
rolfl
Vous devez également considérer le problème qui "\n"n'est peut-être pas le terminateur de ligne droit. printlnterminera automatiquement la ligne avec le (s) bon (s) caractère (s), mais coller \ndirectement un dans votre chaîne peut provoquer des problèmes. Si vous voulez le faire correctement, vous devrez peut-être utiliser la mise en forme des chaînes ou la line.separatorpropriété système . printlnest beaucoup plus propre.
user2357112 prend en charge Monica
3
Il s'agit d'une excellente analyse, alors +1 à coup sûr, mais je dirais qu'une fois que vous êtes déterminé à produire la console, ces différences de performances mineures disparaissent. Si l'algorithme de votre programme s'exécute plus rapidement que la sortie des résultats (à ce petit niveau de sortie), vous pouvez imprimer chaque caractère un par un et ne pas remarquer la différence.
David Harkness
Je crois que c'est une différence entre Java et C / C ++ que la sortie est synchronisée. Je dis cela parce que je me souviens d'avoir écrit un programme multithread et d'avoir des problèmes avec la sortie tronquée si différents threads tentent d'écrire pour écrire sur la console. Quelqu'un peut-il vérifier cela?
6
Il est important de se rappeler également que rien de cette vitesse n'a d'importance du tout lorsqu'il est placé juste à côté de la fonction qui attend l'entrée de l'utilisateur.
vmrob
2

Je ne pense pas que le fait d'avoir un tas de printlns soit un problème de conception. Selon moi, cela peut clairement être fait avec un analyseur de code statique si c'est vraiment un problème.

Mais ce n'est pas un problème car la plupart des gens ne font pas des IO comme ça. Quand ils ont vraiment besoin de faire beaucoup d'E / S, ils utilisent des tampons (BufferedReader, BufferedWriter, etc.) lorsque l'entrée est tamponnée, vous verrez que les performances sont assez similaires, que vous n'avez pas à vous soucier d'avoir un tas printlnou peu println.

Donc, pour répondre à la question d'origine. Je dirais, pas mal si vous utilisez printlnpour imprimer quelques choses comme la plupart des gens le feraient println.

InforméA
la source
1

Dans les langages de niveau supérieur comme C et C ++, c'est moins un problème qu'en Java.

Tout d'abord, C et C ++ définissent la concaténation de chaînes au moment de la compilation, vous pouvez donc quelque chose comme:

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.";

Dans un tel cas, la concaténation de la chaîne n'est pas seulement une optimisation que vous pouvez à peu près, généralement (etc.) dépendre du compilateur à faire. Il est plutôt directement requis par les normes C et C ++ (phase 6 de la traduction: "Les jetons littéraux de chaîne adjacents sont concaténés.").

Bien que cela se fasse au détriment d'un peu plus de complexité dans le compilateur et l'implémentation, C et C ++ font un peu plus pour cacher la complexité de la production efficace de sortie du programmeur. Java ressemble beaucoup plus au langage d'assemblage - chaque appel à se System.out.printlntraduit beaucoup plus directement par un appel à l'exploitation sous-jacente pour écrire les données sur la console. Si vous souhaitez que la mise en mémoire tampon améliore l'efficacité, cela doit être fourni séparément.

Cela signifie, par exemple, qu'en C ++, réécrivant l'exemple précédent, à quelque chose comme ceci:

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.";

... normalement 1 ont presque aucun effet sur l' efficacité. Chaque utilisation de coutdéposerait simplement des données dans un tampon. Ce tampon était vidé dans le flux sous-jacent lorsque le tampon était rempli, ou que le code essayait de lire l'entrée de l'utilisation (comme avec std::cin).

iostreamLes s ont également une sync_with_stdiopropriété qui détermine si la sortie des iostreams est synchronisée avec l'entrée de style C (par exemple, getchar). Par défaut, la valeur sync_with_stdioest true, donc si, par exemple, vous écrivez std::cout, puis lisez via getchar, les données que vous avez écrites coutseront vidées lors de l' getcharappel. Vous pouvez définir sync_with_stdiofalse pour désactiver cela (généralement effectué pour améliorer les performances).

sync_with_stdiocontrôle également un degré de synchronisation entre les threads. Si la synchronisation est activée (par défaut), l'écriture sur un iostream à partir de plusieurs threads peut entraîner l'entrelacement des données des threads, mais empêche toute condition de concurrence critique. IOW, votre programme s'exécutera et produira une sortie, mais si plusieurs threads écrivent dans un flux à la fois, le mélange arbitraire des données des différents threads rendra généralement la sortie assez inutile.

Si vous désactivez la synchronisation, la synchronisation de l'accès à partir de plusieurs threads devient également votre entière responsabilité. Les écritures simultanées à partir de plusieurs threads peuvent / entraîneront une course aux données, ce qui signifie que le code a un comportement indéfini.

Sommaire

Par défaut, C ++ tente de concilier vitesse et sécurité. Le résultat est assez réussi pour le code à un seul thread, mais moins pour le code à plusieurs threads. Le code multithread doit généralement garantir qu'un seul thread écrit dans un flux à la fois pour produire une sortie utile.


1. Il est possible de désactiver la mise en mémoire tampon pour un flux, mais en fait, cela est assez inhabituel, et quand / si quelqu'un le fait, c'est probablement pour une raison assez spécifique, comme s'assurer que toutes les sorties sont capturées immédiatement malgré l'effet sur les performances . Dans tous les cas, cela ne se produit que si le code le fait explicitement.

Jerry Coffin
la source
13
" Dans les langages de niveau supérieur comme C et C ++, c'est moins un problème qu'en Java. " - quoi? C et C ++ sont des langages de niveau inférieur à Java. De plus, vous avez oublié vos terminateurs de ligne.
user2357112 prend en charge Monica
1
Tout au long, je souligne la base objective pour Java étant le langage de niveau inférieur. Vous ne savez pas de quels terminateurs de ligne vous parlez.
Jerry Coffin
2
Java effectue également la concaténation au moment de la compilation. Par exemple, "2^31 - 1 = " + Integer.MAX_VALUEest stocké sous la forme d'une seule chaîne interne (JLS Sec 3.10.5 et 15.28 ).
200_success
2
@ 200_success: Java effectuant la concaténation de chaînes au moment de la compilation semble se résumer au §15.18.1: "L'objet String est nouvellement créé (§12.5) sauf si l'expression est une expression constante à la compilation (§15.28)." Cela semble permettre, mais pas exiger, que la concaténation soit effectuée au moment de la compilation. C'est-à-dire que le résultat doit être nouvellement créé si les entrées ne sont pas des constantes de temps de compilation, mais aucune exigence n'est faite dans les deux sens si ce sont des constantes de temps de compilation. Pour exiger la concaténation au moment de la compilation, vous devez lire son (implicite) "si" comme signifiant réellement "si et seulement si".
Jerry Coffin
2
@Phoshi: Essayez avec des ressources n'est même pas vaguement similaire à RAII. RAII permet à la classe de gérer les ressources, mais essayez avec des ressources nécessite le code client pour gérer les ressources. Les fonctionnalités (abstractions, plus précisément) dont l'une a et l'autre manquent sont entièrement pertinentes - en fait, c'est exactement ce qui fait qu'une langue est de niveau supérieur à une autre.
Jerry Coffin
1

Bien que les performances ne soient pas vraiment un problème ici, la mauvaise lisibilité d'un tas de printlndéclarations pointe vers un aspect de conception manquant.

Pourquoi écrivons-nous une séquence de nombreuses printlndéclarations? S'il ne s'agissait que d'un bloc de texte fixe, comme un --helptexte dans une commande de console, il serait préférable de l'avoir comme ressource distincte et de le lire et de l'écrire à l'écran sur demande.

Mais généralement, c'est un mélange de parties dynamiques et statiques. Disons que nous avons des données de commande nues d'une part, et des parties de texte statique fixes d'autre part, et ces choses doivent être mélangées pour former une feuille de confirmation de commande. Encore une fois, dans ce cas également, il est préférable d'avoir un fichier texte de ressource distinct: La ressource serait un modèle, contenant une sorte de symboles (espaces réservés), qui sont remplacés au moment de l'exécution par les données de commande réelles.

La séparation du langage de programmation du langage naturel présente de nombreux avantages, parmi lesquels l'internationalisation: il se peut que vous deviez traduire le texte si vous souhaitez devenir multilingue avec votre logiciel. En outre, pourquoi une étape de compilation devrait-elle être nécessaire si vous souhaitez uniquement avoir une correction textuelle, par exemple corriger une faute d'orthographe.

rplantiko
la source