web-dev-qa-db-fra.com

Devrais-je utiliser String.format () en Java si les performances sont importantes?

Nous devons construire des chaînes tout le temps pour la sortie du journal, etc. Sur les versions de JDK, nous avons appris quand utiliser StringBuffer (beaucoup d’ajouts, thread-safe) et StringBuilder (beaucoup d’ajouts, non thread-safe).

Quel est le conseil pour utiliser String.format()? Est-ce efficace ou sommes-nous obligés de nous en tenir à la concaténation pour les doubleurs dans lesquels la performance est importante?

par exemple. vieux style moche,

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

vs nouveau style ordonné (String.format, qui est peut-être plus lent),

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

Remarque: mon cas d'utilisation spécifique est constitué de centaines de chaînes de journal "one-liner" dans mon code. Ils ne comportent pas de boucle, donc StringBuilder est un poids trop lourd. Je m'intéresse plus particulièrement à String.format().

203
Air

J'ai écrit une petite classe à tester qui présente la meilleure performance des deux et qui précède le format. par un facteur de 5 à 6. Essayez vous-même

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

    }
}

Exécuter ce qui précède pour différents N montre que les deux se comportent linéairement, mais String.format est 5 à 30 fois plus lent.

La raison en est que, dans l'implémentation actuelle, String.format analyse d'abord l'entrée avec des expressions régulières, puis remplit les paramètres. La concaténation avec plus, quant à elle, est optimisée par javac (pas par JIT) et utilise directement StringBuilder.append.

Runtime comparison

119
hhafez

J'ai pris le code hhafez et ajouté un test de mémoire :

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

J'exécute ceci séparément pour chaque approche, l'opérateur '+', String.format et StringBuilder (appelant toString ()), afin que la mémoire utilisée ne soit pas affectée par les autres approches. J'ai ajouté plus de concaténations, faisant de la chaîne "Blah" + i + "Blah" + i + "Blah" + i + "Blah".

Les résultats sont les suivants (moyenne de 5 essais chacun):
Temps d'approche (ms) Mémoire allouée (longue)
Opérateur '+' 747 320 504
String.format 16484 373.312
StringBuilder 769 57,344

Nous pouvons voir que String '+' et StringBuilder sont pratiquement identiques dans le temps, mais StringBuilder est beaucoup plus efficace en termes d'utilisation de la mémoire. Ceci est très important lorsque nous avons plusieurs appels de journal (ou toute autre instruction impliquant des chaînes) dans un intervalle de temps suffisamment court pour que le récupérateur de place ne puisse pas nettoyer les nombreuses instances de chaîne résultant de l'opérateur '+'.

Et une note, BTW, n'oubliez pas de vérifier le niveau de journalisation avant de construire le message.

Conclusions:

  1. Je continuerai à utiliser StringBuilder.
  2. J'ai trop de temps ou trop peu de vie.
235
Itamar

Tous les repères présentés ici ont quelques défauts , ainsi les résultats ne sont pas fiables.

J'ai été surpris que personne ne l'utilise JMH pour l'analyse comparative, alors je l'ai fait.

Résultats:

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

Les unités sont des opérations par seconde, le plus sera le mieux code source de référence . OpenJDK IcedTea 2.5.4 Java Une machine virtuelle a été utilisée.

Donc, le style ancien (en utilisant +) est beaucoup plus rapide.

25
Adam Stelmaszczyk

Votre ancien style laid est automatiquement compilé par JAVAC 1.6 en tant que:

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

Donc, il n'y a absolument aucune différence entre cela et l'utilisation d'un StringBuilder.

String.format est beaucoup plus lourd puisqu'il crée un nouveau formateur, analyse votre chaîne de format d'entrée, crée un StringBuilder, y ajoute tout et appelle toString ().

21
Raphaël

Java String.format fonctionne comme suit:

  1. il analyse la chaîne de format et explose en une liste de morceaux de format
  2. itère les fragments de format, le rendant dans un StringBuilder, qui est essentiellement un tableau qui se redimensionne lui-même si nécessaire, en le copiant dans un nouveau tableau. cela est nécessaire car nous ne savons pas encore quelle taille attribuer la chaîne finale
  3. StringBuilder.toString () copie son tampon interne dans une nouvelle chaîne

si la destination finale de ces données est un flux (par exemple, restituer une page Web ou écrire dans un fichier), vous pouvez assembler les fragments de format directement dans votre flux:

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

Je suppose que l'optimiseur optimisera le traitement de la chaîne de formatage. Si tel est le cas, il vous reste des performances équivalentes amorties pour dérouler manuellement votre String.format dans un StringBuilder.

12
Dustin Getz

Pour développer/corriger la première réponse ci-dessus, ce n'est pas la traduction que String.format pourrait aider, en fait.
Ce que String.format aidera à faire est lorsque vous imprimez une date/heure (ou un format numérique, etc.), où il existe des différences de localisation (l10n) (c’est-à-dire que certains pays imprimeront le 04Feb2009 et d’autres imprimer février042009).
En ce qui concerne la traduction, vous ne parlez que de déplacer des chaînes externalisables (telles que des messages d'erreur et autres contenus) dans un ensemble de propriétés afin que vous puissiez utiliser le bon ensemble pour le bon langage, à l'aide de ResourceBundle et MessageFormat.

En regardant tout ce qui précède, je dirais que la concaténation simple entre String.format et plain se résume à ce que vous préférez. Si vous préférez regarder les appels à .format plutôt que la concaténation, alors allez-y.
Après tout, le code est lu beaucoup plus qu’il n’a été écrit.

8
dw.mackie

Dans votre exemple, probalby en matière de performances n’est pas très différent, mais il faut prendre en compte d’autres problèmes, notamment la fragmentation de la mémoire. Même l'opération de concaténation crée une nouvelle chaîne, même si elle est temporaire (il faut du temps pour la GC et plus de travail). String.format () est simplement plus lisible et implique moins de fragmentation.

De plus, si vous utilisez beaucoup un format particulier, n'oubliez pas que vous pouvez utiliser directement la classe Formatter () (tout ce que fait String.format () instancie une instance de Formatter à usage unique).

En outre, vous devez être conscient de quelque chose: soyez prudent lorsque vous utilisez substring (). Par exemple:

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

Cette grande chaîne est toujours en mémoire car c'est ainsi que fonctionnent les sous-chaînes Java. Une meilleure version est:

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

ou

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

La seconde forme est probablement plus utile si vous faites d’autres choses en même temps.

6
cletus

En règle générale, vous devez utiliser String.Format car il est relativement rapide et prend en charge la mondialisation (en supposant que vous essayez réellement d'écrire quelque chose qui est lu par l'utilisateur). Cela facilite également la globalisation si vous essayez de traduire une chaîne plutôt que 3 ou plus par instruction (en particulier pour les langues ayant des structures grammaticales radicalement différentes).

Maintenant, si vous ne prévoyez jamais de traduire quoi que ce soit, faites-vous confiance à la conversion intégrée des opérateurs + en Java en StringBuilder. Ou utilisez explicitement le StringBuilder de Java.

5
Orion Adrian

ne autre perspective du point de vue de la journalisation uniquement.

Je vois beaucoup de discussions liées à la connexion sur ce fil alors pensé à ajouter mon expérience en réponse. Peut-être que quelqu'un le trouvera utile.

J'imagine que la motivation de la journalisation à l'aide du formateur provient de la nécessité d'éviter la concaténation de chaînes. Fondamentalement, vous ne voulez pas avoir une surcharge de concaténation de chaîne si vous n'allez pas le journaliser.

Vous n'avez pas vraiment besoin de concat/format sauf si vous voulez vous connecter. Disons que si je définis une méthode comme celle-ci

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

Dans cette approche, cancat/formateur n'est pas vraiment appelé du tout s'il s'agit d'un message de débogage et si debugOn = false

Cependant, il sera toujours préférable d’utiliser StringBuilder au lieu du formateur ici. La principale motivation est d'éviter tout cela.

En même temps, je n’aime pas ajouter un bloc "if" pour chaque instruction de journalisation depuis

  • Cela affecte la lisibilité
  • Réduit la couverture de mes tests unitaires - c'est déroutant lorsque vous voulez vous assurer que chaque ligne est testée.

Par conséquent, je préfère créer une classe d’utilitaire de journalisation avec les méthodes ci-dessus et l’utiliser partout, sans se soucier des performances ni des autres problèmes qui s’y rattachent.

3

Je viens de modifier le test de Hhafez pour inclure StringBuilder. StringBuilder est 33 fois plus rapide que String.format avec le client jdk 1.6.0_10 sous XP. L'utilisation du paramètre -server réduit le facteur à 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);
   }
}

Bien que cela puisse sembler radical, j'estime que ce n'est pertinent que dans de rares cas, car les nombres absolus sont assez bas: 4 s pour 1 million d'appels simples String.format est correct, pour autant que je les utilise pour la journalisation ou la comme.

Mise à jour: Comme l'a souligné sjbotha dans les commentaires, le test StringBuilder n'est pas valide car il manque un .toString() final.

Le facteur d'accélération correct de String.format(.) à StringBuilder est de 23 sur ma machine (16 avec le commutateur -server).

2
the.duckman

Voici la version modifiée de l'entrée de Hhafez. Il inclut une option constructeur de chaînes.

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

}

}

Temps après pour la boucle 391 Temps après pour la boucle 4163 Temps après pour la boucle 227

1
ANON

La réponse à cette question dépend beaucoup de la manière dont votre compilateur Java optimise le code intermédiaire qu'il génère. Les chaînes sont immuables et, en théorie, chaque opération "+" peut en créer une nouvelle. Mais votre compilateur optimise presque certainement les étapes intermédiaires de la construction de longues chaînes. Il est tout à fait possible que les deux lignes de code ci-dessus génèrent exactement le même bytecode.

Le seul moyen de savoir est de tester le code de manière itérative dans votre environnement actuel. Ecrivez une application QD qui concatène les chaînes dans les deux sens de manière itérative et voyez comment elles s'opposent.

0
Yes - that Jake.

Pensez à utiliser "hello".concat( "world!" ) pour un petit nombre de chaînes en concaténation. Cela pourrait être encore meilleur pour la performance que d’autres approches.

Si vous avez plus de 3 chaînes, envisagez d'utiliser StringBuilder ou simplement String, selon le compilateur utilisé.

0
Sasa