Dois-je utiliser String.format () de Java si les performances sont importantes?

216

Nous devons constamment créer des chaînes pour la sortie du journal, etc. Au fil des versions de JDK, nous avons appris quand utiliser StringBuffer(plusieurs ajouts, thread-safe) et StringBuilder(plusieurs ajouts, non-thread-safe).

Quels sont les conseils d'utilisation String.format()? Est-il efficace, ou sommes-nous obligés de nous en tenir à la concaténation pour les lignes simples où les performances sont importantes?

par exemple, vieux style laid,

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

vs nouveau style bien rangé (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 les centaines de chaînes de journaux `` à une ligne '' dans tout mon code. Ils n'impliquent pas de boucle, c'est donc StringBuildertrop lourd. Je m'intéresse String.format()spécifiquement.

Air
la source
28
Pourquoi ne le testes-tu pas?
Ed S.
1
Si vous produisez cette sortie, je suppose qu'elle doit être lisible par un humain comme un taux qu'un humain peut le lire. Disons 10 lignes par seconde au maximum. Je pense que vous constaterez que peu importe l'approche que vous adoptez, si elle est théoriquement plus lente, l'utilisateur pourrait l'apprécier. ;) Donc non, StringBuilder n'est pas lourd dans la plupart des situations.
Peter Lawrey
9
@Peter, non, ce n'est absolument pas pour lire en temps réel par les humains! Il est là pour aider à l'analyse lorsque les choses tournent mal. La sortie du journal sera généralement de milliers de lignes par seconde, elle doit donc être efficace.
Air
5
si vous produisez plusieurs milliers de lignes par seconde, je suggérerais 1) utilisez du texte plus court, même pas de texte tel que CSV ordinaire ou binaire 2) N'utilisez pas du tout de chaîne, vous pouvez écrire les données dans un ByteBuffer sans créer tout objet (sous forme de texte ou binaire) 3) d'arrière-plan l'écriture de données sur le disque ou un socket. Vous devriez pouvoir maintenir environ 1 million de lignes par seconde. (Fondamentalement, autant que votre sous-système de disque le permet), vous pouvez obtenir des rafales de 10 fois.
Peter Lawrey
7
Cela n'est pas pertinent dans le cas général, mais pour la journalisation en particulier, LogBack (écrit par l'auteur original de Log4j) a une forme de journalisation paramétrée qui résout ce problème exact - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell du

Réponses:

124

J'ai écrit une petite classe pour tester ce qui a les meilleures performances des deux et + vient avant le format. par un facteur de 5 à 6. Essayez-le 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);

    }
}

L'exécution de ce qui précède pour différents N montre que les deux se comportent linéairement, mais String.formatsont 5 à 30 fois plus lents.

La raison en est que dans l'implémentation actuelle, String.formatcommence par analyser l'entrée avec des expressions régulières, puis remplit les paramètres. La concaténation avec plus, en revanche, est optimisée par javac (pas par le JIT) et utilise StringBuilder.appenddirectement.

Comparaison de l'exécution

Hhafez
la source
12
Il y a un défaut avec ce test en ce qu'il n'est pas entièrement une bonne représentation de tout le formatage des chaînes. Il y a souvent une logique impliquée dans ce qu'il faut inclure et une logique pour formater des valeurs spécifiques en chaînes. Tout test réel doit examiner des scénarios du monde réel.
Orion Adrian le
9
Il y avait une autre question sur SO à propos de + versets StringBuffer, dans les versions récentes de Java + a été remplacé par StringBuffer lorsque cela était possible afin que les performances ne soient pas différentes
hhafez
25
Cela ressemble beaucoup au type de microbenchmark qui va être optimisé de manière très inutile.
David H.Clements
20
Un autre micro-benchmark mal mis en œuvre. Comment les deux méthodes évoluent-elles par ordre de grandeur. Que diriez-vous d'utiliser, 100, 1000, 10000, 1000000, opérations. Si vous exécutez un seul test, sur un ordre de grandeur, sur une application qui ne s'exécute pas sur un noyau isolé; il n'y a aucun moyen de dire quelle part de la différence peut être annulée en tant qu '«effets secondaires» en raison du changement de contexte, des processus d'arrière-plan, etc.
Evan Plaice
8
De plus, comme vous ne sortez jamais du JIT principal, vous ne pouvez pas entrer en jeu.
Jan Zyka
242

J'ai pris du 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 ()), de sorte que la mémoire utilisée ne sera pas affectée par d'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 exécutions 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 dans l'utilisation de la mémoire. Ceci est très important lorsque nous avons de nombreux appels de journal (ou toute autre instruction impliquant des chaînes) dans un intervalle de temps suffisamment court pour que le garbage collector 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.
Itamar
la source
8
"N'oubliez pas de vérifier le niveau de journalisation avant de construire le message", est un bon conseil, cela devrait être fait au moins pour les messages de débogage, car il pourrait y en avoir beaucoup et ils ne devraient pas être activés en production.
Stivlo
39
Non, ce n'est pas vrai. Désolé d'être franc, mais le nombre de votes positifs qu'il a attiré est tout simplement alarmant. L'utilisation de l' +opérateur compile le StringBuildercode équivalent . Les micro-benchmarks comme celui-ci ne sont pas un bon moyen de mesurer les performances - pourquoi ne pas utiliser jvisualvm, c'est dans le jdk pour une raison. String.format() sera plus lent, mais en raison du temps nécessaire pour analyser la chaîne de format plutôt que des allocations d'objets. Le report de la création d'artefacts de journalisation jusqu'à ce que vous soyez sûr qu'ils sont nécessaires est un bon conseil, mais s'il a un impact sur les performances, il est au mauvais endroit.
CurtainDog
1
@CurtainDog, votre commentaire a été fait sur un post vieux de quatre ans, pouvez-vous pointer vers la documentation ou créer une réponse distincte pour corriger la différence?
kurtzbot
1
Référence à l'appui du commentaire de @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Autrement dit, + est préférable, sauf s'il est effectué en boucle.
abricot
And a note, BTW, don't forget to check the logging level before constructing the message.n'est pas un bon conseil. En supposant que nous parlons java.util.logging.*spécifiquement, la vérification du niveau de journalisation consiste à effectuer un traitement avancé qui entraînerait des effets néfastes sur un programme dont vous ne voudriez pas lorsqu'un programme n'a pas la journalisation activée au niveau approprié. Le formatage des chaînes n'est pas du tout ce type de traitement. Le formatage fait partie de la java.util.loggingstructure et l'enregistreur lui-même vérifie le niveau d'enregistrement avant que le formateur ne soit jamais appelé.
searchengine27
30

Tous les benchmarks présentés ici ont des défauts , donc les résultats ne sont pas fiables.

J'ai été surpris que personne n'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, plus c'est mieux. Code source de référence . OpenJDK IcedTea 2.5.4 Java Virtual Machine a été utilisé.

Ainsi, l'ancien style (en utilisant +) est beaucoup plus rapide.

Adam Stelmaszczyk
la source
5
Ce serait beaucoup plus facile à interpréter si vous annotiez qui était "+" et qui était "format".
AjahnCharles
21

Votre ancien style laid est automatiquement compilé par JAVAC 1.6 comme:

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

Il n'y a donc absolument aucune différence entre cela et l'utilisation d'un StringBuilder.

String.format est beaucoup plus lourd car 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 ().

Raphaël
la source
En termes de lisibilité, le code que vous avez publié est beaucoup plus ... lourd que String.format ("Qu'est-ce que vous obtenez si vous multipliez% d par% d?", VarSix, varNine);
dusktreader
12
Aucune différence entre +et en StringBuildereffet. Malheureusement, il y a beaucoup de désinformation dans d'autres réponses de ce fil. Je suis presque tenté de changer la question en how should I not be measuring performance.
CurtainDog
12

Le String.format de Java fonctionne comme ceci:

  1. il analyse la chaîne de format, explosant dans une liste de morceaux de format
  2. il itère les morceaux de format, en les rendant dans un StringBuilder, qui est essentiellement un tableau qui se redimensionne selon les besoins, en copiant dans un nouveau tableau. cela est nécessaire car nous ne savons pas encore quelle taille allouer 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, le rendu d'une page Web ou l'écriture dans un fichier), vous pouvez assembler les morceaux de format directement dans votre flux:

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

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

Dustin Getz
la source
5
Je ne pense pas que vos spéculations sur l'optimisation du traitement des chaînes de format soient correctes. Dans certains tests réels utilisant Java 7, j'ai constaté que l'utilisation String.formatdans des boucles internes (exécutées des millions de fois) entraînait plus de 10% de mon temps d'exécution java.util.Formatter.parse(String). Cela semble indiquer que dans les boucles internes, vous devez éviter d'appeler Formatter.formatou tout ce qui l'appelle, y compris PrintStream.format(une faille dans la bibliothèque standard de Java, IMO, d'autant plus que vous ne pouvez pas mettre en cache la chaîne de format analysée).
Andy MacKinlay
8

Pour développer / corriger la première réponse ci-dessus, ce n'est pas la traduction que String.format aiderait en fait.
String.format vous aidera à imprimer une date / heure (ou un format numérique, etc.), où il y a des différences de localisation (l10n) (c'est-à-dire que certains pays imprimeront 04Feb2009 et d'autres imprimeront Feb042009).
Avec la traduction, vous parlez simplement de déplacer toutes les chaînes externalisables (comme les messages d'erreur et autres) dans un ensemble de propriétés afin que vous puissiez utiliser le bon ensemble pour la bonne langue, en utilisant ResourceBundle et MessageFormat.

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

dw.mackie
la source
1
Je dirais que les performances, String.format vs la concaténation simple se résument à ce que vous préférez, je pense que c'est incorrect. En termes de performances, la concaténation est bien meilleure. Pour plus de détails, veuillez jeter un œil à ma réponse.
Adam Stelmaszczyk
6

Dans votre exemple, les performances probalby ne sont pas trop différentes mais il y a d'autres problèmes à considérer: à savoir la fragmentation de la mémoire. Même une 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 c'est plus de travail). String.format () est juste 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 () consiste à instancier une instance de Formatter à usage unique).

Aussi, quelque chose d'autre que vous devez savoir: faites attention à utiliser 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));

Le deuxième formulaire est probablement plus utile si vous faites d'autres choses en même temps.

cletus
la source
8
Il convient de souligner que la «question connexe» est en fait C # et donc non applicable.
Air
quel outil avez-vous utilisé pour mesurer la fragmentation de la mémoire et la fragmentation fait-elle même une différence de vitesse pour le ram?
kritzikratzi
Il convient de souligner que la méthode de sous-chaîne a été modifiée à partir de Java 7 +. Il doit maintenant renvoyer une nouvelle représentation String contenant uniquement les caractères sous-chaîne. Cela signifie qu'il n'est pas nécessaire de renvoyer un appel String :: new
João Rebelo
5

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 d'écrire quelque chose qui est lu par l'utilisateur). Cela facilite également la mondialisation si vous essayez de traduire une chaîne contre 3 ou plus par instruction (en particulier pour les langues qui ont des structures grammaticales radicalement différentes).

Maintenant, si vous ne prévoyez jamais de traduire quoi que ce soit, alors vous pouvez vous fier à la conversion Java intégrée des opérateurs + StringBuilder. Ou utilisez StringBuilderexplicitement Java .

Orion Adrian
la source
3

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

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

Je suppose que la motivation de la journalisation à l'aide du formateur vient d'éviter la concaténation des chaînes. Fondamentalement, vous ne voulez pas avoir de surcharge de concaténation de chaîne si vous ne voulez pas l'enregistrer.

Vous n'avez pas vraiment besoin de concaténer / formater, 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, le cancat / formateur n'est pas vraiment appelé du tout si c'est un message de débogage et debugOn = false

Bien qu'il soit 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 "si" pour chaque instruction de journalisation car

  • 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'utilitaires de journalisation avec des méthodes comme ci-dessus et l'utiliser partout sans se soucier des performances atteintes et de tout autre problème lié.

software.wikipedia
la source
Pourriez-vous tirer parti d'une bibliothèque existante comme slf4j-api qui prétend résoudre ce cas d'utilisation avec leur fonction de journalisation paramétrée? slf4j.org/faq.html#logging_performance
ammianus
2

Je viens de modifier le test de hhafez pour inclure StringBuilder. StringBuilder est 33 fois plus rapide que String.format utilisant le client jdk 1.6.0_10 sous XP. L'utilisation du commutateur -serveur abaisse 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 drastique, je considère que cela n'est pertinent que dans de rares cas, car les nombres absolus sont assez faibles: 4 s pour 1 million d'appels String.format simples est en quelque sorte correct - tant 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 une finale .toString().

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

the.duckman
la source
1
Votre test n'est pas valide car il ne prend pas en compte le temps consommé par une simple boucle. Vous devez inclure cela et le soustraire de tous les autres résultats, au minimum (oui, il peut s'agir d'un pourcentage significatif).
cletus
Je l'ai fait, la boucle for prend 0 ms. Mais même si cela prenait du temps, cela ne ferait qu'augmenter le facteur.
the.duckman
3
Le test StringBuilder n'est pas valide car il n'appelle pas toString () à la fin pour vous donner réellement une chaîne que vous pouvez utiliser. J'ai ajouté cela et le résultat est que StringBuilder prend environ le même temps que +. Je suis sûr que si vous augmentez le nombre d'ajouts, cela deviendra finalement moins cher.
Sarel Botha
1

Voici la version modifiée de l'entrée hhafez. Il comprend une option de création de chaîne.

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

ANON
la source
0

La réponse à cela dépend beaucoup de la façon dont votre compilateur Java spécifique optimise le bytecode qu'il génère. Les chaînes sont immuables et, théoriquement, chaque opération "+" peut en créer une nouvelle. Mais, votre compilateur optimise presque certainement les étapes intermédiaires de la création 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.

La seule véritable façon de le savoir est de tester le code de manière itérative dans votre environnement actuel. Écrivez une application QD qui concatène les chaînes de manière itérative et voyez comment elles s'arrêtent les unes contre les autres.

Oui - ce Jake.
la source
1
Le bytecode du deuxième exemple appelle sûrement String.format, mais je serais horrifié si une simple concaténation le faisait. Pourquoi le compilateur utiliserait-il une chaîne de format qui devrait ensuite être analysée?
Jon Skeet
J'ai utilisé "bytecode" où j'aurais dû dire "code binaire". Quand tout se résume à jmps et movs, il peut bien s'agir du même code.
Oui - ce Jake.
0

Envisagez d'utiliser "hello".concat( "world!" )un petit nombre de chaînes dans la concaténation. Il pourrait être encore meilleur pour la performance que d'autres approches.

Si vous avez plus de 3 chaînes, pensez à utiliser StringBuilder, ou simplement String, selon le compilateur que vous utilisez.

Sasa
la source