Devo usar o String.format () do Java se o desempenho for importante?


215

Temos que criar Strings o tempo todo para a saída do log e assim por diante. Nas versões do JDK, aprendemos quando usarStringBuffer (muitos anexos, segurança de thread) e StringBuilder(muitos anexos, não-thread-safe).

Qual é o conselho sobre o uso String.format() ? É eficiente ou somos forçados a manter a concatenação para one-liners onde o desempenho é importante?

por exemplo, feio estilo antigo,

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

vs. novo estilo organizado (String.format, que é possivelmente mais lento),

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

Nota: meu caso de uso específico são as centenas de seqüências de log 'one-liner' em todo o meu código. Eles não envolvem um loop, por isso StringBuilderé muito pesado. Estou interessado String.format()especificamente.


28
Por que você não testa?
Ed S.

1
Se você está produzindo essa saída, presumo que ela deva ser legível por um ser humano, como uma taxa que um ser humano pode ler. Vamos dizer 10 linhas por segundo, no máximo. Eu acho que você descobrirá que realmente não importa qual abordagem você adote, se for nocionalmente mais lenta, o usuário poderá apreciá-la. ;) Portanto, não, StringBuilder não é pesado na maioria das situações.
Peter Lawrey

9
@ Peter, não, absolutamente não é para leitura em tempo real por seres humanos! Está lá para ajudar na análise quando as coisas dão errado. A saída do log normalmente será de milhares de linhas por segundo, portanto, precisa ser eficiente.
Air

5
se você estiver produzindo milhares de linhas por segundo, sugiro 1) usar texto mais curto, mesmo nenhum texto como CSV comum ou binário 2) Não usar String, você pode gravar os dados em um ByteBuffer sem criar quaisquer objetos (como texto ou binário) 3) realizam a gravação de dados em disco ou soquete. Você deve conseguir sustentar cerca de 1 milhão de linhas por segundo. (Basicamente, tanto quanto o subsistema de disco permitir) Você pode obter rajadas de 10x isso.
Peter Peterrey

7
Isso não é relevante para o caso geral, mas para o log em particular, o LogBack (escrito pelo autor original do Log4j) possui uma forma de log parametrizado que soluciona esse problema exato - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell

Respostas:


122

Eu escrevi uma turma pequena para testar qual o melhor desempenho dos dois e + vem à frente do formato. por um fator de 5 a 6. Tente você mesmo

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

    }
}

A execução do acima para N diferente mostra que ambos se comportam linearmente, mas String.formatsão de 5 a 30 vezes mais lentos.

O motivo é que, na implementação atual, String.formatprimeiro analisa a entrada com expressões regulares e depois preenche os parâmetros. A concatenação com plus, por outro lado, é otimizada pelo javac (não pelo JIT) e usa StringBuilder.appenddiretamente.

Comparação de tempo de execução


12
Há uma falha neste teste, pois ela não é uma boa representação de toda a formatação de string. Freqüentemente, há lógica envolvida no que incluir e lógica para formatar valores específicos em strings. Qualquer teste real deve considerar cenários do mundo real.
Orion Adrian

9
Houve outra pergunta sobre SO sobre + versos StringBuffer, nas versões recentes do Java + foi substituído com StringBuffer quando possível para que o desempenho não seria diferente
hhafez

25
Isso se parece muito com o tipo de marca de microbench que será otimizada de uma maneira muito inútil.
David H. Clements

20
Outro micro-benchmark mal implementado. Como os dois métodos são escalonados por ordens de magnitude. Que tal usar operações 100, 1000, 10000, 1000000. Se você executar apenas um teste, em uma ordem de magnitude, em um aplicativo que não esteja sendo executado em um núcleo isolado; não há nenhuma maneira de dizer o quanto da diferença pode ser escrito como 'efeitos colaterais', devido à mudança de contexto, os processos de fundo, etc.
Evan Solha

8
Além disso, como você não precisa nem sair de JIT principal não pode chutar.
Jan Zyka

241

Tomei hhafez código e acrescentou um teste de memória :

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

Eu executo isso separadamente para cada abordagem, o operador '+', String.format e StringBuilder (chamando toString ()), para que a memória usada não seja afetada por outras abordagens. Adicionei mais concatenações, criando a string como "Blah" + i + "Blah" + i + "Blah" + i + "Blah".

O resultado é o seguinte (média de 5 execuções cada):
Tempo de Aproximação (ms) Memória alocada (longa)
operador '+' 747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

Podemos ver que String '+' e StringBuilder são praticamente idênticos em termos de tempo, mas StringBuilder é muito mais eficiente no uso de memória. Isso é muito importante quando temos muitas chamadas de log (ou quaisquer outras instruções que envolvem cadeias) em um intervalo de tempo suficientemente curto para que o Garbage Collector não consiga limpar as muitas instâncias de cadeias resultantes do operador '+'.

E uma observação, BTW, não se esqueça de verificar o nível de log antes de construir a mensagem.

Conclusões:

  1. Vou continuar usando o StringBuilder.
  2. Eu tenho muito tempo ou muito pouca vida.

8
"não se esqueça de verificar o nível de log antes de construir a mensagem", é um bom conselho, isso deve ser feito pelo menos para mensagens de depuração, porque pode haver muitas delas e elas não devem ser ativadas na produção.
stivlo

39
Não, isso não está certo. Desculpe por ser franco, mas o número de votos que atraiu é nada menos que alarmante. O uso do +operador compila para o StringBuildercódigo equivalente . Marcas de micropreenchimento como essa não são uma boa maneira de medir o desempenho - por que não usar o jvisualvm, ele está no jdk por um motivo. String.format() será mais lento, mas devido ao tempo para analisar a sequência de formatação, em vez de qualquer alocação de objeto. Adiar a criação de artefatos de registro até que você tenha certeza de que eles são necessários é um bom conselho, mas se isso tiver um impacto no desempenho, ele estará no lugar errado.
CurtainDog

1
@CurtainDog, seu comentário foi feito em um post de quatro anos, você pode apontar para a documentação ou criar uma resposta separada para resolver a diferença?
Kurtzbot

1
Referência no suporte do comentário do @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Ou seja, + é preferível, a menos que seja feito em um loop.
Apricot

And a note, BTW, don't forget to check the logging level before constructing the message.não é um bom conselho. Supondo que estamos falando java.util.logging.*especificamente, verificar o nível de log é quando você está falando sobre fazer processamento avançado que causaria efeitos adversos em um programa que você não desejaria quando um programa não tivesse o log ativado no nível apropriado. A formatação de string não é esse tipo de processamento. A formatação faz parte da java.util.loggingestrutura, e o próprio criador de logs verifica o nível de criação de log antes que o formatador seja chamado.
usar o seguinte código

30

Todos os benchmarks apresentados aqui apresentam algumas falhas , portanto, os resultados não são confiáveis.

Fiquei surpreso que ninguém usasse o JMH para fazer benchmarking, então usei .

Resultados:

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

Unidades são operações por segundo, quanto mais, melhor. Código fonte de referência . Foi utilizada a Java Virtual Machine do OpenJDK IcedTea 2.5.4.

Portanto, o estilo antigo (usando +) é muito mais rápido.


5
Isso seria muito mais fácil de interpretar se você anotasse qual era "+" e qual era "formato".
AjahnCharles

21

Seu velho estilo feio é compilado automaticamente pelo JAVAC 1.6 como:

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

Portanto, não há absolutamente nenhuma diferença entre isso e o uso de um StringBuilder.

String.format é muito mais pesado, pois cria um novo Formatador, analisa sua string de formato de entrada, cria um StringBuilder, acrescenta tudo a ele e chama toString ().


Em termos de legibilidade, o código que você postou é muito mais ... complicado que String.format ("O que você ganha se multiplicar% d por% d?", VarSix, varNine);
Dusktreader 23/08/12

12
Não há diferença entre +e de StringBuilderfato. Infelizmente, há muitas informações erradas em outras respostas neste tópico. Estou quase tentado a mudar a pergunta how should I not be measuring performance.
precisa saber é o seguinte

12

O String.format do Java funciona assim:

  1. analisa a cadeia de formato, explodindo em uma lista de blocos de formato
  2. itera os pedaços de formato, renderizando em um StringBuilder, que é basicamente uma matriz que se redimensiona conforme necessário, copiando para uma nova matriz. isso é necessário porque ainda não sabemos o tamanho da alocação da String final
  3. StringBuilder.toString () copia seu buffer interno em uma nova String

se o destino final desses dados for um fluxo (por exemplo, renderizar uma página da Web ou gravar em um arquivo), você poderá montar os pedaços de formato diretamente no seu fluxo:

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

Especulo que o otimizador otimizará o processamento da string de formato. Nesse caso, você terá um desempenho amortizado equivalente para desenrolar manualmente seu String.format em um StringBuilder.


5
Eu não acho que sua especulação sobre a otimização do processamento da string de formato esteja correta. Em alguns testes do mundo real usando o Java 7, descobri que usar String.formatloops internos (executando milhões de vezes) resultou em mais de 10% do meu tempo de execução gasto java.util.Formatter.parse(String). Isso parece indicar que, em loops internos, você deve evitar chamar Formatter.formatou qualquer coisa que o chamar, incluindo PrintStream.format(uma falha na lib padrão do Java, IMO, especialmente porque você não pode armazenar em cache a sequência de formato analisada).
Andy MacKinlay

8

Para expandir / corrigir a primeira resposta acima, não é a tradução com a qual String.format ajudaria.
O que o String.format ajudará é quando você estiver imprimindo uma data / hora (ou um formato numérico, etc.), onde existem diferenças de localização (l10n) (ou seja, alguns países imprimirão 04Feb2009 e outros imprimirão Feb042009).
Com a tradução, você está apenas falando sobre mover quaisquer strings externalizáveis ​​(como mensagens de erro e outras coisas) para um pacote configurável de propriedades, para que você possa usar o pacote correto para o idioma certo, usando ResourceBundle e MessageFormat.

Observando tudo o que foi dito acima, eu diria que a concatenação String.format versus planilha se resume ao que você preferir. Se você preferir olhar as chamadas para .format do que a concatenação, faça isso de qualquer maneira.
Afinal, o código é lido muito mais do que está escrito.


1
Eu diria que, em termos de desempenho, String.format vs. concatenação simples se resume ao que você prefere , acho que isso está incorreto. Em termos de desempenho, a concatenação é muito melhor. Para mais detalhes, dê uma olhada na minha resposta.
Adam Stelmaszczyk

6

No seu exemplo, o desempenho provavelmente não é muito diferente, mas há outros problemas a serem considerados: a fragmentação da memória. Até a operação concatenada está criando uma nova string, mesmo que seja temporária (leva tempo para GC e é mais trabalho). String.format () é apenas mais legível e envolve menos fragmentação.

Além disso, se você estiver usando muito um determinado formato, não esqueça que pode usar a classe Formatter () diretamente (tudo o que String.format () faz é instanciar uma instância do Formatter de uso único).

Além disso, você deve estar ciente de outra coisa: tenha cuidado ao usar substring (). Por exemplo:

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

Essa cadeia grande ainda está na memória, porque é assim que as substrings Java funcionam. Uma versão melhor é:

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

ou

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

A segunda forma é provavelmente mais útil se você estiver fazendo outras coisas ao mesmo tempo.


8
Vale ressaltar a "questão relacionada" é realmente C # e, portanto, não é aplicável.
Air

qual ferramenta você usou para medir a fragmentação da memória e a fragmentação faz diferença na velocidade do RAM?
kritzikratzi

Vale ressaltar que o método de substring foi alterado do Java 7 +. Agora, ele deve retornar uma nova representação de String contendo apenas os caracteres substringed. Isso significa que não há nenhuma necessidade de retornar uma chamada de Cordas :: nova
João Rebelo

5

Geralmente você deve usar String.Format porque é relativamente rápido e suporta a globalização (supondo que você esteja realmente tentando escrever algo que é lido pelo usuário). Também facilita a globalização se você estiver tentando traduzir uma string em vez de 3 ou mais por instrução (especialmente para idiomas que possuem estruturas gramaticais drasticamente diferentes).

Agora, se você nunca planeja traduzir nada, confie na conversão incorporada de Java dos operadores + em StringBuilder. Ou use StringBuilderexplicitamente o Java .


3

Outra perspectiva apenas do ponto de vista de registro.

Eu vejo muitas discussões relacionadas ao logon neste tópico, então pensei em adicionar minha experiência em resposta. Pode ser que alguém ache útil.

Eu acho que a motivação do log usando o formatador vem de evitar a concatenação de strings. Basicamente, você não deseja ter uma sobrecarga de concat de strings se não quiser registrá-lo.

Você realmente não precisa concat / formatar, a menos que queira fazer logon. Digamos que se eu definir um método como este

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

Nesta abordagem, o cancelador / formatador não é realmente chamado se for uma mensagem de depuração e debugOn = false

Embora ainda seja melhor usar o StringBuilder em vez do formatador aqui. A principal motivação é evitar nada disso.

Ao mesmo tempo, não gosto de adicionar o bloco "if" para cada instrução de log desde

  • Afeta a legibilidade
  • Reduz a cobertura nos meus testes de unidade - isso é confuso quando você deseja garantir que todas as linhas sejam testadas.

Portanto, prefiro criar uma classe de utilitário de log com métodos como acima e usá-la em qualquer lugar sem se preocupar com o desempenho atingido e quaisquer outros problemas relacionados a ela.


Você poderia aproveitar uma biblioteca existente como slf4j-api, que pretende resolver esse caso de uso com o recurso de registro parametrizado? slf4j.org/faq.html#logging_performance
ammianus

2

Acabei de modificar o teste de Hhafez para incluir o StringBuilder. O StringBuilder é 33 vezes mais rápido que o String.format usando o cliente jdk 1.6.0_10 no XP. O uso da opção -server reduz o fator para 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);
   }
}

Embora isso possa parecer drástico, considero relevante apenas em casos raros, porque os números absolutos são bastante baixos: 4 s para 1 milhão de chamadas String.format simples é uma espécie de ok - desde que eu os use para registrar ou gostar.

Atualização: Como apontado por sjbotha nos comentários, o teste StringBuilder é inválido, pois está faltando uma final .toString().

O fator de aceleração correto de String.format(.)a StringBuilderé 23 na minha máquina (16 com a -serverchave).


1
Seu teste é inválido porque não leva em consideração o tempo gasto apenas com um loop. Você deve incluir isso e subtraí-lo de todos os outros resultados, no mínimo (sim, pode ser uma porcentagem significativa).
Cletus

Eu fiz isso, o loop for leva 0 ms. Mas, mesmo que demorasse algum tempo, isso apenas aumentaria o fator.
the.duckman

3
O teste StringBuilder é inválido porque não chama toString () no final para realmente fornecer uma String que você pode usar. Adicionei isso e o resultado é que o StringBuilder leva aproximadamente a mesma quantidade de tempo que o +. Tenho certeza de que, à medida que você aumenta o número de anexos, ele se tornará mais barato.
Sarel Botha

1

Aqui está a versão modificada da entrada hhafez. Inclui uma opção do construtor de cadeias.

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

}

}

Time after for loop 391 Time after for loop 4163 Time after for loop 227


0

A resposta para isso depende muito de como o seu compilador Java específico otimiza o bytecode gerado. Strings são imutáveis ​​e, teoricamente, cada operação "+" pode criar uma nova. Mas seu compilador quase certamente otimiza as etapas intermediárias na construção de seqüências longas. É perfeitamente possível que ambas as linhas de código acima gerem exatamente o mesmo bytecode.

A única maneira real de saber é testar o código iterativamente no seu ambiente atual. Escreva um aplicativo QD que concatene seqüências de caracteres nos dois sentidos de forma iterativa e veja como elas se esgotam.


1
O bytecode para o segundo exemplo certamente chama String.format, mas ficaria horrorizado se uma simples concatenação o fizesse. Por que o compilador usaria uma string de formato que teria que ser analisada?
911 Jon Skeet

Eu usei "bytecode" onde deveria ter dito "código binário". Quando tudo se resume a jmps e movs, pode muito bem ser o mesmo código.
Sim - esse Jake.

0

Considere usar "hello".concat( "world!" )para um pequeno número de seqüências de caracteres na concatenação. Poderia ser ainda melhor para desempenho do que outras abordagens.

Se você tiver mais de três seqüências, considere usar StringBuilder ou apenas String, dependendo do compilador que você usa.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.