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

Dobbiamo build sempre stringhe per l’output dei registri e così via. Sopra le versioni di JDK abbiamo imparato quando usare StringBuffer (molti appends, thread safe) e StringBuilder (molti appends, non thread-safe).

Qual è il consiglio sull’uso di String.format() ? E ‘efficiente o siamo costretti ad attenerci alla concatenazione per one-liner in cui le prestazioni sono importanti?

ad esempio brutto vecchio stile,

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

contro un nuovo stile ordinato (e possibilmente 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 log ‘one-liner’ nel mio codice. Non implicano un ciclo, quindi StringBuilder è troppo pesante. Sono interessato a String.format() particolare.

Ho scritto una piccola class per testare che ha le migliori prestazioni dei due e + arriva prima del formato. da un fattore di 5 a 6. Provatelo

 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 differenti mostra che entrambi si comportano in modo lineare, ma String.format è 5-30 volte più lento.

Il motivo è che nell'implementazione corrente String.format analizza prima l'input con espressioni regolari e quindi inserisce i parametri. La concatenazione con plus, d'altra parte, viene ottimizzata da javac (non dal JIT) e usa StringBuilder.append direttamente.

Confronto di runtime

Ho preso il codice hhafez e ho 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 ciascun 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 “Blah” + i + “Blah” + i + “Blah” + i + “Blah”.

Il risultato è il seguente (media di 5 corse ciascuna):
Tempo di avvicinamento (ms) Memoria allocata (lunga)
Operatore ‘+’ 747 320,504
String.format 16484 373,312
StringBuilder 769 57.344

Possiamo vedere che String ‘+’ e StringBuilder sono praticamente identici in termini di tempo, 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 consentire a Garbage Collector di pulire le numerose istanze di stringhe risultanti dall’operatore ‘+’.

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

conclusioni:

  1. Continuerò a usare StringBuilder.
  2. Ho troppo tempo o vita troppo piccola.

Il tuo vecchio stile brutto 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’uso di un StringBuilder.

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

Tutti i benchmark presentati qui hanno 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, più sono e meglio è. Codice sorgente di riferimento . È stata utilizzata OpenJDK IcedTea 2.5.4 Java Virtual Machine.

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

Java’s String.format funziona in questo modo:

  1. analizza la stringa di formato, esplodendo in un elenco di blocchi di formato
  2. itera i blocchi di formato, rendendolo in uno StringBuilder, che è fondamentalmente un array che si ridimensiona se necessario, copiando in un nuovo array. questo è necessario perché non sappiamo ancora quanto sia grande l’allocazione della Stringa finale
  3. StringBuilder.toString () copia il suo buffer interno in una nuova stringa

se la destinazione finale di questi dati è uno stream (ad esempio, il rendering di una pagina Web o la scrittura in un file), è ansible assemblare i blocchi di formato direttamente nel stream:

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

Suppongo che l’ottimizzatore ottimizzi l’elaborazione della stringa di formato. In tal caso, ti rimane un equivalente rendimento ammortizzato per srotolare manualmente il tuo String.format in un object StringBuilder.

Per espandere / correggere la prima risposta sopra, non è la traduzione che String.format sarebbe d’aiuto, in realtà.
L’aspetto di String.format è quando si stampa una data / ora (o un formato numerico, ecc.), Dove vi sono differenze di localizzazione (l10n) (ad esempio, alcuni paesi stamperanno 04Feb2009 e altri stamperanno il febbraio042009).
Con la traduzione, stai solo parlando di spostare qualsiasi stringa esternalizzabile (come messaggi di errore e non-cosa) in un pacchetto di proprietà in modo da poter utilizzare il pacchetto giusto per la lingua corretta, utilizzando ResourceBundle e MessageFormat.

Considerando tutto quanto sopra, direi che il formato String.format rispetto alla semplice concatenazione delle prestazioni si riduce a ciò che preferisci. Se preferisci guardare le chiamate a .format sulla concatenazione, allora con tutti i mezzi, vai con quello.
Dopotutto, il codice viene letto molto più di quanto non sia scritto.

Nel tuo esempio, le prestazioni probalby non sono molto diverse, ma ci sono altri aspetti da considerare: la frammentazione della memoria. Anche l’operazione concatenata sta creando una nuova stringa, anche se temporanea (ci vuole del tempo in GC ed è più lavoro). String.format () è appena più leggibile e comporta una minore frammentazione.

Inoltre, se si utilizza molto un formato particolare, non dimenticare che è ansible utilizzare direttamente la class Formatter () (tutto ciò che fa String.format () è un’istanza di formattazione one use).

Inoltre, qualcos’altro da tenere presente: fai attenzione all’utilizzo della sottostringa (). Per esempio:

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

Quella stringa grande è ancora in memoria perché è proprio come 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.

In genere si dovrebbe usare String.Format perché è relativamente veloce e supporta la globalizzazione (supponendo che si stia effettivamente cercando di scrivere qualcosa che viene letto dall’utente). Rende anche più facile 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 pianifichi mai di tradurre nulla, allora fai affidamento sulla conversione integrata di + operatori in StringBuilder . Oppure usa Java’s StringBuilder esplicitamente.

Un’altra prospettiva dal punto di vista del logging Only.

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 formattatore derivi dall’evitare la concatenazione delle stringhe. Fondamentalmente, non si vuole avere un sovraccarico di string concat se non si ha intenzione di registrarlo.

Non hai davvero bisogno di concatenare / formattare a meno che tu non voglia loggarti. 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 viene realmente chiamato affatto se è un messaggio di debug e debugOn = false

Anche se sarà ancora meglio usare StringBuilder invece di formattatore qui. La motivazione principale è evitare tutto ciò.

Allo stesso tempo non mi piace aggiungere il blocco “if” per ogni istruzione di logging da allora

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

Pertanto preferisco creare una class di utilità di registrazione con metodi come sopra e usarla ovunque senza preoccuparmi del rendimento della prestazione e di qualsiasi altro problema ad esso correlato.

Ho appena modificato il test di hhafez per includere StringBuilder. StringBuilder è 33 volte più veloce di String.format utilizzando il client jdk 1.6.0_10 su XP. Usando l’opzione -server si 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 potrebbe sembrare drastico, ritengo che sia rilevante solo in rari casi, perché i numeri assoluti sono piuttosto bassi: 4 s per 1 milione di semplici chiamate String.format è un po 'ok - finché li uso 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(.) StringBuilder è 23 sulla mia macchina (16 con l' String.format(.) ).

Ecco la versione modificata della voce hhafez. Include un’opzione costruttore 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 

}

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

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

L’unico vero modo per sapere è testare il codice in modo iterativo nel tuo ambiente attuale. Scrivi un’app QD che concatena le stringhe in entrambe le direzioni in modo iterativo e osserva come si scontrano l’una con l’altra.

Potresti usare "hello".concat( "world!" ) Per un piccolo numero di stringhe in concatenazione. Potrebbe essere anche migliore per le prestazioni rispetto ad altri approcci.

Se hai più di 3 stringhe, pensa di usare StringBuilder, o solo String, a seconda del compilatore che usi.