Dovrei usare Java String.format () se le prestazioni sono importanti?


216

Dobbiamo creare stringhe continuamente per l'output del registro e così via. Nelle versioni JDK abbiamo imparato quando usare StringBuffer(molti appendi, thread-safe) e StringBuilder(molti appendi, non thread-safe).

Qual è il consiglio sull'uso String.format()? È efficiente o siamo costretti a rimanere con la concatenazione per i one-liner in cui le prestazioni sono importanti?

ad es. brutto vecchio stile,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

vs. un nuovo stile ordinato (String.format, che è probabilmente più lento),

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

Nota: il mio caso d'uso specifico sono le centinaia di stringhe di registro "one-liner" nel mio codice. Non comportano un ciclo, quindi StringBuilderè troppo pesante. Sono interessato in modo String.format()specifico.


28
Perché non lo provi?
Ed S.

1
Se stai producendo questo risultato, suppongo che debba essere leggibile da un essere umano come un tasso che un essere umano può leggere. Diciamo al massimo 10 righe al secondo. Penso che troverai che non importa quale approccio segui, se è teoricamente più lento, l'utente potrebbe apprezzarlo. ;) Quindi no, StringBuilder non è dei pesi massimi nella maggior parte delle situazioni.
Peter Lawrey,

9
@Peter, no non è assolutamente per la lettura in tempo reale da parte degli umani! È lì per aiutare l'analisi quando le cose vanno male. L'output del log sarà in genere di migliaia di righe al secondo, quindi deve essere efficiente.
Air

5
se stai producendo molte migliaia di righe al secondo, suggerirei 1) usare un testo più breve, anche senza testo come CSV semplice o binario 2) Non usare affatto String, puoi scrivere i dati in un ByteBuffer senza creare qualsiasi oggetto (come testo o binario) 3) in background la scrittura di dati su disco o su un socket. Dovresti essere in grado di sostenere circa 1 milione di linee al secondo. (Fondamentalmente quanto il tuo sottosistema di dischi consentirà) Puoi ottenere esplosioni di 10 volte.
Peter Lawrey,

7
Questo non è rilevante per il caso generale, ma per la registrazione in particolare, LogBack (scritto dall'autore originale di Log4j) ha una forma di registrazione parametrica che risolve questo esatto problema - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell,

Risposte:


123

Ho scritto una piccola classe per testare che ha le prestazioni migliori dei due e + precede il formato. di un fattore da 5 a 6. Provalo tu stesso

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

L'esecuzione di quanto sopra per N diversi mostra che entrambi si comportano in modo lineare, ma String.formatsono 5-30 volte più lenti.

Il motivo è che nell'attuale implementazione String.formatprima analizza l'input con espressioni regolari e quindi riempie i parametri. La concatenazione con plus, d'altra parte, viene ottimizzata da javac (non dalla JIT) e utilizza StringBuilder.appenddirettamente.

Confronto di runtime


12
C'è un difetto in questo test in quanto non è del tutto una buona rappresentazione di tutta la formattazione delle stringhe. Spesso c'è la logica coinvolta in cosa includere e la logica per formattare valori specifici in stringhe. Qualsiasi test reale dovrebbe guardare scenari del mondo reale.
Orion Adrian,

9
C'era un'altra domanda su SO riguardo a + versi StringBuffer, nelle ultime versioni di Java + è stato sostituito con StringBuffer quando possibile, quindi le prestazioni non sarebbero state diverse
hhafez,

25
Questo assomiglia molto al tipo di microbenchmark che verrà ottimizzato in modo molto inutile.
David H. Clements,

20
Un altro micro-benchmark mal implementato. In che modo entrambi i metodi si ridimensionano in base agli ordini di grandezza. Che ne dici di usare, 100, 1000, 10000, 1000000, operazioni. Se si esegue solo un test, su un ordine di grandezza, su un'applicazione che non è in esecuzione su un core isolato; non c'è modo di dire quanta differenza possa essere cancellata come "effetti collaterali" a causa del cambio di contesto, dei processi in background, ecc.
Evan Plaice,

8
Inoltre, dato che non esci mai dal JIT principale, non riesci ad entrare.
Jan Zyka,

242

Ho preso Hhafez codice e aggiunto un test di memoria :

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

Lo eseguo separatamente per ogni approccio, l'operatore '+', String.format e StringBuilder (chiamando toString ()), quindi la memoria utilizzata non sarà influenzata da altri approcci. Ho aggiunto più concatenazioni, rendendo la stringa come "Blah" + i + "Blah" + i + "Blah" + i + "Blah".

Il risultato è il seguente (media di 5 corse ciascuno):
Tempo di avvicinamento (ms)
Operatore memoria allocata (lunga) '+' 747 320.504
String.format 16484 373.312
StringBuilder 769 57.344

Possiamo vedere che String '+' e StringBuilder sono praticamente identici dal punto di vista temporale, ma StringBuilder è molto più efficiente nell'uso della memoria. Questo è molto importante quando abbiamo molte chiamate di registro (o qualsiasi altra istruzione che coinvolge stringhe) in un intervallo di tempo abbastanza breve da impedire a Garbage Collector di pulire le numerose istanze di stringa risultanti dall'operatore '+'.

E una nota, a proposito, non dimenticare di controllare la registrazione livello di prima di costruire il messaggio.

conclusioni:

  1. Continuerò a utilizzare StringBuilder.
  2. Ho troppo tempo o troppo poca vita.

8
"non dimenticare di controllare il livello di registrazione prima di costruire il messaggio", è un buon consiglio, questo dovrebbe essere fatto almeno per i messaggi di debug, perché potrebbero essercene molti e non dovrebbero essere abilitati in produzione.
stivlo,

39
No, non è giusto. Mi dispiace essere schietto ma il numero di voti che ha attirato è a dir poco allarmante. L'uso +dell'operatore compila il StringBuildercodice equivalente . I microbench come questo non sono un buon modo per misurare le prestazioni - perché non usare jvisualvm, è nel jdk per un motivo. String.format() sarà più lento, ma a causa del tempo necessario per analizzare la stringa di formato anziché qualsiasi allocazione di oggetti. Rinviare la creazione di artefatti di registrazione fino a quando non si è sicuri che siano necessari è un buon consiglio, ma se avrebbe un impatto sulle prestazioni è nel posto sbagliato.
CurtainDog

1
@CurtainDog, il tuo commento è stato fatto su un post di quattro anni, puoi indicare la documentazione o creare una risposta separata per affrontare la differenza?
Kurtzbot,

1
Riferimento a sostegno del commento di @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Cioè, + è preferito a meno che non sia fatto in un ciclo.
albicocca,

And a note, BTW, don't forget to check the logging level before constructing the message.non è un buon consiglio. Supponendo che stiamo parlando in java.util.logging.*particolare, verificare il livello di registrazione è quando si parla di eseguire elaborazioni avanzate che potrebbero causare effetti negativi su un programma che non si vorrebbe quando un programma non ha la registrazione attivata al livello appropriato. La formattazione delle stringhe non è affatto quel tipo di elaborazione. La formattazione fa parte del java.util.loggingframework e il logger stesso controlla il livello di registrazione prima che il formatter venga mai richiamato.
searchengine27,

30

Tutti i parametri di riferimento qui presentati presentano alcuni difetti , quindi i risultati non sono affidabili.

Sono rimasto sorpreso dal fatto che nessuno abbia usato JMH per il benchmarking, quindi l'ho fatto.

risultati:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

Le unità sono operazioni al secondo, tanto meglio è. Codice sorgente benchmark . È stata utilizzata la macchina virtuale Java OpenJDK IcedTea 2.5.4.

Quindi, il vecchio stile (usando +) è molto più veloce.


5
Ciò sarebbe molto più semplice da interpretare se si annotasse quale fosse "+" e quale fosse "formato".
Ajahn Charles

21

Il tuo vecchio brutto stile viene compilato automaticamente da JAVAC 1.6 come:

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

Quindi non c'è assolutamente alcuna differenza tra questo e l'utilizzo di StringBuilder.

String.format è molto più pesante poiché crea un nuovo Formatter, analizza la stringa del formato di input, crea StringBuilder, aggiunge tutto ad esso e chiama toString ().


In termini di leggibilità, il codice che hai pubblicato è molto più ... ingombrante di String.format ("Cosa ottieni se moltiplichi% d per% d?", VarSix, varNine);
dusktreader,

12
Nessuna differenza tra +e StringBuilderdavvero. Sfortunatamente c'è molta disinformazione in altre risposte in questo thread. Sono quasi tentato di cambiare la domanda in how should I not be measuring performance.
CurtainDog

12

String.format di Java funziona così:

  1. analizza la stringa di formato, esplodendo in un elenco di blocchi di formato
  2. esegue l'iterazione dei blocchi di formato, eseguendo il rendering in StringBuilder, che è fondamentalmente un array che si ridimensiona secondo necessità, copiandolo in un nuovo array. questo è necessario perché non sappiamo ancora quanto grande allocare la stringa finale
  3. StringBuilder.toString () copia il suo buffer interno in una nuova stringa

se la destinazione finale di questi dati è un flusso (ad es. rendering di una pagina Web o scrittura su un file), è possibile assemblare i blocchi di formato direttamente nel flusso:

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

Immagino che l'ottimizzatore ottimizzerà l'elaborazione delle stringhe di formato. In tal caso, hai prestazioni ammortizzate equivalenti a srotolare manualmente String.format in StringBuilder.


5
Non credo che la tua speculazione sull'ottimizzazione dell'elaborazione della stringa di formato sia corretta. In alcuni test del mondo reale utilizzando Java 7, ho scoperto che l'uso String.formatdi cicli interni (in esecuzione milioni di volte) ha comportato oltre il 10% del mio tempo di esecuzione impiegato java.util.Formatter.parse(String). Questo sembra indicare che nei loop interni, dovresti evitare di chiamare Formatter.formato qualsiasi cosa lo chiami, incluso PrintStream.format(un difetto nella lib standard di Java, IMO, specialmente perché non puoi memorizzare nella cache la stringa di formato analizzata).
Andy MacKinlay il

8

Per espandere / correggere la prima risposta sopra, in realtà String.format non è di traduzione.
Ciò che String.format ti aiuterà è quando stai stampando una data / ora (o un formato numerico, ecc.), Dove ci sono differenze di localizzazione (l10n) (cioè, alcuni paesi stamperanno 04Feb2009 e altri stamperanno Feb042009).
Con la traduzione, stai solo parlando di spostare eventuali stringhe esternabili (come messaggi di errore e cosa no) in un pacchetto di proprietà in modo da poter utilizzare il pacchetto giusto per la lingua giusta, utilizzando ResourceBundle e MessageFormat.

Guardando tutto quanto sopra, direi che per quanto riguarda le prestazioni, String.format vs. semplice concatenazione si riduce a ciò che preferisci. Se preferisci guardare le chiamate a .format rispetto alla concatenazione, allora segui quello.
Dopotutto, il codice viene letto molto più di quanto sia scritto.


1
Direi che dal punto di vista delle prestazioni, String.format e semplice concatenazione si riducono a ciò che preferisci, penso che sia errato. Per quanto riguarda le prestazioni, la concatenazione è molto migliore. Per maggiori dettagli, dai un'occhiata alla mia risposta.
Adam Stelmaszczyk il

6

Nel tuo esempio, il probalby delle prestazioni non è troppo diverso ma ci sono altri problemi da considerare: la frammentazione della memoria. Anche l'operazione concatenata sta creando una nuova stringa, anche se temporanea (ci vuole tempo per GC ed è più lavoro). String.format () è solo più leggibile e comporta meno frammentazione.

Inoltre, se usi molto un determinato formato, non dimenticare che puoi usare direttamente la classe Formatter () (tutto String.format () fa un'istanza di un uso Formatter).

Inoltre, qualcos'altro di cui dovresti essere a conoscenza: fai attenzione all'uso della sottostringa (). Per esempio:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

Quella stringa di grandi dimensioni è ancora in memoria perché è così che funzionano le sottostringhe Java. Una versione migliore è:

  return new String(largeString.substring(100, 300));

o

  return String.format("%s", largeString.substring(100, 300));

Il secondo modulo è probabilmente più utile se stai facendo altre cose allo stesso tempo.


8
Vale la pena sottolineare che la "domanda correlata" è in realtà C # e quindi non applicabile.
Air

quale strumento hai usato per misurare la frammentazione della memoria e la frammentazione fa anche la differenza di velocità per ram?
kritzikratzi,

Vale la pena sottolineare che il metodo di sottostringa è stato modificato da Java 7+. Ora dovrebbe restituire una nuova rappresentazione String contenente solo i caratteri con sottostringa. Ciò significa che non è necessario restituire una chiamata String :: new
João Rebelo il

5

Generalmente dovresti usare String.Format perché è relativamente veloce e supporta la globalizzazione (supponendo che stai effettivamente cercando di scrivere qualcosa che viene letto dall'utente). Semplifica anche la globalizzazione se stai cercando di tradurre una stringa contro 3 o più per istruzione (specialmente per le lingue che hanno strutture grammaticali drasticamente diverse).

Ora, se non hai mai intenzione di tradurre qualcosa, fai affidamento sulla conversione integrata di + operatori in Java StringBuilder. O usa Java StringBuilderesplicitamente.


3

Un'altra prospettiva solo dal punto di vista della registrazione.

Vedo molte discussioni relative all'accesso a questo thread, quindi ho pensato di aggiungere la mia esperienza in risposta. Potrebbe essere qualcuno lo troverà utile.

Immagino che la motivazione del logging usando il formatter provenga dall'evitare il concatenamento delle stringhe. Fondamentalmente, non si desidera avere un overhead di string concat se non si intende registrarlo.

Non è necessario concaturare / formattare se non si desidera accedere. Diciamo se definisco un metodo come questo

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

In questo approccio il cancat / formatter non si chiama affatto se è un messaggio di debug e debugOn = false

Anche se sarà comunque meglio usare StringBuilder invece del formatter qui. La motivazione principale è quella di evitare tutto ciò.

Allo stesso tempo, non mi piace aggiungere il blocco "if" per ogni istruzione di registrazione da allora

  • Colpisce la leggibilità
  • Riduce la copertura dei test delle mie unità, il che è fonte di confusione quando si desidera assicurarsi che ogni linea sia testata.

Pertanto, preferisco creare una classe di utilità di registrazione con metodi come sopra e usarla ovunque senza preoccuparsi delle prestazioni e di eventuali altri problemi ad essa correlati.


Potresti sfruttare una libreria esistente come slf4j-api che pretende di risolvere questo caso d'uso con la sua funzione di registrazione parametrizzata? slf4j.org/faq.html#logging_performance
ammianus

2

Ho appena modificato il test di Hhafez per includere StringBuilder. StringBuilder è 33 volte più veloce di String.format usando il client jdk 1.6.0_10 su XP. L'uso dell'opzione -server riduce il fattore a 20.

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

Anche se questo può sembrare drastico, lo considero rilevante solo in rari casi, perché i numeri assoluti sono piuttosto bassi: 4 s per 1 milione di semplici chiamate String.format sono in qualche modo ok - purché le utilizzi per la registrazione o il piace.

Aggiornamento: come sottolineato da sjbotha nei commenti, il test StringBuilder non è valido, poiché manca un finale .toString().

Il fattore di accelerazione corretto da String.format(.)a StringBuilderè 23 sulla mia macchina (16 con l' -serverinterruttore).


1
Il tuo test non è valido perché non tiene conto del tempo consumato da un semplice ciclo. Dovresti includerlo e sottrarlo da tutti gli altri risultati, almeno (sì, può essere una percentuale significativa).
cletus,

L'ho fatto, il ciclo for richiede 0 ms. Ma anche se ci fosse voluto del tempo, questo non farebbe che aumentare il fattore.
the.duckman,

3
Il test StringBuilder non è valido perché alla fine non chiama toString () per fornire effettivamente una stringa che è possibile utilizzare. Ho aggiunto questo e il risultato è che StringBuilder impiega circa la stessa quantità di tempo di +. Sono sicuro che aumentando il numero di aggiunte alla fine diventerà più economico.
Sarel Botha,

1

Ecco la versione modificata della voce hhafez. Include un'opzione per la creazione di stringhe.

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

Tempo dopo per il ciclo 391 Tempo dopo per il ciclo 4163 Tempo dopo per il ciclo 227


0

La risposta dipende molto da come il tuo compilatore Java specifico ottimizza il bytecode che genera. Le stringhe sono immutabili e, teoricamente, ogni operazione "+" può crearne una nuova. Ma il compilatore quasi sicuramente ottimizza i passaggi intermedi nella creazione di stringhe lunghe. È del tutto possibile che entrambe le righe di codice sopra generino esattamente lo stesso bytecode.

L'unico vero modo per sapere è testare il codice in modo iterativo nell'ambiente attuale. Scrivi un'app QD che concatena le stringhe in entrambi i modi in modo iterativo e vedi come si scontrano.


1
Il bytecode per il secondo esempio sicuramente chiama String.format, ma sarei inorridito se lo facesse una semplice concatenazione. Perché il compilatore dovrebbe usare una stringa di formato che dovrebbe essere analizzata?
Jon Skeet,

Ho usato "bytecode" dove avrei dovuto dire "codice binario". Quando tutto si riduce a jmps e mov, potrebbe essere esattamente lo stesso codice.
Sì, quel Jake.

0

Prendi in considerazione l'utilizzo "hello".concat( "world!" )per un numero limitato di stringhe in concatenazione. Potrebbe essere persino migliore per le prestazioni rispetto ad altri approcci.

Se hai più di 3 stringhe, considera l'utilizzo di StringBuilder o solo String, a seconda del compilatore che usi.

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.