Quanto è grave chiamare println () spesso che concatenare stringhe insieme e chiamarlo una volta?


23

So che l'output sulla console è un'operazione costosa. Nell'interesse della leggibilità del codice a volte è bello chiamare una funzione per generare due volte il testo, anziché avere una lunga stringa di testo come argomento.

Ad esempio, quanto meno efficiente deve avere

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

vs.

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

Nell'esempio la differenza è solo una chiamata a println()ma cosa succede se è di più?

In una nota correlata, le dichiarazioni che coinvolgono la stampa di testo possono apparire strane durante la visualizzazione del codice sorgente se il testo da stampare è lungo. Supponendo che il testo stesso non possa essere abbreviato, cosa si può fare? Questo dovrebbe essere un caso in cui è println()possibile effettuare più chiamate? Qualcuno una volta mi ha detto che una riga di codice non dovrebbe contenere più di 80 caratteri (IIRC), quindi cosa faresti

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

Lo stesso vale per linguaggi come C / C ++ poiché ogni volta che i dati vengono scritti in un flusso di output è necessario effettuare una chiamata di sistema e il processo deve passare in modalità kernel (che è molto costoso)?


Anche se questo è un codice molto piccolo, devo dire che mi chiedevo la stessa cosa. Sarebbe bello determinare la risposta una volta per tutte
Simon Forsberg,

@ SimonAndréForsberg Non sono sicuro che sia applicabile a Java perché funziona su una macchina virtuale, ma in linguaggi di livello inferiore come C / C ++ immagino che sarebbe costoso poiché ogni volta che qualcosa scrive su un flusso di output, una chiamata di sistema deve essere fatto.

C'è anche questo da considerare: stackoverflow.com/questions/21947452/…
hjk

1
Devo dire che non vedo il punto qui. Quando interagisco con un utente tramite terminale, non riesco a immaginare alcun problema di prestazioni perché di solito non c'è molto da stampare. E le applicazioni con una GUI o una webapp dovrebbero scrivere in un file di registro (di solito usando un framework).
Andy,

1
Se stai dicendo buongiorno, lo fai una o due volte al giorno. L'ottimizzazione non è un problema. Se è qualcos'altro, è necessario il profilo per sapere se è un problema. Il codice che lavoro sulla registrazione rallenta il codice a inutilizzabile a meno che non si crei un buffer multilinea e si scarichi il testo in una chiamata.
Mattnz,

Risposte:


29

Ci sono due "forze" qui, in tensione: prestazioni contro leggibilità.

Affrontiamo prima il terzo problema, linee lunghe:

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

Il modo migliore per implementare questo e mantenere la leggibilità è usare la concatenazione di stringhe:

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 concatenazione costante di stringa avverrà in fase di compilazione e non avrà alcun effetto sulle prestazioni. Le righe sono leggibili e puoi semplicemente andare avanti.

Ora, riguardo al:

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

vs.

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

La seconda opzione è significativamente più veloce. Suggerirò circa 2X più velocemente .... perché?

Perché il 90% (con un ampio margine di errore) del lavoro non è legato al dumping dei caratteri sull'output, ma è necessario per proteggere l'output per scrivere su di esso.

Sincronizzazione

System.outè un PrintStream. Tutte le implementazioni Java che conosco sincronizzano internamente PrintStream: vedi il codice su GrepCode! .

Cosa significa questo per il tuo codice?

Significa che ogni volta che chiami System.out.println(...)stai sincronizzando il tuo modello di memoria, stai controllando e aspettando un blocco. Anche tutti gli altri thread che chiamano System.out verranno bloccati.

Nelle applicazioni a thread singolo l'impatto di System.out.println()è spesso limitato dalle prestazioni di I / O del sistema, dalla velocità con cui è possibile scrivere su file. Nelle applicazioni multithread, il blocco può rappresentare un problema maggiore rispetto all'IO.

risciacquo

Ogni stampa è arrossata . Questo fa sì che i buffer vengano cancellati e innesca una scrittura a livello di console nei buffer. La quantità di sforzo fatta qui dipende dall'implementazione, ma è generalmente inteso che le prestazioni del flush sono solo in piccola parte correlate alla dimensione del buffer da scaricare. Esiste un sovraccarico significativo correlato al flush, in cui i buffer di memoria sono contrassegnati come sporchi, la macchina virtuale sta eseguendo IO e così via. Incorrere in tale overhead una volta, anziché due, è un'ovvia ottimizzazione.

Alcuni numeri

Ho messo insieme il seguente piccolo test:

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

    }


}

Il codice è relativamente semplice, stampa ripetutamente una stringa corta o lunga per l'output. La lunga stringa contiene più nuove righe. Misura il tempo impiegato per stampare 1000 iterazioni di ciascuno.

Se lo eseguo al prompt dei comandi di unix (Linux) e reindirizzo STDOUTa /dev/null, e stampa i risultati effettivi su STDERR, posso fare quanto segue:

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

L'output (in errlog) è simile a:

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

Cosa significa questo? Lasciami ripetere l'ultima "strofa":

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

Significa che, a tutti gli effetti, anche se la linea "lunga" è circa 5 volte più lunga e contiene più linee nuove, ci vuole circa il tempo necessario per l'output della linea corta.

Il numero di caratteri al secondo per il lungo periodo è 5 volte maggiore e il tempo trascorso è circa lo stesso .....

In altre parole, le prestazioni vengono ridimensionate in base al numero di println che hai, non a quello che stampano.

Aggiornamento: cosa succede se si reindirizza a un file anziché a / 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

È molto più lento, ma le proporzioni sono più o meno le stesse ....


Aggiunti alcuni numeri di prestazione.
rolfl,

Devi anche considerare il problema che "\n"potrebbe non essere il terminatore della linea giusta. printlntermina automaticamente la riga con i caratteri giusti, ma attaccare \ndirettamente una stringa può causare problemi. Se si desidera farlo nel modo giusto, potrebbe essere necessario utilizzare la formattazione della stringa o la line.separatorproprietà di sistema . printlnè molto più pulito.
user2357112 supporta Monica il

3
Questa è un'ottima analisi quindi +1 di sicuro, ma direi che una volta che sei impegnato nell'output della console, queste differenze di prestazioni minori volano fuori dalla finestra. Se l'algoritmo del tuo programma funziona più velocemente dell'output dei risultati (a questo piccolo livello di output), puoi stampare ogni carattere uno per uno e non notare la differenza.
David Harkness,

Credo che questa sia una differenza tra Java e C / C ++ che l'output è sincronizzato. Lo dico perché ricordo di aver scritto un programma multithread e di avere problemi con l'output confuso se diversi thread tentano di scrivere per scrivere sulla console. Qualcuno può verificarlo?

6
È anche importante ricordare che nessuna di queste velocità è importante quando viene messa accanto alla funzione che attende l'input dell'utente.
vmrob

2

Non credo che avere un sacco di printlns sia un problema di progettazione. Il modo in cui lo vedo è che questo può essere fatto chiaramente con l'analizzatore di codice statico se è davvero un problema.

Ma non è un problema perché la maggior parte delle persone non esegue IO in questo modo. Quando hanno davvero bisogno di fare molti IO, usano quelli bufferizzati (BufferedReader, BufferedWriter, ecc.) Quando l'ingresso è bufferizzato, vedrai che le prestazioni sono abbastanza simili, che non devi preoccuparti di avere un mazzo di printlno pochi println.

Quindi, per rispondere alla domanda originale. Direi, non male se si utilizza printlnper stampare alcune cose come la maggior parte della gente userebbe println.


1

In linguaggi di livello superiore come C e C ++, questo è meno un problema che in Java.

Prima di tutto, C e C ++ definiscono la concatenazione di stringhe in fase di compilazione, quindi puoi fare qualcosa del genere:

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 tal caso, concatenare la stringa non è solo un'ottimizzazione che puoi praticamente fare, di solito (ecc.) In base al compilatore da creare. Piuttosto, è richiesto direttamente dagli standard C e C ++ (fase 6 della traduzione: "I token letterali stringa adiacenti sono concatenati.").

Sebbene sia a scapito di un po 'di complessità in più nel compilatore e nell'implementazione, C e C ++ fanno un po' di più per nascondere la complessità della produzione di output in modo efficiente dal programmatore. Java è molto più simile al linguaggio assembly: ogni chiamata si System.out.printlntraduce in modo molto più diretto in una chiamata all'operato sottostante per scrivere i dati sulla console. Se si desidera il buffering per migliorare l'efficienza, questo deve essere fornito separatamente.

Ciò significa, ad esempio, che in C ++, riscrivendo l'esempio precedente, qualcosa del genere:

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

... normalmente 1 non avrebbe quasi alcun effetto sull'efficienza. Ogni utilizzo di coutsemplicemente depositerebbe i dati in un buffer. Quel buffer verrebbe scaricato nel flusso sottostante quando il buffer si riempiva, o il codice tentava di leggere l'input dall'uso (come con std::cin).

iostreams hanno anche una sync_with_stdioproprietà che determina se l'output degli iostreams è sincronizzato con l'input di tipo C (ad es getchar.). Per impostazione predefinita sync_with_stdioè impostato su true, quindi se, ad esempio, si scrive std::cout, quindi si legge tramite getchar, i dati a cui si è scritto coutverranno cancellati quando getcharviene chiamato. È possibile impostare sync_with_stdiosu false per disabilitarlo (di solito fatto per migliorare le prestazioni).

sync_with_stdiocontrolla anche un grado di sincronizzazione tra i thread. Se la sincronizzazione è attivata (impostazione predefinita), la scrittura su un iostream da più thread può comportare l'interlacciamento dei dati dai thread, ma impedisce qualsiasi condizione di competizione. IOW, il tuo programma verrà eseguito e produrrà output, ma se più di un thread scrive su uno stream alla volta, la mescolanza arbitraria dei dati dai diversi thread renderà l'output piuttosto inutile.

Se disattivi la sincronizzazione, anche la sincronizzazione dell'accesso da più thread diventa interamente tua responsabilità. Scritture simultanee da più thread possono / porteranno a una corsa ai dati, il che significa che il codice ha un comportamento indefinito.

Sommario

Per impostazione predefinita, C ++ tenta di bilanciare la velocità con la sicurezza. Il risultato ha un discreto successo per il codice a thread singolo, ma meno per il codice a thread multipli. Il codice multithread in genere deve garantire che solo un thread scriva su uno stream alla volta per produrre output utili.


1. È possibile disattivare il buffering per uno stream, ma in realtà farlo è piuttosto insolito e quando / se qualcuno lo fa, è probabilmente per un motivo abbastanza specifico, come garantire che tutto l'output venga catturato immediatamente nonostante l'effetto sulle prestazioni . In ogni caso, ciò accade solo se il codice lo fa esplicitamente.


13
" In linguaggi di livello superiore come C e C ++, questo è un problema minore rispetto a Java. " - Cosa? C e C ++ sono linguaggi di livello inferiore rispetto a Java. Inoltre, hai dimenticato i terminatori di linea.
user2357112 supporta Monica il

1
In tutto, sottolineo che la base oggettiva per Java è il linguaggio di livello inferiore. Non sono sicuro di quali terminatori di linea stai parlando.
Jerry Coffin,

2
Java esegue anche la concatenazione in fase di compilazione. Ad esempio, "2^31 - 1 = " + Integer.MAX_VALUEè memorizzato come una singola stringa internata (JLS Sec 3.10.5 e 15.28 ).
200_successo

2
@ 200_success: Java che esegue la concatenazione di stringhe al momento della compilazione sembra scendere a §15.18.1: "L'oggetto String è stato creato di recente (§12.5) a meno che l'espressione non sia un'espressione costante in fase di compilazione (§15.28)." Ciò sembra consentire ma non richiedere che la concatenazione venga eseguita in fase di compilazione. Vale a dire, il risultato deve essere appena creato se gli input non sono costanti di tempo di compilazione, ma non è richiesto alcun requisito in entrambe le direzioni se sono costanti di tempo di compilazione. Per richiedere la concatenazione in fase di compilazione, dovresti leggere il "(implicito)" se "come significato reale" se e solo se ".
Jerry Coffin,

2
@Phoshi: provare con le risorse non è nemmeno vagamente simile a RAII. RAII consente alla classe di gestire le risorse, ma provare con le risorse richiede che il codice client gestisca le risorse. Le caratteristiche (astrazioni, più precisamente) di cui uno ha e l'altra manca sono del tutto rilevanti - in effetti, sono esattamente ciò che rende una lingua di livello superiore rispetto a un'altra.
Jerry Coffin,

1

Sebbene le prestazioni non siano davvero un problema qui, la cattiva leggibilità di un mucchio di printlnaffermazioni indica un aspetto del design mancante.

Perché scriviamo una sequenza di molti println affermazioni? Se fosse solo un blocco di testo fisso, come un --helptesto in un comando di console, sarebbe molto meglio averlo come risorsa separata e leggerlo e scriverlo sullo schermo su richiesta.

Ma di solito è una miscela di parti dinamiche e statiche. Supponiamo che da un lato abbiamo alcuni dati di ordine nudo e dall'altro alcune parti di testo statico fisso, e queste cose devono essere mescolate insieme per formare un foglio di conferma dell'ordine. Ancora una volta, anche in questo caso, è meglio disporre di un file di testo di risorse separato: la risorsa sarà un modello, contenente alcuni tipi di simboli (segnaposto), che vengono sostituiti in fase di esecuzione dai dati dell'ordine effettivi.

La separazione del linguaggio di programmazione dal linguaggio naturale presenta molti vantaggi, tra cui l'internazionalizzazione: potrebbe essere necessario tradurre il testo se si desidera diventare multilingue con il proprio software. Inoltre, perché dovrebbe essere necessaria una fase di compilazione se si desidera solo una correzione testuale, ad esempio correggere un errore di ortografia.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.