Quanto è pericoloso chiamare println () spesso che concatenare stringhe e chiamarle una volta sola?

23

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

Ad esempio quanto meno efficiente è 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 è più?

In una nota correlata, le dichiarazioni che coinvolgono il testo di stampa possono sembrare strane mentre si visualizza il codice sorgente se il testo da stampare è lungo. Supponendo che il testo stesso non possa essere ridotto, cosa si può fare? Dovrebbe essere questo il caso in cui vengono effettuate più chiamate println() ? Qualcuno una volta mi ha detto che una riga di codice non deve contenere più di 80 caratteri (IIRC), quindi cosa faresti con

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 su un flusso di output deve essere effettuata una chiamata di sistema e il processo deve passare alla modalità kernel (che è molto costosa)?

    
posta Celeritas 01.07.2014 - 02:03
fonte

4 risposte

29

Qui ci sono due "forze", in tensione: prestazioni vs. leggibilità.

Affrontiamo prima il terzo problema, però: le 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à è utilizzare la concatenazione delle 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 di stringhe costanti avverrà in fase di compilazione e non avrà alcun effetto sulle prestazioni. Le linee sono leggibili e puoi semplicemente andare avanti.

Ora, riguardo a:

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. Ti suggerirò circa 2 volte più veloce .... perché?

Poiché il 90% (con un ampio margine di errore) del lavoro non è correlato al dumping dei caratteri nell'output, ma è necessario un sovraccarico per proteggere l'output in modo che scriva ad esso.

Sincronizzazione

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

Che 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 eventuali altri thread che chiamano System.out verranno bloccati.

Nelle applicazioni a thread singolo l'impatto di System.out.println() è spesso limitato dalle prestazioni IO del tuo sistema, quanto velocemente puoi scrivere su file. Nelle applicazioni multithread, il blocco può essere più un problema dell'IO.

Flushing

Ogni println viene svuotato . Ciò fa sì che i buffer vengano cancellati e attiva una scrittura a livello di console nei buffer. La quantità di sforzo svolto qui dipende dall'implementazione, ma è generalmente inteso che le prestazioni dello svuotamento sono solo in piccola parte correlate alla dimensione del buffer che viene svuotato. Esiste un sovraccarico significativo relativo allo svuotamento, in cui i buffer di memoria sono contrassegnati come sporchi, la macchina virtuale sta eseguendo l'I / O e così via. Incrementare il sovraccarico 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 breve o lunga da emettere. La lunga stringa ha più righe nuove in essa. Misura il tempo necessario per stampare 1000 iterazioni di ciascuno.

Se lo eseguo al prompt dei comandi di Unix (Linux) e reindirizzo STDOUT a /dev/null e stampo i risultati effettivi su STDERR , posso eseguire le seguenti operazioni:

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

L'output (in errlog) assomiglia 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

Che cosa significa? Permettimi di ripetere l'ultima 'stanza':

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ù righe nuove, ci vuole un tempo di uscita pari a quella della linea corta.

Il numero di caratteri al secondo a lungo termine è 5 volte tanto, e il tempo trascorso è circa lo stesso .....

In altre parole, le tue prestazioni sono ridimensionate rispetto al numero di printlns che hai, non che loro stampano.

Aggiornamento: cosa succede se 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 quasi le stesse ...

    
risposta data 01.07.2014 - 02:36
fonte
2

Non penso che avere un sacco di println s sia un problema di design. Il mio modo di vedere è che questo può essere fatto chiaramente con l'analizzatore di codice statico, se è davvero un problema.

Ma non è un problema perché molte persone non fanno IO in questo modo. Quando hanno davvero bisogno di fare molti IO, usano dei buffer (BufferedReader, BufferedWriter, ecc.) Quando l'input è bufferizzato, vedrai che le prestazioni sono abbastanza simili, che non devi preoccuparti di avere un mazzetto di println o pochi println .

Quindi per rispondere alla domanda originale. Direi, non male se usi println per stampare alcune cose perché la maggior parte delle persone userebbe println per.

    
risposta data 02.07.2014 - 08:53
fonte
1

Nei linguaggi di alto livello 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 qualcosa del tipo:

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, di solito (ecc.) dipende dal compilatore da realizzare. Piuttosto, è direttamente richiesto dagli standard C e C ++ (fase 6 della traduzione: "I token stringa letterali adiacenti sono concatenati.").

Anche se è a scapito di una piccola 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 a System.out.println si traduce molto più direttamente in una chiamata al funzionamento sottostante per scrivere i dati sulla console. Se vuoi che il buffering migliori l'efficienza, deve essere fornito separatamente.

Ciò significa, ad esempio, che in C ++, riscrivendo l'esempio precedente, a qualcosa di simile:

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 ha quasi alcun effetto sull'efficienza. Ogni utilizzo di cout semplicemente depositerebbe i dati in un buffer. Questo buffer verrebbe svuotato al flusso sottostante quando il buffer si riempiva, o il codice tentava di leggere l'input dall'uso (come con std::cin ).

iostream s ha anche una proprietà sync_with_stdio che determina se l'output da iostreams è sincronizzato con l'input in stile C (ad esempio getchar ). Per impostazione predefinita, sync_with_stdio è impostato su true, quindi se, ad esempio, scrivi std::cout , quindi leggi getchar , i dati scritti su cout verranno svuotati quando viene chiamato getchar . Puoi impostare sync_with_stdio su false per disabilitarlo (di solito fatto per migliorare le prestazioni).

sync_with_stdio controlla 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 eseguirà e produrrà output, ma se più di un thread scrive su un flusso alla volta, il mescolamento arbitrario dei dati dai diversi thread renderà l'output piuttosto inutile.

Se disattivi off la sincronizzazione, anche la sincronizzazione dell'accesso da più thread diventa interamente tua responsabilità. La scrittura simultanea da più thread può / porterà a una corsa di dati, il che significa che il codice ha un comportamento non definito.

Sommario

Il valore predefinito di C ++ è un tentativo di bilanciare la velocità con la sicurezza. Il risultato è discreto per il codice a thread singolo, ma meno per il codice a più thread. Il codice multithreading in genere deve garantire che solo un thread scriva su un flusso alla volta per produrre un output utile.

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 ad esempio assicurarsi che tutto l'output sia catturato immediatamente nonostante l'effetto sul rendimento . In ogni caso, ciò accade solo se il codice lo fa esplicitamente.

    
risposta data 01.07.2014 - 04:11
fonte
1

Sebbene le prestazioni non siano davvero un problema qui, la cattiva leggibilità di una serie di dichiarazioni println indica un aspetto del design mancante.

Perché scriviamo una sequenza di molte dichiarazioni println ? Se fosse solo un blocco di testo fisso, come un testo --help in un comando della 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. Diciamo che abbiamo da un lato alcuni dati di ordine nullo, dall'altro alcune parti di testo statico e fisso, e queste cose devono essere mescolate insieme per formare un foglio di conferma dell'ordine. Anche in questo caso, è preferibile avere un file di testo di risorsa separato: la risorsa dovrebbe essere 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 multilanguinati con il proprio software. Inoltre, perché dovrebbe essere necessario un passaggio di compilazione se desideri solo una correzione testuale, ad esempio correggi un errore di ortografia.

    
risposta data 02.07.2014 - 08:14
fonte

Leggi altre domande sui tag